DEV Community

Armand al-farizy
Armand al-farizy

Posted on

Evaluating Client-Side Document Processing in Next.js: Architectural Trade-offs

Introduction

When building document utility applications, developers inevitably face a critical architectural crossroad: Should file manipulation occur on a backend server, or directly within the user's browser?

Historically, heavy lifting was always delegated to the server. However, with the rise of strict data privacy regulations (like GDPR) and the increasing power of modern browsers, client-side processing—often referred to as the Local-First approach—has become a highly attractive proposition.

To evaluate the true viability of this architecture, I built a Next.js application designed to merge, split, and manipulate PDF documents entirely in the browser using JavaScript. The goal was simple: zero server compute costs and absolute data privacy.

In this article, we will examine the mechanics of client-side PDF manipulation, walk through a core implementation using pdf-lib, and critically analyze the severe technical bottlenecks developers must consider before adopting this architecture for production workloads.

The Appeal of the Local-First Architecture

Before diving into the code, it is important to understand why companies are pushing for in-browser compute:

  1. Absolute Privacy: Sensitive documents (medical records, legal contracts) never leave the user's local machine. This mitigates massive legal liabilities for the developer.
  2. Zero Compute Costs: By shifting the processing load to the client's CPU and RAM, cloud hosting bills are reduced to practically nothing. You only pay to serve the static frontend assets.
  3. Offline Capabilities: Once the JavaScript bundle is loaded, the application can function entirely offline.

"The best way to secure user data is to never collect it in the first place."

The Implementation: Merging PDFs in Next.js

To handle PDF manipulation without a Node.js or Python backend, the browser needs to read the physical file into its memory as an ArrayBuffer. We can then use libraries like pdf-lib to modify the binary data.

Here is a core implementation within a Next.js environment. This function takes an array of uploaded files, merges them, and prepares a new document for download.

import { PDFDocument } from 'pdf-lib';

/**
 * Merges multiple PDF files entirely on the client side.
 * @param {File[]} fileList - Array of File objects from an HTML file input.
 * @returns {Promise<Uint8Array>} - The merged PDF as a byte array ready for download.
 */
export async function mergePDFsClientSide(fileList) {
  try {
    // 1. Initialize a new, empty PDF document
    const mergedPdf = await PDFDocument.create();

    // 2. Iterate through each uploaded file
    for (const file of fileList) {
      // Read the file into browser memory
      const arrayBuffer = await file.arrayBuffer();
      const loadedPdf = await PDFDocument.load(arrayBuffer);

      // Extract all pages from the current document
      const pageIndices = loadedPdf.getPageIndices();
      const copiedPages = await mergedPdf.copyPages(loadedPdf, pageIndices);

      // 3. Append copied pages to our new canvas
      copiedPages.forEach((page) => mergedPdf.addPage(page));
    }

    // 4. Serialize the PDFDocument to bytes (a Uint8Array)
    const pdfBytes = await mergedPdf.save();
    return pdfBytes;

  } catch (error) {
    console.error("Failed to merge documents:", error);
    throw new Error("Client-side merging failed.");
  }
}
Enter fullscreen mode Exit fullscreen mode

Triggering the Download

Once the Uint8Array is generated, we can force the browser to download it using a Blob URL:

const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'merged-document.pdf';
link.click();
URL.revokeObjectURL(url); // Clean up memory
Enter fullscreen mode Exit fullscreen mode

The Reality Check: Where Client-Side Falls Short

While the implementation above works flawlessly for lightweight, text-based files, rigorous testing reveals significant bottlenecks that make pure client-side processing dangerous for heavy workloads.

  1. Memory Heap Limitations (The Silent Crash): Browsers enforce strict, hard-coded limits on the amount of RAM a single tab can consume (often around 2GB to 4GB depending on the browser and OS). When a user attempts to merge large, image-heavy PDFs (e.g., a 100MB scanned document), the browser must load the entire uncompressed data into its memory heap. This frequently leads to severe UI freezing, thread blocking, and eventual browser crashes with the dreaded "Out of Memory" error. There is no graceful way to catch this error in JavaScript; the tab simply dies.

  2. Single-Threaded UI Blocking: JavaScript executes on a single main thread. Heavy mathematical operations—like parsing and serializing complex PDF binary trees—will completely block the UI thread. Even within a highly optimized framework like Next.js, unless this workload is intentionally offloaded to Web Workers, the entire application becomes unresponsive. Animations freeze, buttons cannot be clicked, and the user assumes the app is broken.

  3. Format Conversion is a Nightmare: Merging PDFs is one thing, but converting a .docx (Word document) into a .pdf purely via client-side JavaScript is highly inefficient. Word documents are essentially complex XML archives. Browsers lack the native rendering engines required to interpret complex XML structures, pagination rules, and proprietary fonts accurately. Attempting this on the client side usually results in broken layouts and missing text. Robust document conversion inherently requires heavy backend dependencies like headless browsers (Puppeteer) or LibreOffice binaries.

Architectural Verdict

Building a purely client-side document processor highlights a clear dividing line in system design. Here is when you should use each approach:

Choose Client-Side (Browser) When:

  • The expected file sizes are strictly small (under 10MB).
  • Absolute data privacy is the core selling point of your application.
  • You want to eliminate server processing costs entirely.

Choose Server-Side (Node.js/Python) When:

  • You expect large, image-heavy files.
  • You need to perform complex format conversions (e.g., Word/Excel to PDF).
  • You require stable, predictable performance regardless of the user's hardware.

Conclusion
The local-first approach is incredibly powerful for privacy-centric utility apps. However, developers must be acutely aware of browser memory limits and the single-threaded nature of JavaScript. For enterprise-grade applications, offloading compute to a dedicated server—or leveraging WebAssembly (Wasm) for near-native in-browser performance—remains mandatory to ensure application stability.

What are your thoughts?
Have you ever tried pushing the limits of client-side processing in your Next.js apps, or do you strictly rely on backend architectures for heavy tasks? Let’s discuss the trade-offs in the comments below!

Top comments (0)