Boosting 3D Asset Decompression with WebAssembly: A Step‑by‑Step Guide
This article explains how to replace JSZip with a C‑based WebAssembly unzip module using Emscripten, integrate it via a Web Worker, and achieve faster decompression for Three.js 3D assets, complete with performance test results and detailed code examples.
Background
3D web applications often receive assets as zip archives. Decompressing these archives in the browser with JavaScript (e.g., JSZip) reduces latency to ~100 ms but remains CPU‑bound. Compiling a native C unzip library to WebAssembly (Wasm) can further lower the unzip time.
Zip library and C wrapper
The open‑source zip library (derived from MiniZ) provides zip.h, zip.c, and miniz.h. A small C wrapper calls the library and forwards each entry to JavaScript via a callback.
#include <stdio.h>
#include <stdlib.h>
#include <emscripten.h>
#include "zip/src/zip.h"
EMSCRIPTEN_KEEPALIVE
int load_zip_data(void (*callback)(void *buf, int size,
const char *name, int i, int n)) {
struct zip_t *zip = zip_open("archive.zip", 0, 'r');
int n = zip_total_entries(zip);
void *buf = NULL;
size_t bufSize;
for (int 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, (int)bufSize, name, i, n);
zip_entry_close(zip);
free(buf);
buf = NULL;
}
zip_close(zip);
return n;
}The EMSCRIPTEN_KEEPALIVE macro (or -s EXPORTED_FUNCTIONS="['_load_zip_data']") prevents dead‑code elimination. A typedef can simplify the callback signature:
typedef void (*callback)(void *buf, int size,
const char *name, int i, int n);Compiling to WebAssembly
Using emcc the C sources are compiled into a Wasm module and a JavaScript glue file. The command below produces unzip.wasm (~65 KB) and unzip.js with ES6 module output, a virtual filesystem, and support for function pointers.
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=1Web Worker integration
Loading and instantiating the Wasm module inside a Web Worker keeps the UI thread free. The worker fetches unzip.wasm, creates an instance, and registers two JavaScript wrappers: _loadZipEntryData – cwrapped load_zip_data (returns the number of entries). _addZipEntryDataPtr – a function pointer created with addFunction that points to the JavaScript callback addZipEntryData.
import getModule from '../unzip/unzip';
let wasmResolve;
const 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(out => { wasmResolve(out.instance); successCallback(out.instance); })
.catch(e => console.warn(`[js] wasm instantiation failed! ${e}`));
return {};
},
print: console.log,
printErr: console.error
});
function onWasmLoaded() {
self._loadZipEntryData = Module.cwrap('load_zip_data', 'number', ['number']);
self._addZipEntryDataPtr = Module.addFunction(addZipEntryData);
postMessage({type: 'inited'});
}When a zip file is received, it is written to the virtual filesystem and the C unzip function is invoked:
function loadZipEntryData(zipBuffer) {
Module.FS.writeFile('archive.zip', new Uint8Array(zipBuffer));
self._loadZipEntryData(self._addZipEntryDataPtr);
}The callback extracts the entry data from the Wasm heap, creates a Blob URL, and sends the collection back to the main thread after the last entry.
let files = {};
function addZipEntryData(ptr, size, namePtr, i, n) {
const data = Module.HEAPU8.subarray(ptr, ptr + size);
const name = Module.UTF8ToString(namePtr);
if (name.indexOf('__') === -1) {
const blob = new Blob([data]);
files[name] = URL.createObjectURL(blob);
}
if (i === n - 1) {
postMessage({files: {...files}});
files = {};
}
}Performance evaluation
Ten zip archives (0.3 MB–2 MB) were downloaded and decompressed three times each. The Wasm implementation consistently achieved initial unzip latencies of a few tens of milliseconds, whereas JSZip required additional JIT warm‑up time. After JIT optimization, JSZip’s average time approached the Wasm baseline, but the Wasm version showed less variance across runs because it bypasses the JIT compilation step.
References
WebAssembly – https://webassembly.org/
Emscripten – https://emscripten.org/
Zip library – https://github.com/kuba--/zip
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Huajiao Technology
The Huajiao Technology channel shares the latest Huajiao app tech on an irregular basis, offering a learning and exchange platform for tech enthusiasts.
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.
