DEV Community

Cover image for How I Reduced PDF File Size by 80% in the Browser — No Server Needed
kabir daki
kabir daki

Posted on

How I Reduced PDF File Size by 80% in the Browser — No Server Needed

When I started building PDFOnlineLovePDF, I faced one big challenge: how do you compress a PDF file entirely in the browser, without sending it to a server?

Most PDF tools upload your file to a remote server, process it, then send it back. That's slow, raises privacy concerns, and costs money to run at scale.

I wanted something different: 100% client-side PDF compression.

Here's exactly how I did it.


The Problem With Server-Side PDF Compression

Most online PDF tools work like this:

  1. User uploads file → goes to your server
  2. Server runs ghostscript or similar tool
  3. Compressed file is sent back to the user

This works, but has real problems:

  • Privacy: Your files touch someone else's server
  • Speed: Upload + processing + download = slow
  • Cost: Server bandwidth and CPU are expensive
  • Scalability: 1000 users uploading 10MB files = infrastructure nightmare

I wanted to eliminate all of these problems.


The Solution: PDF.js + PDF-lib in the Browser

The key insight is that modern browsers are incredibly powerful. With the right libraries, you can parse, manipulate, and re-render PDF files entirely in JavaScript — no server required.

Here are the two libraries that made this possible:

1. PDF.js (by Mozilla)

PDF.js lets you read and render PDF files in the browser. It parses the PDF structure and gives you access to every page, image, font, and element.

import * as pdfjsLib from 'pdfjs-dist';

const loadPDF = async (file) => {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
  return pdf;
};
Enter fullscreen mode Exit fullscreen mode

2. PDF-lib

PDF-lib lets you create and modify PDF files entirely in JavaScript. You can add pages, embed images, compress content, and save the result as a new PDF.

import { PDFDocument } from 'pdf-lib';

const compressPDF = async (arrayBuffer) => {
  const pdfDoc = await PDFDocument.load(arrayBuffer);
  const compressedBytes = await pdfDoc.save({
    useObjectStreams: true, // This is key for compression
  });
  return compressedBytes;
};
Enter fullscreen mode Exit fullscreen mode

The Compression Strategy

Simply re-saving a PDF with useObjectStreams: true gives you some compression, but not enough. The real gains come from re-rendering each page as a compressed image.

Here's the full strategy I used:

Step 1: Render Each Page to Canvas

