Modern web applications are changing.
A few years ago, building a document editor, image compressor, or PDF toolkit almost always meant uploading files to a backend server, processing them remotely, and sending the results back to the user.
Today, modern browsers are powerful enough to perform many of those same tasks locally.
When building Fillora PDF, we re-evaluated our architecture and discovered that many common PDF operations didn't actually require a server. By moving supported workflows directly into the browser, we reduced unnecessary network requests while improving performance and privacy.
In this article, I'll explain what local-first means, the browser technologies that make it possible, and the engineering trade-offs we encountered along the way.
🏗️ What Does "Local-First" Mean?
A local-first application performs as much work as possible on the user's device before relying on cloud infrastructure.
Instead of treating the browser as a thin client, it becomes the primary execution environment.
For document applications, that means:
- 📄 Opening files locally
- ✏️ Editing documents in memory
- 📥 Generating downloads without uploads
- ☁️ Using the server only when it provides real value
Local-first doesn't mean eliminating servers completely.
Instead, it means choosing the right place to execute each workload.
🌐 Traditional Server-Centric Architecture
Many online document tools still follow this workflow:
Browser
│
├── Upload File
▼
Cloud Server
│
├── Process Document
▼
Browser
└── Download Result
Although straightforward, this architecture introduces several challenges:
- Upload latency for large files
- Higher bandwidth usage
- Increased server CPU and memory consumption
- Processing queues during peak traffic
- Privacy concerns when handling sensitive documents
For many PDF editing operations, these round trips simply aren't necessary.
⚡ Browser-Native Architecture
Modern browsers already provide the APIs needed to process many documents locally.
Browser
│
├── Read Local File
├── Process in Memory
├── Generate Output
└── Download Result
For supported workflows:
- ✅ Documents remain on the user's device
- ✅ No upload is required
- ✅ Server CPU isn't consumed
- ✅ Users receive results without waiting for upload and download cycles
🧩 Browser APIs That Make It Possible
The browser has evolved into a capable application runtime.
Several Web APIs make local-first document processing practical.
📂 FileReader API
The FileReader API allows applications to read files directly from a user's device.
const reader = new FileReader();
reader.onload = (event) => {
console.log(event.target.result);
};
reader.readAsArrayBuffer(file);
Perfect for:
- Image previews
- PDF editors
- CSV importers
- Markdown editors
📦 ArrayBuffer & Uint8Array
PDFs, images, and many other file formats are fundamentally binary data.
ArrayBuffer and Uint8Array allow JavaScript to manipulate those bytes efficiently without converting everything into strings.
These typed arrays are the foundation of many browser-native document tools.
💾 Blob & URL.createObjectURL()
After generating a new file, browsers can immediately create a downloadable object.
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "merged.pdf";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
Common use cases:
- PDF generation
- CSV exports
- Image editors
- ZIP downloads
Instead of uploading generated files back to a server, everything happens locally.
⚙️ Web Workers
Heavy JavaScript running on the main thread can freeze the user interface.
Web Workers move expensive computations into a background thread.
const worker = new Worker("worker.js");
worker.postMessage(data);
worker.onmessage = ({ data }) => {
console.log(data);
};
They're particularly useful for:
- Large PDF processing
- Image manipulation
- OCR
- Data parsing
- Compression
Keeping long-running tasks off the main thread makes applications feel significantly more responsive.
📄 JavaScript PDF Libraries
Libraries like pdf-lib make browser-native PDF editing remarkably straightforward.
A simplified merge example looks like this:
import { PDFDocument } from "pdf-lib";
async function mergePdfFiles(buffers) {
const mergedPdf = await PDFDocument.create();
for (const buffer of buffers) {
const pdf = await PDFDocument.load(buffer);
const pages = await mergedPdf.copyPages(
pdf,
pdf.getPageIndices()
);
pages.forEach((page) => mergedPdf.addPage(page));
}
return await mergedPdf.save();
}
For many everyday PDF operations, the entire workflow completes without requiring the document to leave the user's device.
🧠 Where WebAssembly Fits In
JavaScript is incredibly capable, but some workloads demand even more performance.
WebAssembly (Wasm) enables code compiled from languages like C, C++, or Rust to run securely inside the browser at near-native speed.
It's particularly valuable for computationally intensive tasks such as:
- OCR engines
- Image processing
- Compression algorithms
- Advanced rendering
- Computer vision
Rather than replacing JavaScript, WebAssembly complements it by accelerating performance-critical components while JavaScript orchestrates the overall application.
⚖️ Engineering Trade-Offs
Local-first architecture isn't a silver bullet.
Several practical limitations remain.
1. Browser Memory Limits
Browsers enforce memory limits for each tab.
Very large PDFs or thousands of pages can exceed available memory, particularly on mobile devices.
2. Complex Document Formats
High-fidelity Office document rendering often depends on sophisticated rendering engines that are still better suited to dedicated backend services.
3. Bundle Size
Shipping every processing engine to every visitor would dramatically increase initial page load times.
Code splitting and lazy loading are essential for maintaining performance.
4. Hybrid Architectures Still Matter
Not every workload belongs in the browser.
A practical architecture combines:
- Browser-native processing for supported editing tasks
- Server-side processing for workloads that genuinely require backend compute
Choosing the right execution environment is often more important than forcing everything onto one side.
📈 What Changed
Moving supported PDF workflows into the browser produced several practical improvements.
✅ Reduced infrastructure costs
✅ Lower bandwidth consumption
✅ Faster perceived performance
✅ Better privacy for supported workflows
✅ Improved scalability by reducing dependency on server compute
Most importantly, backend resources became available for workloads that genuinely benefit from server-side processing.
💡 Lessons Learned
The browser has evolved far beyond rendering HTML.
Modern Web APIs, JavaScript, Web Workers, and WebAssembly make it possible to build applications that once required dedicated backend infrastructure.
Local-first architecture isn't about eliminating the cloud.
It's about using cloud resources intentionally while allowing capable client devices to perform the work they're already equipped to handle.
🎯 Final Thoughts
The future of many web applications isn't about moving everything to the server.
It's about moving the right workloads to the client.
For us, adopting a local-first approach led to faster interactions, improved privacy for supported workflows, and a more scalable architecture while still relying on server-side processing where it provides clear benefits.
As browsers continue to evolve, the line between desktop software and web applications will only become thinner.
If you're building document tools, editors, design software, or productivity applications, it's worth asking one simple question:
Does this operation really need to leave the user's device?
I'd love to hear how you're using browser APIs, WebAssembly, or local-first architecture in your own projects.
`
Top comments (0)