Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

How to Merge PDF Files in the Browser Without Server Uploads (Zero-Server Architecture)

Most web applications that manipulate PDF files follow an outdated pattern: The user uploads confidential tax forms, bank statements, or medical records to a remote server. An expensive backend microservice (running Py

Most web applications that manipulate PDF files follow an outdated pattern:

  1. The user uploads confidential tax forms, bank statements, or medical records to a remote server.
  2. An expensive backend microservice (running Python with PyPDF2, or headless Chrome) processes the file.
  3. The server stores the temporary output, incurs disk I/O and egress bandwidth costs, and returns a download link.

In 2026, sending private PDFs across the network just to merge or split pages is an anti-pattern. Modern client devices (laptops, phones, and tablets) have more than enough compute power and memory to manipulate PDFs locally in memory.

In this guide, we'll build a 100% client-side PDF merger using JavaScript and pdf-lib. No files ever touch a server, meaning zero bandwidth costs for your backend, zero storage liabilities, and instant GDPR/HIPAA compliance by design.

Why Client-Side PDF Processing Wins

Before diving into code, consider the architectural benefits of processing PDFs directly in the browser:

  • Absolute User Privacy: Confidential documents (passwords, contracts, IDs) never traverse external networks or sit in transient cloud buckets.
  • $0 Infrastructure & Egress Bills: Your server only delivers static JavaScript and HTML bundles (served over a CDN). You never pay for CPU rendering or file storage.
  • Offline / Edge Capable: Once the bundle is cached via a Service Worker, users can merge PDFs even on airplanes or in spotty subway Wi-Fi.

(We recently integrated this zero-server architecture into UtilifyAI's Free PDF Suite to handle unlimited document combinations without requiring user sign-ups or server uploads).

Step 1: Installing / Importing pdf-lib

pdf-lib is a lightweight, pure TypeScript/JavaScript library for creating and modifying PDF documents in any modern browser without native binary dependencies.

If you are using npm/Next.js/Vite:

npm install pdf-lib

If you are using vanilla HTML/JS, load it from a CDN:

<script src="https://unpkg.com/[email protected]/dist/pdf-lib.min.js"></script>

Step 2: The Core PDF Merging Function

Here is the complete asynchronous function that takes an array of browser File objects (e.g. from an <input type="file" multiple> or drag-and-drop zone) and merges them into a single downloadable PDF blob.

import { PDFDocument } from 'pdf-lib';

/**
 * Merges multiple PDF files entirely within the client's browser memory.
 * @param {File[]} pdfFiles - Array of File objects selected by the user.
 * @param {Function} onProgress - Optional callback reporting progress (0 to 100).
 * @returns {Promise<Blob>} Merged PDF as an application/pdf Blob.
 */
export async function mergePDFsInBrowser(pdfFiles, onProgress) {
  if (!pdfFiles || pdfFiles.length < 2) {
    throw new Error('Please select at least two PDF documents to merge.');
  }

  // 1. Create a fresh target PDF document
  const mergedPdf = await PDFDocument.create();
  const totalFiles = pdfFiles.length;

  for (let i = 0; i < totalFiles; i++) {
    const file = pdfFiles[i];

    // Read the File as an ArrayBuffer directly from browser memory
    const fileBuffer = await file.arrayBuffer();

    // Load the source PDF into pdf-lib (ignore encryption warnings if permitted)
    const sourcePdf = await PDFDocument.load(fileBuffer, { 
      ignoreEncryption: true 
    });

    // Copy all pages from the source document to the target
    const pageIndices = sourcePdf.getPageIndices();
    const copiedPages = await mergedPdf.copyPages(sourcePdf, pageIndices);

    for (const page of copiedPages) {
      mergedPdf.addPage(page);
    }

    // Report progress if callback is provided
    if (onProgress) {
      onProgress(Math.round(((i + 1) / totalFiles) * 100));
    }
  }

  // 2. Serialize the merged PDF into a Uint8Array
  const mergedPdfBytes = await mergedPdf.save();

  // 3. Return as a native browser Blob
  return new Blob([mergedPdfBytes], { type: 'application/pdf' });
}

Step 3: Triggering a Download Without Server Round-Trips

Once you have the merged Blob, you can trigger an instant browser download using standard DOM URL APIs:

/**
 * Triggers a native browser file download for a generated Blob.
 * @param {Blob} blob - The merged PDF Blob.
 * @param {string} filename - Desired output filename.
 */
export function downloadBlob(blob, filename = 'merged-document.pdf') {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement('a');
  anchor.href = url;
  anchor.download = filename;
  document.body.appendChild(anchor);
  anchor.click();

  // Cleanup object URL from memory after download triggers
  setTimeout(() => {
    document.body.removeChild(anchor);
    URL.revokeObjectURL(url);
  }, 100);
}

Step 4: Putting It Together with Interactive UI

Here is a minimal React/Next.js component with progress indication and drag-and-drop support:

import React, { useState } from 'react';
import { mergePDFsInBrowser, downloadBlob } from './pdfMerger';

export default function ClientPdfMerger() {
  const [files, setFiles] = useState([]);
  const [isProcessing, setIsProcessing] = useState(false);
  const [progress, setProgress] = useState(0);

  const handleFileChange = (e) => {
    setFiles(Array.from(e.target.files));
  };

  const handleMerge = async () => {
    if (files.length < 2) return;
    setIsProcessing(true);
    setProgress(0);

    try {
      const mergedBlob = await mergePDFsInBrowser(files, (p) => setProgress(p));
      downloadBlob(mergedBlob, 'merged_utilifyai.pdf');
    } catch (err) {
      console.error('Merge failed:', err);
      alert('Failed to merge documents: ' + err.message);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <div className="p-6 border border-slate-700 rounded-xl bg-slate-900 text-white max-w-lg mx-auto">
      <h2 className="text-xl font-bold mb-4">Merge PDFs In Your Browser</h2>
      <input
        type="file"
        multiple
        accept="application/pdf"
        onChange={handleFileChange}
        className="mb-4 block w-full text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:bg-blue-600 file:text-white"
      />

      {files.length > 0 && (
        <p className="text-sm text-slate-300 mb-4">{files.length} documents selected.</p>
      )}

      <button
        onClick={handleMerge}
        disabled={isProcessing || files.length < 2}
        className="w-full py-2.5 px-4 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 font-semibold rounded-lg transition"
      >
        {isProcessing ? `Merging (${progress}%)...` : 'Merge & Download'}
      </button>
    </div>
  );
}

Memory Optimization & Edge Cases

When working in browser memory, keep these two tips in mind:

  1. Handling 100MB+ Documents: If users merge massive scans, JavaScript heaps can exceed memory caps on low-end mobile devices. To avoid memory crashes, batch page copies and release ArrayBuffers as soon as each document is copied.
  2. Password Protected / Restricted PDFs: If a source PDF has permissions restrictions, standard loaders may throw errors. Setting ignoreEncryption: true in pdf-lib allows merging unrestricted pages, while encrypted user passwords must be supplied or unlocked beforehand using a client-side PDF unlock utility.

Summary

By taking advantage of modern JavaScript and WebAssembly capabilities, you eliminate backend infrastructure, provide instantaneous response times, and offer enterprise-grade privacy to your users.

If you'd like to test this in action without writing boilerplate code, test the production implementation over at UtilifyAI PDF Merge Tool.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.