How to Store WebAssembly Modules in Browser Cache

WebAssembly (WASM) modules can significantly improve web application performance, but fetching and compiling them on every page load introduces latency. This article explains how to efficiently store and retrieve WASM modules in the browser using the Cache API and IndexedDB, reducing network overhead and startup times for your web applications.

The Modern Standard: The Cache API

The most efficient and modern way to cache WASM modules is by using the browser’s Cache API. Instead of storing the compiled machine code, you cache the HTTP response of the .wasm file.

This approach is highly efficient because it integrates perfectly with WebAssembly.instantiateStreaming(). When you retrieve the response from the cache, you can feed it directly into the streaming compiler. The browser compiles the module in the background while it is being loaded from the local disk cache, resulting in near-instantaneous startup times.

Here is how to implement caching with the Cache API:

async function loadWasm(url) {
  const cacheName = 'wasm-cache-v1';
  const cache = await caches.open(cacheName);
  
  // Try to find the cached response
  let response = await cache.match(url);
  
  if (!response) {
    // Fetch from network if not cached and add to cache
    await cache.add(url);
    response = await cache.match(url);
  }
  
  // Instantiate directly from the cached response
  const { instance } = await WebAssembly.instantiateStreaming(response);
  return instance;
}

The Alternative: IndexedDB

Historically, developers attempted to store compiled WebAssembly.Module objects directly in IndexedDB to avoid the recompilation step entirely. However, storing compiled modules in IndexedDB has been deprecated or disabled in most modern browsers due to security vulnerabilities and serialization issues.

If you must use IndexedDB, the best practice is to store the raw binary data as an ArrayBuffer. When retrieving it, you must compile it using WebAssembly.instantiate().

// Storing WASM ArrayBuffer in IndexedDB
function storeWasmInDB(db, key, arrayBuffer) {
  const transaction = db.transaction(['wasmStore'], 'readwrite');
  const store = transaction.objectStore('wasmStore');
  store.put(arrayBuffer, key);
}

// Retrieving and instantiating from IndexedDB
async function loadWasmFromDB(db, key) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['wasmStore'], 'readonly');
    const store = transaction.objectStore('wasmStore');
    const request = store.get(key);

    request.onsuccess = async () => {
      const arrayBuffer = request.result;
      if (arrayBuffer) {
        const { instance } = await WebAssembly.instantiate(arrayBuffer);
        resolve(instance);
      } else {
        reject('WASM module not found in IndexedDB');
      }
    };
    request.onerror = () => reject('Database error');
  });
}

While IndexedDB works for raw binary storage, it requires full compilation of the ArrayBuffer upon retrieval, which is less efficient than the streaming compilation offered by the Cache API.

Summary of Best Practices

For optimal performance and cross-browser compatibility: * Use the Cache API as your primary caching mechanism. * Use WebAssembly.instantiateStreaming with the cached response to compile and instantiate the module simultaneously. * Avoid storing compiled modules in IndexedDB, as browser security policies restrict this behavior. Use raw ArrayBuffer storage in IndexedDB only as a fallback.