Privacy & Security

WebAssembly: the quiet backbone of privacy-first tools

Every tool on Unwrite runs in your browser. WebAssembly is what makes that possible.

8 min read
Free Guide

Every tool on Unwrite processes files in your browser. PDFs get compressed locally. Images get optimised locally. Language models run locally. Nothing uploads. Nothing phones home.

That claim would have been absurd ten years ago. The browser was for documents and forms, not for running compiled C++ at near-native speed. WebAssembly changed that.

What WebAssembly actually is

WebAssembly (WASM) is a binary instruction format that runs in every modern browser. Think of it as a compact, portable compilation target. You write code in C, C++, Rust, or Go, compile it to WASM, and the browser executes it in a sandboxed virtual machine at speeds close to native.

Three properties matter here.

It is fast. WASM runs at 80-95% of native speed for most workloads. That is fast enough to compress a 50MB PDF or encode a 4000x3000 image without the user noticing they are not using a desktop application.

It is sandboxed. WASM modules cannot access the filesystem, the network, or anything outside the memory the browser allocates for them. A compiled C library running as WASM has less access to your system than a typical browser extension.

It is portable. The same WASM binary runs on Windows, macOS, Linux, ChromeOS, Android, and iOS. No installers, no architecture-specific builds, no "works on my machine" problems.

How we use it

Every Unwrite tool that needs heavy processing delegates to a WASM module. Here is the full breakdown.

ToolLibraryOriginal languageWhat it does
PDF compressGhostscript-wasmCRecompresses images inside PDFs, strips unused objects
PDF merge/split/reorderpdfcpu-wasmGoPage-level operations, metadata editing, repair
PDF sanitiseQPDF-wasmC++Linearisation, encryption, structural validation
Image optimise (JPEG)MozJPEGCProduces smaller JPEGs than the standard encoder
Image optimise (PNG)OxiPNGRustLossless PNG compression, often 30-60% smaller
Image optimise (WebP)libwebpC++Lossy and lossless WebP encoding
Image optimise (AVIF)libavifCAV1-based image encoding, best size-to-quality ratio
AI / LLM inferenceONNX RuntimeC++WASM backend for running quantised language models

One tool does not use WASM at all. The HTML cleaner runs as pure JavaScript because DOM parsing and string manipulation are fast enough without compiled code. There is no reason to add WASM overhead when the JS engine handles the job in milliseconds.

Why WASM enables privacy

The privacy argument is simple. If processing happens in compiled code running inside the browser sandbox, your files never need to leave your device. There is no upload step, no server-side processing queue, no temporary storage bucket, no retention policy to read and hope someone follows.

This is not a policy decision. It is an architectural one. The data path from "user picks a file" to "user downloads the result" never touches a network socket. You can verify this yourself: open DevTools, watch the Network tab, and process a file. After the initial page load, there is zero outbound traffic.

Compare that to a typical online PDF compressor. You upload your file over HTTPS to a server. The server runs Ghostscript (the same library we use, just not compiled to WASM). It stores the result. You download it. The server "promises" to delete your file within an hour. Maybe it does. You have no way to verify.

Same library. Same compression. Completely different privacy properties. The only difference is where the code runs.

The tradeoffs

WASM is not free. There are real costs.

Download size. Our WASM modules range from 2MB to 15MB each. Ghostscript alone is around 12MB. That is a meaningful first-load cost, especially on slow connections. We mitigate this with aggressive caching (the module downloads once and stays in the browser cache) and lazy loading (modules only download when you actually use the tool that needs them).

Cold start. Compiling a WASM module takes time. The first PDF compression after a fresh page load is slower than subsequent ones because the browser needs to compile and instantiate the module. Chrome's V8 engine caches compiled modules in its code cache, so repeat visits are faster.

Memory. WASM modules run in linear memory that must be allocated up front or grown dynamically. Processing a 200MB PDF means holding the file, the WASM module, and intermediate buffers all in browser memory. Chrome tabs typically have a 4GB memory ceiling. For most files this is fine. For very large documents, it is a real constraint.

No filesystem. WASM sees a virtual filesystem, not your real one. We use Emscripten's MEMFS (an in-memory filesystem) to feed files into libraries that expect to read from disk. This works well but adds complexity to the build pipeline.

These tradeoffs are worth it. A 12MB download that caches forever is a better deal than uploading every file you process to a server for the rest of your life.

Web Workers: keeping the UI responsive

WASM is fast, but "fast" still means "blocks the main thread if you run it there." Compressing a PDF takes real CPU time. If that runs on the main thread, the entire page freezes.

Every heavy WASM operation on Unwrite runs in a Web Worker. Workers are background threads that cannot touch the DOM but can execute WASM modules without blocking the UI. The pattern is straightforward:

  1. 1Main thread sends a file (as an ArrayBuffer) to the Worker via postMessage
  2. 2Worker loads the WASM module, processes the file, and sends the result back
  3. 3Main thread receives the result and triggers the download

The user sees a progress indicator and a responsive page. Behind the scenes, a compiled C library is chewing through their PDF in a separate thread.

The build pipeline

Getting WASM modules into a Next.js static site is not as simple as npm install. Each library has its own compilation toolchain.

Ghostscript compiles with Emscripten from C source. pdfcpu compiles with TinyGo from Go source. OxiPNG compiles with wasm-pack from Rust source. Each produces a .wasm binary and a JavaScript glue file.

We build Workers separately with esbuild, copy WASM binaries to the public directory at build time, and bundle the glue code into the Worker scripts. The build order matters: Workers first, then WASM copy, then the Next.js build.

It is fiddly. It is worth it.

What is coming next

WASM is still young. Several proposals in the pipeline will make it significantly more capable.

WASM GC (Garbage Collection). Currently shipping in Chrome and Firefox. This lets languages with managed memory (like Kotlin, Dart, and eventually Java) compile to WASM without shipping their own garbage collector. Smaller binaries, faster startup.

The Component Model. A standard for WASM modules to expose typed interfaces and compose with each other. Today, every WASM module is an island. The Component Model would let a Rust image encoder and a Go PDF library share data through a well-defined contract instead of raw memory pointers.

WASI (WebAssembly System Interface). A standardised set of system APIs for WASM outside the browser. This is more relevant for server-side WASM, but it signals a future where the same WASM binary runs in browsers, edge functions, and containers with identical behaviour.

Better SIMD. Relaxed SIMD instructions are already landing in browsers. These unlock faster image processing, audio processing, and neural network inference by letting WASM use the CPU's vector units more effectively.

The point

WebAssembly is not a feature we advertise on a landing page. Most users never think about it. They click "compress," wait a moment, and download a smaller PDF. The fact that a compiled C library just ran in a sandboxed virtual machine inside their browser tab is invisible.

That is the point. The best infrastructure is invisible. WASM lets us make a simple promise: your files stay on your device. And it lets us keep that promise without asking users to install anything, trust any server, or sacrifice performance.

If you want to see it in action: PDF tools, image optimiser, or the in-browser LLM. Open DevTools, watch the Network tab, and process a file. Nothing leaves.