DEV Community

nikoo li
nikoo li

Posted on

Processing 100MB PDFs in the Browser: The Performance Optimizations That Made TinyPDF Usable

Processing 100MB PDFs in the Browser: The Performance Optimizations That Made TinyPDF Usable

When I built TinyPDF (https://tinypdf.cn/?utm_source=devto&utm_medium=blog&utm_campaign=performance_optimization&utm_content=devto_performance_2026-07-28), I had one hard rule: no backend.

Everything had to run in the browser. No file uploads, no servers, no costs. Just drag, drop, compress, download.

But when I tested the first version with a real portfolio—88MB, 45 pages, full of high-res images—it froze the tab for 12 seconds.

Here's what I changed to get that down to 2 seconds, without losing any features.


1. Use Web Workers for PDF Parsing (Don't Block the Main Thread)

The first mistake: I ran PDF.js parsing directly on the main thread.

// ❌ Bad: Blocks UI while parsing
const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
Enter fullscreen mode Exit fullscreen mode

The fix: Offload everything to a Web Worker. The main thread only handles user input and progress updates.

// ✅ Good: Web Worker does the heavy lifting
// Main thread
const worker = new Worker('pdf-compressor.worker.js');
worker.postMessage({ type: 'process', data: arrayBuffer, targetSizeMB: 2 });
worker.onmessage = (e) => {
  if (e.data.type === 'progress') updateProgress(e.data.percent);
  if (e.data.type === 'done') downloadBlob(e.data.blob);
};
Enter fullscreen mode Exit fullscreen mode

Result: Tab stays responsive even with 100MB files.


2. Stream Image Processing (Don't Load All Pages Into Memory)

Second mistake: I loaded every page into memory at once before processing.

For a 45-page portfolio, that's 45 full-res images in memory simultaneously.

The fix: Process one page at a time, and stream results to the output blob incrementally.

// ✅ Good: Process one page, free memory, repeat
for (let i = 1; i <= numPages; i++) {
  const page = await pdf.getPage(i);
  const viewport = page.getViewport({ scale: 1 });
  const canvas = document.createElement('canvas');
  canvas.width = viewport.width;
  canvas.height = viewport.height;

  // Render page to canvas
  await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;

  // Compress and add to output
  const compressedImage = compressCanvas(canvas, currentQuality);
  addToOutputBlob(compressedImage);

  // Clean up immediately (critical for 100MB files)
  page.cleanup();
  canvas.width = 0;
  canvas.height = 0;

  updateProgress(i / numPages * 100);
}
Enter fullscreen mode Exit fullscreen mode

Result: Memory usage stays flat even for 100-page documents.


3. Binary Search for Target Size (Avoid Wasted Cycles)

The core feature of TinyPDF is "compress to an exact target size in MB."

My first approach: Start at 100% quality, keep dropping by 10% until we hit the target.

That could mean 5-10 full passes over the document.

The fix: Binary search on quality.

// ✅ Good: Binary search finds optimal quality in ~5 steps
let minQuality = 0.1;
let maxQuality = 1.0;
let bestQuality = 1.0;
let bestSize = Infinity;

for (let attempt = 0; attempt < 5; attempt++) {
  const midQuality = (minQuality + maxQuality) / 2;
  const size = await estimateSizeAtQuality(midQuality);

  if (size <= targetSizeBytes) {
    // We're under target—see if we can go higher quality
    bestQuality = midQuality;
    bestSize = size;
    minQuality = midQuality;
  } else {
    // Over target—need to go lower
    maxQuality = midQuality;
  }
}
Enter fullscreen mode Exit fullscreen mode

Result: Gets to optimal quality in 5 attempts, not 10+.


4. Early Termination for Impossible Targets

If a user asks to compress a 50MB document of pure text to 1MB, that's impossible—there's no image data to drop.

My first version would try anyway, wasting 30 seconds before failing.

The fix: Calculate a theoretical minimum size first.

// ✅ Good: Estimate minimum possible size before trying
const textVectorSize = await estimateTextVectorSize(pdf);
if (targetSizeBytes < textVectorSize * 0.9) {
  showWarning(`That target size might be too small—this document has ${(textVectorSize/1024/1024).toFixed(1)}MB of non-image content.`);
}
Enter fullscreen mode Exit fullscreen mode

Result: User gets immediate feedback, no wasted processing.


The Result

  • 100MB file: 12s → 2s
  • Tab stays responsive the whole time
  • Memory doesn't balloon
  • No backend, no uploads, no account needed

Try it: https://tinypdf.cn/?utm_source=devto&utm_medium=blog&utm_campaign=performance_optimization&utm_content=devto_performance_2026-07-28


What I Learned

Performance optimization isn't about "making it fast." It's about:

  1. Not blocking the main thread (users notice freezes more than slowness)
  2. Cleaning up immediately (memory is finite, especially on mobile)
  3. Avoiding unnecessary work (binary search > linear search)
  4. Setting expectations early (tell users when a target might be impossible)

What's the biggest performance win you've had in a browser-only app?

Top comments (0)