Back to KB

reduce((acc, e) => acc + e.buffer.byteLength, 0) / 1024 / 1024

Difficulty
Intermediate
Read Time
86 min

Zero-Server PDF Assembly: Building a Client-Side Composition Pipeline with Vue 3 and pdf-lib

By Codcompass TeamΒ·Β·86 min read

Zero-Server PDF Assembly: Building a Client-Side Composition Pipeline with Vue 3 and pdf-lib

Current Situation Analysis

Traditional PDF merging workflows default to server-side processing. The pattern is familiar: users upload files, the backend queues them, a CLI tool or library processes the bytes, and the result is streamed back. While straightforward to implement, this approach introduces hidden operational debt. Every uploaded document requires ephemeral storage, virus scanning, access control, and a deletion queue to comply with data retention policies. For privacy-sensitive industries, this model creates compliance friction and increases infrastructure costs proportional to upload volume.

The misconception driving this pattern is that browsers lack the computational capability to handle binary document manipulation. In reality, modern JavaScript engines can parse, mutate, and serialize PDF structures entirely in memory. The shift to client-side composition isn't just a convenience feature; it's an architectural decision that eliminates data residency risks, removes upload bandwidth bottlenecks, and scales horizontally without provisioning additional compute nodes.

The trade-off is deterministic: client-side processing is bounded by the user's device RAM and single-threaded execution limits. For typical office documents, contracts, and invoices (usually under 50MB), this constraint is negligible. However, developers often overlook the need for explicit memory management, thumbnail rendering pipelines, and state immutability when building these tools. The result is either silent failures on large files or UI thread blocking during preview generation.

WOW Moment: Key Findings

Shifting PDF composition to the client fundamentally changes the cost and latency profile of document workflows. The following comparison highlights the operational impact of moving from a traditional backend pipeline to a browser-native approach.

ApproachInfrastructure CostData ResidencyFirst-Byte LatencyScalability ModelHardware Dependency
Server-Side MergeHigh (storage, scanning, egress)Third-party controlled200ms–2s (upload + queue)Vertical/Horizontal scaling requiredNone (server handles load)
Client-Side MergeZero (compute shifts to user)User device only<50ms (local parsing)Infinite (no server limits)Bounded by client RAM/CPU

This finding matters because it decouples document processing from backend capacity planning. Teams can ship privacy-first tools without provisioning S3 buckets, Lambda functions, or deletion cron jobs. The browser becomes the execution environment, and the only constraint is the user's willingness to allocate memory for the task.

Core Solution

Building a client-side PDF composer requires separating three distinct concerns: file ingestion, visual reordering, and binary composition. Each layer operates independently to prevent state corruption and maintain UI responsiveness.

1. File Ingestion & State Management

The first step is reading uploaded files into ArrayBuffer instances and parsing them with pdf-lib. Unlike server-side tools that stream bytes, pdf-lib requires the entire document in memory. We wrap this in a Vue 3 composable that maintains an immutable registry of loaded documents.

import { ref, readonly } from 'vue'
import { PDFDocument } from 'pdf-lib'

export interface LoadedDocument {
  registryId: string
  fileName: string
  pageCount: number
  rawBuffer: ArrayBuffer
  parsedDoc: PDFDocument
}

export function usePdfRegistry() {
  const registry = ref<LoadedDocument[]>([])

  async function ingestFiles(fileList: FileList) {
    const entries = Array.from(fileList).filter(f => f.type === 'application/pdf')
    
    for (const file of entries) {
      const buffer = await file.arrayBuffer()
      const parsed = await PDFDocument.load(buffer)
      
      registry.value.push({
        registryId: crypto.randomUUID(),
        fileName: file.name,
        pageCount: parsed.getPageCount(),
        rawBuffer: buffer,
        parsedDoc: parsed
      })
    }
  }

  return {
    regis

πŸŽ‰ Mid-Year Sale β€” Unlock Full Article

Base plan from just $4.99/mo or $49/yr

Sign in to read the full article and unlock all 635+ tutorials.

Sign In / Register β€” Start Free Trial

7-day free trial Β· Cancel anytime Β· 30-day money-back