缓存已编译的WebAssembly模块

对于提高应用的性能来说,缓存是很有用的——我们可以在客户端存储已编译的WebAssembly模块,从而可以避免每次都下载和编译它们。本文解释了这方面的最佳实践。

使用IndexedDB实现缓存

IndexedDB是一个事务型数据库系统,它允许你在客户端存储和获取结构化数据。它适合在本地保存包含应用程序状态的资源,包括文本、二进制大对象以及其他任何可以克隆的对象。

这包括已编译的wasm模块(WebAssembly.Module JavaScript对象)。

建立缓存库

因为IndexedDB在一定程度上是老式风格的API,所以,我们想提供一个库函数以便加快写缓存代码的速度并使它能够更好的与当今更现代的API配合。

在我们的wasm-utils.js脚本库中,你会发现instantiateCachedURL()——该函数使用给定版本的dbVersion获取给定url的wasm模块,使用给定的importObject实例化,并且返回一个将会解析成最终的wasm实例的promise。而且,它会创建一个用来把已编译的wasm模块缓存起来的数据库,尝试在数据库中存储新的模块,并且从数据库中获取之前缓存的模块,从而使你免于再次下载它们。

: 整个网站的wasm缓存(不只是给定的URL)是通过传入到函数中的dbVersion进行版本控制的。如果wasm模块代码更新了或者它的URL发生了变化,你需要更新dbVersion。对于instantiateCachedURL()的任何后续调用将会清除掉全部的缓存,从而使你避免使用过时的模块。

该函数从定义一些必要的常量开始:

    function instantiateCachedURL(dbVersion, url, importObject) {
     const dbName = 'wasm-cache';
     const storeName = 'wasm-cache';

建立数据库

在instantiateCachedURL()中的第一个辅助函数openDatabase(),创建一个存储wasm模块的对象存储空间,以及在dbVersion更新后清除数据库;它返回一个解析成新的数据库的promise。

     function openDatabase() {
     return new Promise((resolve, reject) => {
     var request = indexedDB.open(dbName, dbVersion);
     request.onerror = reject.bind(null, 'Error opening wasm cache database');
     request.onsuccess = () => { resolve(request.result) };
     request.onupgradeneeded = event => {
     var db = request.result;
     if (db.objectStoreNames.contains(storeName)) {
     console.log(`Clearing out version ${event.oldVersion} wasm cache`);
     db.deleteObjectStore(storeName);
     }
     console.log(`Creating version ${event.newVersion} wasm cache`);
     db.createObjectStore(storeName)
     };
     });
     }

在数据库中查找模块

我们的下一个函数lookupInDatabase(),提供了一个简单的基于promise的操作,用来在我们之前创建的对象存储空间中查找给定的url。如果成功,它可以解析出存储的已编译模块;如果失败,它会给出一个错误。

     function lookupInDatabase(db) {
     return new Promise((resolve, reject) => {
     var store = db.transaction([storeName]).objectStore(storeName);
     var request = store.get(url);
     request.onerror = reject.bind(null, `Error getting wasm module ${url}`);
     request.onsuccess = event => {
     if (request.result)
     resolve(request.result);
     else
     reject(`Module ${url} was not found in wasm cache`);
     }
     });
     }

存储和实例化模块

接下来,我们定义了一个函数storeInDatabase(),它可以触发一个异步操作,从而在给定的数据库中存储给定的wasm模块。

     function storeInDatabase(db, module) {
     var store = db.transaction([storeName], 'readwrite').objectStore(storeName);
     var request = store.put(module, url);
     request.onerror = err => { console.log(`Failed to store in wasm cache: ${err}`) };
     request.onsuccess = err => { console.log(`Successfully stored ${url} in wasm cache`) };
     }

最后,我们定义一个辅助函数——fetchAndInstantiate(),它从给定的url获取数据,将其编译成一个模块,并且使用给定的导入对象实例化该模块。

     function fetchAndInstantiate() {
     return fetch(url).then(response =>
     response.arrayBuffer()
     ).then(buffer =>
     WebAssembly.instantiate(buffer, importObject)
     )
     }

使用辅助函数

使用这些定义好的基于Promise的辅助函数,我们现在可以表达一个IndexedDB缓存查找的核心逻辑了。首先,我们通过尝试打开一个数据库,然后,查看在给定的数据库db中是否存在与url相对应的模块:

     return openDatabase().then(db => {
     return lookupInDatabase(db).then(module => {

如果找到了模块,那么,使用给定的导入对象对其进行实例化:

     console.log(`Found ${url} in wasm cache`);
     return WebAssembly.instantiate(module, importObject);
     },

如果没有找到,那么,我们从零开始编译它,然后,使用给定的url作为键,将已编译的模块存储到数据库中,方便下次使用。

    errMsg => {
        console.log(errMsg);
        return fetchAndInstantiate().then(results => {
            storeInDatabase(db, results.module);
            return results.instance;
        });
     },

WebAssembly.instantiate()返回一个模块和实例为的就是这种用处:模块表示已经编译的代码,并且可以在IndexedDB中存取或者通过postMessage()在Workers之间共享;实例是具有状态的,并且包含了可以调用的JavaScript函数,因此不能够被存储或者共享。

如果打开数据库失败(比如由于权限或者空间限制),我们改用获取和编译模块的方式,并且不再尝试保存结果(因为没有数据库可以保存它们)。

     errMsg => {
        console.log(errMsg);
        return fetchAndInstantiate().then(results =>
            results.instance
        );
    }

缓存wasm模块

有了上面定义的库函数,取得一个wasm模块实例,并且使用它的导出特性(同时在后台处理缓存)只需要使用下面的参数进行调用即可:

  • 缓存的版本号——正如我们上面解释的那样,当任何wasm模块发生更新或者移动到不同的URL,你都需要更新它。
  • 你想要实例化的wasm模块的URL。
  • 一个可选的导入对象。
const wasmCacheVersion = 1;

instantiateCachedURL(wasmCacheVersion, 'test.wasm').then(instance =>
  console.log("Instance says the answer is: " + instance.exports.answer())
).catch(err =>
  console.error("Failure to instantiate: " + err)
);

你可以在GitHub上找到这个例子的源代码indexeddb-cache.html(或者实时运行)。