Most online file converters require uploading your documents, images, or videos to an unknown server. This is slow, inconvenient, and raises serious privacy concerns. I decided to build something different: a converter that works entirely inside your browser.
Processing large files without a backend presents two main engineering challenges:
- How do you avoid consuming all available RAM?
- How do you prevent the user interface from freezing?
This article explains how I solved these problems using OPFS, Web Workers, and a Backpressure mechanism. The result is a working tool you can try right now: PixelForge Free.
The Architecture at a Glance
Here is the simplified data flow of the entire pipeline:
Drag & Drop → OPFS (Virtual Disk) → Worker Pool (Backpressure) → ZIP Stream → Download
Problem 1: Out-of-Memory (OOM) Crashes
The Challenge: Loading many files directly into the browser's memory is impossible. A user dropping a folder with 100+ high-resolution images would instantly crash the tab.
The Solution: Origin Private File System (OPFS)
OPFS provides a fast, isolated virtual disk inside the browser. Instead of loading files into RAM, my pipeline intercepts the drop event and streams the raw binary data directly to this virtual disk.
Here is a simplified version of how it works:
// Get a reference to the virtual disk
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle(`input_${id}.raw`, { create: true });
// Create a writable stream to the disk
const writable = await fileHandle.createWritable();
// Stream the file directly from the user's computer to the virtual disk
await file.stream().pipeTo(writable);
This allows the application to accept a folder with 500+ items without consuming more than a few megabytes of actual RAM. The data stays on the user's SSD, not in memory.
Problem 2: UI Freezing
The Challenge: Image compression, PDF parsing, and video encoding are CPU-intensive operations. Running them on the browser's main thread makes the entire interface unresponsive.
The Solution: A Custom Web Worker Pool
All conversion and scaling tasks are delegated to a pool of background Web Workers. The number of active workers scales dynamically based on the user's CPU core count (navigator.hardwareConcurrency).
const worker = new Worker(new URL("./converter.worker.ts", import.meta.url), {
type: "module",
});
worker.postMessage({ fileHandle, targetFormat });
worker.onmessage = (e) => {
// Handle the converted file
};
Each worker loads the necessary tools (e.g., FFmpeg.wasm, PDF.js) and executes compression in total isolation from the UI. The interface stays buttery-smooth at 60fps.
Why Not Just Use WebAssembly?
A common question: "Why not just compile FFmpeg to WASM and call it a day?"
I do use WebAssembly for specific tasks, such as:
- PNG to SVG conversion via Potrace.wasm
- HEIC to JPEG via libheif-wasm
However, the core pipeline relies on native browser APIs (OPFS for streaming, Web Workers for threading) for a few reasons:
- Memory Streaming: Managing backpressure and streaming large files into a virtual disk is much easier with native Streams API.
- Multi-threading: A single WASM module runs on one thread. My WorkerPool can parallelize work across all CPU cores.
- Native Codecs: For formats like WebP, the browser's native OffscreenCanvas is significantly faster and more memory-efficient than a WASM alternative.
It is not an "either/or" decision. It is a hybrid approach: JavaScript handles orchestration and streaming, while WebAssembly is injected strictly where JS is too slow.
Problem 3: Explosive RAM Growth
The Challenge: If you feed 100 workers at once, the browser will still crash due to rapid memory allocation. Each worker needs a chunk of memory to process its file.
The Solution: Backpressure Mechanism
My WorkerPool tracks the total number of bytes currently being processed. If the memory footprint of "in-flight" files approaches a limit (200 MB), the pipeline pauses reading from OPFS.
As soon as a worker finishes compressing an image and releases its buffer, the pipeline pushes the next asset forward. This guarantees that total memory usage stays stable, regardless of the total number or size of the queued files.
class WorkerPool {
private currentBytesProcessing = 0;
private readonly MAX_BYTES_IN_FLIGHT = 200 * 1024 * 1024; // 200 MB
async addTask(task, signal) {
// Wait if we are over the memory limit
while (this.currentBytesProcessing + task.size > this.MAX_BYTES_IN_FLIGHT) {
await this.waitForCompletion();
}
this.currentBytesProcessing += task.size;
this.executeTask(task);
}
}
Putting It All Together: The Final Pipeline
- Input: User drops files (or a folder) onto the dropzone.
- OPFS Streaming: Files are written directly to the browser's virtual disk. RAM remains almost empty.
- Worker Pool: The WorkerPool manager takes files one by one, respecting the 200 MB backpressure limit.
- Parallel Processing: Available Web Workers pick up files, convert them (using WASM, Canvas, or PDF.js), and return the result as a Blob.
- ZIP Archiving: The fflate library streams all resulting blobs into a ZIP archive.
- Download: The user gets the archive. All temporary files are deleted from the virtual disk.
Performance Benchmarks
Here are real-world results on a 2021 MacBook (8-core M1 Pro):
Scenario Result
100 images (10MB each) → WebP ~45 seconds
500 mobile assets (2MB each) → WebP ~2 minutes
Peak RAM usage < 200 MB (capped)
You can verify this yourself by opening the browser's task manager.
What Tools Are Currently Available?
The converter includes 15+ tools, including:
- Image Compressor (PNG, JPEG, WebP)
- HEIC to JPG (for iPhone photos)
- PNG to SVG vectorization (via Potrace.wasm)
- PDF to Images / PDF to Word / PDF to EPUB
- JPG/HEIC to PDF compilation
- DOCX to PDF and EPUB to PDF
All of these run entirely offline after the first page load.
Try It Yourself
The project is live and 100% free:
🔗 Try it live: LocalForge (PixelForgeFree)
💻 Explore the logic or star the project on GitHub: Mykhailo Sapianyi / PixelForgeFree
No signup, no uploads, no limits. Your files never leave your device.
Discussion
I would love to hear your thoughts:
- Have you built any client-side processing tools?
- What is your experience with OPFS or Web Workers?
- Do you see the "privacy-first, client-side" approach as a viable trend for more complex web applications?
Let's discuss in the comments!
Top comments (0)