Batch processing PDFs sounds like a server-side job, but with pdf-lib and a little patience, you can build it entirely in the browser. This post walks through a minimal but production-ready batch PDF processor.
Why client-side batch?
Server-side batch processing works, but it introduces latency, bandwidth costs, and privacy risk. A browser-based batch processor:
- Keeps files on the user's device
- Processes files without uploading anything
- Avoids server-side PDF processing entirely
- Handles large batches without queueing
The stack
- Vue 3 with Composition API
- pdf-lib for PDF manipulation
- JSZip for packaging results
- Native File API for reading uploads
Minimal implementation
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument } from 'pdf-lib'
import JSZip from 'jszip'
const files = ref<File[]>([])
const processing = ref(false)
const results = ref<Blob[]>([])
async function processBatch() {
processing.value = true
results.value = []
try {
for (const file of files.value) {
const bytes = await file.arrayBuffer()
const pdf = await PDFDocument.load(bytes)
// Example: compress by removing metadata
const modifiedBytes = await pdf.save({
useObjectStreams: false,
addDefaultPage: false,
objectsPerTick: 50,
maxIterations: 2,
})
results.value.push(new Blob([modifiedBytes], { type: 'application/pdf' }))
}
// Package all results into a ZIP
const zip = new JSZip()
results.value.forEach((blob, i) => {
zip.file(`output_${i + 1}.pdf`, blob)
})
const zipBlob = await zip.generateAsync({ type: 'blob' })
downloadBlob(zipBlob, 'batch-output.zip')
} finally {
processing.value = false
}
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
</script>
The key is pdf.save({ useObjectStreams: false, objectsPerTick: 50 }). Setting useObjectStreams: false produces slightly larger files but is faster to save. objectsPerTick: 50 controls how many objects are processed per microtask, preventing the UI from freezing during large batches.
Handling large batches
Browser memory is the main constraint. For a production tool:
const MAX_FILES = 100
const MAX_TOTAL_SIZE = 500 * 1024 * 1024 // 500 MB
Validate before processing. Show a progress indicator. And always wrap the save in a try/catch — one bad file should not crash the entire batch.
UX tips from a live tool
At en.sotool.top/batch-merge, we learned a few things from real users:
- Show a progress bar. Users want to know how far along they are.
- Allow cancellation. If a batch is taking too long, let users bail.
- Package results as ZIP. Nobody wants to download 50 separate files.
- Track events separately. Measure drop-off between upload, processing, and download.
Tracking the funnel
We use GA4 custom events:
onFileUpload(files)
onActionClick('batch-merge')
onCompleted({ file_count: files.length })
onDownload({ file_count: files.length })
This lets us see exactly where users drop off.
Going further
For a simple batch processor, pdf-lib is enough. If you need OCR, bookmarks, or form flattening, you'll want a server-side component or a desktop tool.
Want to see the full source? The site is built in public at github.com/sunshey/pdf-tool.
If you need desktop-grade PDF editing — batch processing with advanced settings, OCR, or form management — check out Wondershare PDFelement.
Top comments (0)