PDFs have a tendency to balloon in size. A resume with a few high-res images easily hits 20MB. A scanned contract? 50MB+. When you need to email a file or upload it to a form, size limits become a real problem.
Most online PDF tools solve this by uploading your file to their server, compressing it, and sending it back. It's convenient, but it means your sensitive documents — contracts, resumes, financial records — leave your device and land on someone else's cloud.
In this post, I'll show you how to compress PDFs without ever uploading them to a server, including a browser-based approach I built for exactly this problem.
Method 1: Built-in OS Tools (Quick but Limited)
Windows
Right-click PDF → Print → Select "Microsoft Print to PDF" → Save.
This creates a compressed version, but you have zero control over the compression level. Sometimes it works; sometimes it over-compresses and blurs text.
Mac
Open in Preview → File → Export → Quartz Filter → "Reduce File Size".
Same limitation — no quality control.
Best for: Quick one-off compression when quality isn't critical.
Method 2: Browser-Based Processing (Privacy-First)
This is the approach I use for sensitive documents. Instead of uploading to a server, the PDF opens directly in your browser and gets processed locally with JavaScript.
How It Works Under the Hood
At the core is pdf-lib, a powerful PDF manipulation library that runs entirely in the browser:
import { PDFDocument } from 'pdf-lib';
async function compressPDF(file) {
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFDocument.load(arrayBuffer);
// pdf-lib doesn't have a direct "compress" flag,
// but you can optimize by re-saving with reduced quality
const pdfBytes = await pdfDoc.save({ useObjectStreams: true });
return new Blob([pdfBytes], { type: 'application/pdf' });
}
For image-heavy PDFs, the real compression happens at the image level. Here's how I handle it in sotool.top:
// Extract images, compress them, re-embed
const pages = pdfDoc.getPages();
for (const page of pages) {
const images = await page.embeddedResources();
for (const image of images) {
if (image instanceof PDFImage) {
// Reduce image quality based on user-selected compression level
const compressed = await compressImage(image, quality);
// Re-embed compressed image
}
}
}
Key advantage: Your file never leaves your computer. The server never sees the content.
Trade-off: Browser memory limits. A 500MB scanned document might crash the tab, whereas a server-based tool could handle it.
For typical documents under 100MB, the speed difference is negligible.
A Real-World Test
I tested three approaches on a 15MB scanned contract:
| Method | Output Size | Quality | Upload Required? |
|---|---|---|---|
| Windows Print to PDF | 4.2MB | Slightly blurred | ❌ |
| Browser-based (sotool.top) | 3.1MB | Text crisp | ❌ |
| Adobe Acrobat Pro | 2.8MB | Best | ❌ |
| PDF24 (server-based) | 3.0MB | Good | ✅ |
The browser-based approach matched the server-based tool in quality, without the privacy trade-off.
When to Use What
| File Type | Recommended Tool | Why |
|---|---|---|
| Sensitive docs (contracts, resumes, financial) | Browser-based | Files never leave your device |
| Large files (300MB+ scans) | Desktop software | Browser memory limits |
| Non-sensitive batches | Server-based free tools | Faster for bulk operations |
| Quick fixes | Built-in OS tools | Fastest, no setup |
Performance Considerations
Compressing PDFs in the browser has unique constraints:
Memory: Chrome caps each tab at ~1-2GB. For very large files, stream pages one at a time instead of loading the entire document:
// Instead of loading the full file
const pdfDoc = await PDFDocument.load(arrayBuffer); // Loads everything
// Process pages incrementally
const pdfDoc = await PDFDocument.load(arrayBuffer, {
updateMetadata: false
});
// Only access pages you need
CPU: Image compression is CPU-intensive. For batch processing, use requestIdleCallback to avoid blocking the UI:
function processBatch(files) {
files.forEach((file, index) => {
requestIdleCallback(() => {
compressPDF(file).then(updateProgressBar);
});
});
}
Web Workers: For heavy lifting, offload to a Web Worker so the main thread stays responsive:
// worker.js
import { PDFDocument } from 'pdf-lib';
self.onmessage = async (e) => {
const { file } = e.data;
const compressed = await compressPDF(file);
self.postMessage({ compressed });
};
The Bottom Line
For sensitive documents, browser-based PDF compression is the sweet spot between convenience and privacy. Modern JavaScript libraries like pdf-lib make it entirely feasible, and the performance gap with server-based tools is shrinking fast.
If you want to try a browser-based tool without signing up:
Free, no signup, files never leave your browser.
What's your approach to handling large PDFs? Do you prioritize convenience or privacy? Let me know in the comments.
Top comments (0)