const renderPageToCanvas = async (page, scale = 1.5) => {
  const viewport = page.getViewport({ scale });
  const canvas = document.createElement('canvas');
  canvas.width = viewport.width;
  canvas.height = viewport.height;

  const context = canvas.getContext('2d');
  await page.render({
    canvasContext: context,
    viewport,
  }).promise;

  return canvas;
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Convert Canvas to Compressed JPEG

const canvasToJpeg = (canvas, quality = 0.7) => {
  return new Promise((resolve) => {
    canvas.toBlob(
      (blob) => resolve(blob),
      'image/jpeg',
      quality // 0.7 = 70% quality, good balance
    );
  });
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Rebuild the PDF With Compressed Images

const rebuildPDF = async (originalArrayBuffer, quality = 0.7) => {
  const pdfjsLib = window['pdfjs-dist/build/pdf'];
  const loadingTask = pdfjsLib.getDocument({ data: originalArrayBuffer });
  const pdfDoc_original = await loadingTask.promise;

  const newPdfDoc = await PDFDocument.create();

  for (let i = 1; i <= pdfDoc_original.numPages; i++) {
    const page = await pdfDoc_original.getPage(i);
    const canvas = await renderPageToCanvas(page, 1.5);
    const jpegBlob = await canvasToJpeg(canvas, quality);
    const jpegArrayBuffer = await jpegBlob.arrayBuffer();

    const jpegImage = await newPdfDoc.embedJpg(jpegArrayBuffer);
    const { width, height } = jpegImage.scale(1);

    const newPage = newPdfDoc.addPage([width, height]);
    newPage.drawImage(jpegImage, {
      x: 0,
      y: 0,
      width,
      height,
    });
  }

  return await newPdfDoc.save();
};
Enter fullscreen mode Exit fullscreen mode

The Results

Using this approach, here's what I observed across different PDF types:

PDF Type Original Size Compressed Size Reduction
Scanned document (10 pages) 8.2 MB 1.4 MB 83%
Report with images (5 pages) 3.1 MB 0.7 MB 77%
Text-heavy document (20 pages) 1.8 MB 0.4 MB 78%
Presentation slides (15 pages) 12.4 MB 2.1 MB 83%

Average reduction: ~80% — entirely in the browser.


Quality vs Compression Tradeoff

The JPEG quality setting is the main lever you can pull:

// Higher quality = larger file, better text readability
const HIGH_QUALITY = 0.85;    // ~60% reduction
const MEDIUM_QUALITY = 0.7;   // ~75% reduction  
const LOW_QUALITY = 0.5;      // ~85% reduction
Enter fullscreen mode Exit fullscreen mode

For my tool, I offer three preset levels so users can choose based on their needs:

  • High Quality — best for documents you'll print
  • Medium Quality — best for email attachments
  • Low Quality — best for web upload or WhatsApp sharing

Handling Large Files Without Freezing the UI

Processing large PDFs page by page can freeze the browser if you're not careful. The solution is to use Web Workers and update a progress bar as each page is processed.

// In your main thread
const worker = new Worker('/pdf-compress-worker.js');

worker.onmessage = (e) => {
  const { progress, result } = e.data;

  if (progress) {
    updateProgressBar(progress); // e.g. "Processing page 3 of 10"
  }

  if (result) {
    downloadFile(result);
  }
};

worker.postMessage({ file: arrayBuffer, quality: 0.7 });
Enter fullscreen mode Exit fullscreen mode
// In pdf-compress-worker.js
self.onmessage = async (e) => {
  const { file, quality } = e.data;
  const totalPages = /* get from pdf */;

  for (let i = 0; i < totalPages; i++) {
    // process page i
    self.postMessage({ progress: `${i + 1} / ${totalPages}` });
  }

  self.postMessage({ result: compressedBytes });
};
Enter fullscreen mode Exit fullscreen mode

Privacy as a Feature

One unexpected benefit of this approach: you can genuinely tell users their files never leave their device.

This turned out to be a real differentiator. Many users explicitly mentioned in feedback that they chose PDFOnlineLovePDF because they didn't want to upload sensitive documents (contracts, medical records, financial statements) to an unknown server.

The privacy benefit is not just marketing — it's a direct result of the technical architecture.


Limitations

This approach isn't perfect. Here are the tradeoffs:

1. Text becomes non-selectable
When you re-render pages as images, the text layer is lost. The output is a purely image-based PDF — you can't select or copy text from it.

2. File size depends on content complexity
Text-heavy PDFs compress more than image-heavy ones. A PDF that's already full of JPEG photos may not compress much.

3. Processing is CPU-intensive
On older or low-end devices, processing a 50-page PDF can take 10-20 seconds. The progress bar is essential for UX.

4. Very large files (100+ pages) are slow
For very large documents, a hybrid approach (client-side for preview, server-side for heavy processing) might be better.


What I Learned

Building this feature taught me several things:

  • The browser is more powerful than most developers assume. PDF manipulation, image compression, file download — all doable without a backend.
  • Privacy-first architecture is a real competitive advantage, especially for document tools.
  • Web Workers are underused. They make CPU-intensive tasks smooth and non-blocking.
  • UX matters more than perfect compression. Users care more about a fast, responsive experience than squeezing out the last few kilobytes.

Try It Yourself

The PDF compression tool is live at PDFOnlineLovePDF.com. It's completely free, no signup required.

If you're building something similar or have questions about the implementation, drop a comment below — happy to help!


If this was useful, consider following me here on DEV.to for more posts about building browser-based tools.

Top comments (0)