How to use WebAssembly modules in a web worker
This article was first published in June 2020 and was updated in September 2024.
WebAssembly is a binary format that helps developers achieve near-native performance inside the browser. However, even though the native-like performance is a big advantage of using WebAssembly, it has its share of issues.
Creating an instance of a WebAssembly module (wasm) can take several seconds. This is dependent upon its size, which in turn can have an effect on the load time. Meanwhile, when this module is moved to a web worker, the main thread is kept free because the process of fetching, compiling, and initializing happens on a separate thread.
In this article, you’ll create a small example to learn how to use WebAssembly modules inside web workers. You’ll write a function to add numbers in C++, convert the function to wasm using Emscripten, and then import the wasm file in a web worker. At the same time, you’ll also learn about communicating data across the main and worker threads in a convenient way.
Prerequisites
You’ll need Emscripten to convert your C++ code to WebAssembly, which can then be used on the web. You can do this by running the following commands:
# Get the emsdk repo. git clone https://github.com/emscripten-core/emsdk.git # Enter the directory. cd emsdk # Download and install the latest SDK tools. ./emsdk install latest # Make the "latest" SDK "active" for the current user (writes `~/.emscripten` file). ./emsdk activate latest # Activate `PATH` and other environment variables in the current terminal. source ./emsdk_env.sh
If you are using Windows, run emsdk
instead of ./emsdk
, and run emsdk_env.bat
instead of source ./emsdk_env.sh
.
You also have to install Comlink, webpack-cli, and few loaders:
npm i --save comlink npm i --save-dev webpack webpack-cli file-loader worker-loader
Next, as this blog post from LogRocket explains, “Comlink turns this message-based API into something more developer-friendly by providing an RPC implementation: values from one thread can be used within the other thread (and vice versa) just like local values.”
Project structure
Before starting, you can take a look at the final project structure, which will help you locate the correct place to put a particular file:
┣ wasm/ ┃ ┣ add.cpp ┃ ┣ add.js ┃ ┗ add.wasm ┣ index.html ┣ index.js ┣ package.json ┣ wasm.worker.js ┣ webpack.config.js
Start with index.html
, which is the main HTML file for your project. It’ll include the bundle.js
file generated by webpack, which contains the bundled JavaScript code for your application. The body section is left empty, as your JavaScript code will dynamically handle the page content and interactions:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Document</title> </head> <body></body> <script src="./dist/bundle.js"></script> </html>
C++
Once you’ve installed Emscripten, you can write a function that adds two numbers in C++:
// wasm/add.cpp #include <iostream> // extern "C" makes sure that the compiler does not mangle the name. extern "C" { int add(int a, int b) { return a + b; } }
Now you can convert the code above to a WebAssembly module that can be consumed on the web. Before running the conversion command, make sure you navigate to the wasm
directory:
cd wasm
Then, use Emscripten to compile the C++ code to WebAssembly with the following command:
emcc add.cpp -s ENVIRONMENT=worker -s MODULARIZE=1 -s EXPORTED_FUNCTIONS="['_add']" -o add.js
This will generate two files, named add.wasm
and add.js
, in the same directory. These files are the ones you can import directly on the web.
webpack configuration
You’ll need the following loaders to make sure you’re able to load the wasm file in a worker file and then register that worker as a script:
-
file-loader
— The default way in which webpack loads wasm files won’t work in a worker, so you’ll have to disable webpack’s default handling of wasm files and then fetch the wasm file by using the file path you get usingfile-loader
. -
worker-loader
— This loader allows you to import the worker file directly in your main file without worrying about its location. This loader also provides compatibility if the browser doesn’t support web workers.
The final webpack configuration will look like this:
// webpack.config.js module.exports = { mode: 'development', // Set to "development", "production", or "none". entry: './index.js', output: { filename: 'bundle.js', publicPath: 'dist/', globalObject: 'typeof self !== "object" ? self : this', }, module: { rules: [ { test: /\.worker\.js$/, use: { loader: 'worker-loader', options: { filename: '[name].worker.js', // Customize the filename for the worker if needed. publicPath: 'dist/', // Specify the public path if necessary. esModule: false, // Set esModule to `false` for compatibility. }, }, }, { test: /\.wasm$/, type: 'javascript/auto', // This disables webpack's default handling of wasm. use: [ { loader: 'file-loader', options: { name: 'wasm/[name].[hash].[ext]', publicPath: '/dist/', }, }, ], }, ], }, };
After setting up webpack, you can now successfully import the wasm file in the web worker file. Then you’ll use expose
from Comlink to expose the function so that it can easily be consumed by index.js
:
// `wasm.worker.js` import { expose } from 'comlink'; import addWasm from './wasm/add.wasm'; import addJS from './wasm/add.js'; const sum = async (a, b) => new Promise(async (resolve) => { const wasm = await fetch(addWasm); const buffer = await wasm.arrayBuffer(); const _instance = await addJS({ wasmBinary: buffer, }); resolve(_instance._add(a, b)); }); expose(sum);
In index.js
, you’ll import the worker file, which can be executed as a script, thanks to worker-loader
. Then you’ll use wrap
from Comlink to get a function that will directly call the sum
function you defined in the worker file. Since it has to wait until it receives a response from the worker, this function is always asynchronous. So the index.js
file will look like this:
// `index.js` import { wrap } from 'comlink'; import WasmWorker from './wasm.worker'; const wasmWorker = wrap(new WasmWorker()); (async function () { const result = await wasmWorker(1, 4); alert(result); })();
Now, add a script in package.json
to generate a bundle:
"scripts": {
"build": "webpack"
},
You can generate the bundle file by running npm run build
in the terminal, and the generated dist/bundle.js
can be imported in index.html
.
Run npx serve
from the root directory to serve the HTML on http://localhost:3000/. As soon as you open the URL on the browser, you’ll see an alert with the number five logged on it. This means the setup is working correctly. You can see the source code of this example on GitHub.
Conclusion
In this blog post, you created a small example to demonstrate how you can use WebAssembly modules inside a web worker. In Nutrient Web SDK, we use WebAssembly to provide a client-only standalone solution. This makes it easy for users to get up and running without worrying about complex backend infrastructure.
FAQ
Here are a few frequently asked questions about how to use WebAssembly modules in a web worker.
Why should WebAssembly modules be used in web workers?
Using WebAssembly in a web worker allows intensive tasks to run off the main thread, improving page load times and UI responsiveness.
How can Comlink simplify WebAssembly communication in a web worker?
Comlink streamlines data transfer between the main thread and web worker by making remote calls feel like local function calls.
What are the steps to compile a C++ function to WebAssembly using Emscripten?
After installing Emscripten, compile C++ code to WebAssembly by setting the ENVIRONMENT
and MODULARIZE
options for compatibility with web workers.
Why do I need file-loader and worker-loader for loading wasm files in webpack?
file-loader
correctly loads .wasm
files, while worker-loader
enables worker files to be imported as scripts in the main JavaScript file.
Can WebAssembly fully handle complex tasks in the browser without backend support?
Yes, WebAssembly can handle complex processing client-side, providing fast and efficient performance without needing backend infrastructure.