I built my first client-side image compressor using pure JavaScript canvas operations. It worked for small images. On a 10MB JPEG, it froze the browser tab for 3 seconds. Moving the compression to a Web Worker with a WebAssembly-compiled codec brought that to 300ms with zero UI freeze. Understanding why requires looking at what the browser is actually doing when you drop a file.
Tools built with this architecture.
All FileMint tools use Web Workers and WASM where appropriate. No upload. No server.
View All Local Tools →The File API: Reading Files Without a Network Request
// Reading a file from disk into browser memory
// This uses the File API — no network request involved
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
if (!file) return;
// Method 1: ArrayBuffer (for binary processing — PDFs, images)
const buffer = await file.arrayBuffer();
// → Raw binary data in memory, ready for WASM libraries
// Method 2: Text (for text files — JSON, CSV, YAML)
const text = await file.text();
// → UTF-8 string
// Method 3: DataURL (for displaying in <img> or <canvas>)
const url = await new Promise<string>(resolve => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
// File metadata — read without loading file content
console.log(file.name); // "document.pdf"
console.log(file.size); // 1048576 (bytes)
console.log(file.type); // "application/pdf"
console.log(file.lastModified); // Unix timestamp
});WebAssembly: Running C++ Code in the Browser
WebAssembly is a binary format that browsers can compile and execute at near-native speed. It was designed specifically for performance-critical code — the kind of computational work that file processing requires.
// Loading and using a WebAssembly module
// Example: a hypothetical image processing WASM module
async function loadWasmModule(): Promise<WebAssembly.Instance> {
// Fetch the compiled WASM binary
const response = await fetch('/codecs/image-processor.wasm');
const wasmBuffer = await response.arrayBuffer();
// Compile and instantiate — V8 compiles to machine code
const { instance } = await WebAssembly.instantiate(wasmBuffer, {
env: {
// WASM imports — memory allocators, etc.
malloc: (size: number) => 0, // Simplified
}
});
return instance;
}
// Real-world: using squoosh's WASM codecs for image compression
// The squoosh library wraps MozJPEG, libavif, libwebp in WASM
import { ImagePool } from '@squoosh/lib';
async function compressImage(file: File): Promise<Blob> {
const pool = new ImagePool();
const image = pool.ingestImage(await file.arrayBuffer());
await image.encode({
webp: { quality: 85 }, // MozWebP at quality 85
});
const { webp } = await image.encodedWith;
await pool.close();
return new Blob([webp.binary], { type: 'image/webp' });
}Web Workers: Background Threads Without Blocking the UI
// main.ts — main thread
const worker = new Worker(new URL('./compression.worker.ts', import.meta.url));
// Transfer the ArrayBuffer to the worker (zero-copy — no duplication)
const buffer = await file.arrayBuffer();
worker.postMessage({ buffer, quality: 85 }, [buffer]);
// ↑ Passing buffer in the "transfer" array transfers ownership
// The buffer is no longer accessible in the main thread after this
worker.addEventListener('message', (event) => {
const { compressed, duration } = event.data;
console.log(`Compressed in ${duration}ms`);
downloadFile(new Blob([compressed], { type: 'image/webp' }));
});
// compression.worker.ts — background thread
// This runs on a separate OS thread — UI stays responsive
self.addEventListener('message', async (event) => {
const { buffer, quality } = event.data;
const start = performance.now();
// Heavy WASM compression — safe here, not on the main thread
const compressed = await compressWithWasm(buffer, quality);
self.postMessage({
compressed,
duration: performance.now() - start
}, [compressed]); // Transfer result back (zero-copy)
});OffscreenCanvas: GPU Rendering Off the Main Thread
// OffscreenCanvas allows GPU canvas operations in a Web Worker
// (Normally canvas is restricted to the main thread)
// In a Web Worker:
const canvas = new OffscreenCanvas(1920, 1080);
const ctx = canvas.getContext('2d');
// Draw image data to canvas (off main thread)
ctx.drawImage(bitmap, 0, 0);
// Export as WebP — GPU-accelerated, off main thread
const blob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.85 });
// Transfer blob back to main thread for download
self.postMessage({ blob }, []);Triggering Downloads Without a Server
// Writing processed data back to the user's filesystem
// No server involved — the browser handles the download
function downloadBlob(blob: Blob, filename: string): void {
// Create a temporary object URL pointing to the blob in memory
const url = URL.createObjectURL(blob);
// Trigger a download via a programmatic click
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Release the object URL from memory (important for large files)
// The blob remains in memory until garbage collected,
// but the URL reference is freed immediately
URL.revokeObjectURL(url);
}
// For ZIP files containing multiple outputs (e.g., batch compression):
import JSZip from 'jszip';
async function downloadAsZip(files: { name: string; blob: Blob }[]): Promise<void> {
const zip = new JSZip();
files.forEach(({ name, blob }) => zip.file(name, blob));
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadBlob(zipBlob, 'compressed-images.zip');
}For the practical user perspective on why this architecture matters for privacy, our client-side processing privacy guide explains what “no upload” means in terms you can verify with DevTools. For the broader comparison of when to use server-side processing instead, our client-side vs server-side guide covers the genuine tradeoffs.
Tools built on this architecture.
FileMint tools use Web Workers, WebAssembly, and OffscreenCanvas. No server required.
View All Local Tools →