Improving Front‑End Decompression Performance with WebAssembly and Web Workers
This article describes how to replace the JavaScript‑based JSZip decompression in a Three.js 3D viewer with a C‑based Zip library compiled to WebAssembly via Emscripten, run inside a Web Worker, and demonstrates measurable performance gains through detailed testing.
The project originally used JSZip to decompress 3D model assets delivered as zip files, achieving decompression times of a few hundred milliseconds down to tens of milliseconds after optimization, but sought further speed by leveraging WebAssembly for the CPU‑intensive unzip step.
Emscripten was used to compile the lightweight C Zip library (extracted from MiniZ) into a Wasm module; the library consists of three source files (miniz.h, zip.h, zip.c) and provides the necessary unzip API.
#include #include #include #include "zip/src/zip.h" EMSCRIPTEN_KEEPALIVE int load_zip_data(void (*callback)(void *buf, int, const char*, int, int)) { struct zip_t *zip = zip_open("archive.zip", 0, 'r'); int i, n = zip_total_entries(zip); void *buf = NULL; size_t bufSize; for (i = 0; i < n; i++) { zip_entry_openbyindex(zip, i); { const char *name = zip_entry_name(zip); zip_entry_open(zip, name); { zip_entry_read(zip, &buf, &bufSize); } callback(buf, bufSize, name, i, n); } zip_entry_close(zip); free(buf); } zip_close(zip); return n; }
The EMSCRIPTEN_KEEPALIVE macro prevents dead‑code elimination, ensuring the load_zip_data function is exported; it is equivalent to specifying -s EXPORTED_FUNCTIONS="['_load_zip_data']" on the command line.
A typedef for the callback signature can be declared as:
typedef void (*callback)(void *buf, int size, const char* name, int i, int n);
The Wasm module is built with the following emcc command:
emcc c/unzip.c c/zip/src/zip.c \ -o unzip/unzip.js \ -O3 \ -s WASM=1 \ -s FORCE_FILESYSTEM=1 \ -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'addFunction', 'UTF8ToString', 'FS']" \ -s RESERVED_FUNCTION_POINTERS=1 \ -s MODULARIZE=1 \ -s ENVIRONMENT='worker' \ -s ASSERTIONS=1 \ -s EXPORT_ES6=1
The resulting unzip.wasm (≈65 KB) and unzip.js are loaded inside a Web Worker to keep the main UI thread responsive.
import getModule from '../unzip/unzip'; let wasmResolve; let wasmReady = new Promise(resolve => { wasmResolve = resolve; }); const Module = getModule({ onRuntimeInitialized() { onWasmLoaded(); }, instantiateWasm(importObject, successCallback) { self.fetch('unzip.wasm', { mode: 'cors' }) .then(r => r.ok ? r.arrayBuffer() : Promise.reject(r.status)) .then(buf => WebAssembly.instantiate(new Uint8Array(buf), importObject)) .then(o => { wasmResolve(o.instance); successCallback(o.instance); }) .catch(e => console.warn(`[js] wasm instantiation failed! ${e}`)); return {}; }, print(text) { console.log(text); }, printErr(err) { console.error(err); } });
When the Wasm instance is ready, onWasmLoaded registers the exported C function and a JavaScript callback pointer:
function onWasmLoaded() { self._loadZipEntryData = Module.cwrap('load_zip_data', 'number', ['number']); self._addZipEntryDataPtr = Module.addFunction(addZipEntryData.bind(this)); postMessage({ type: 'inited' }); }
Files are written to the virtual FS and the unzip function is invoked:
function loadZipEntryData(zipBuffer) { Module.FS.writeFile('archive.zip', new Uint8Array(zipBuffer)); self._loadZipEntryData(self._addZipEntryDataPtr); }
The JavaScript callback processes each extracted file, creates a Blob URL, and once all entries are processed sends the collection back to the main thread:
let obj = {}; function addZipEntryData(buff, size, namePtr, i, n) { const outArray = Module.HEAPU8.subarray(buff, buff + size); const fileName = Module.UTF8ToString(namePtr); if (fileName.indexOf('__') === -1) { const blob = new Blob([outArray]); obj[fileName] = URL.createObjectURL(blob); } if (i === (n - 1)) { postMessage({ url: zipUrl, files: Object.assign({}, obj) }); obj = {}; } }
Performance tests loading ten zip archives (0.3 MB–2 MB) three times each showed that the Wasm version starts faster because it does not rely on JIT warm‑up, while JSZip catches up after the JavaScript engine optimizes the hot code paths, narrowing the gap.
References: WebAssembly (webassembly.org), Emscripten (emscripten.org), Zip library (github.com/kuba--/zip).
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.