Webpack can bundle code for Web Workers, allowing you to run JavaScript in background threads separate from your main browser UI thread.
Let’s see it in action. Imagine you have a computationally intensive task, like generating a large prime number list, that would otherwise freeze your UI.
// src/worker.js
self.onmessage = function(event) {
const maxNumber = event.data;
const primes = [];
for (let i = 2; i <= maxNumber; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
}
self.postMessage(primes);
};
This worker.js file defines a simple prime number generator. When it receives a message (the maxNumber), it calculates the primes up to that number and sends the result back using self.postMessage().
Now, let’s configure Webpack to bundle this worker code.
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
exclude: /node_modules/,
},
],
},
// This is the key for Web Workers
experiments: {
outputModule: true, // Needed for modern worker imports
},
};
The crucial part here is experiments.outputModule: true. This enables ES module output, which is necessary for the modern new Worker('./worker.js') syntax to work correctly with Webpack. Webpack will automatically detect the new Worker() call and create a separate bundled file for your worker.
Here’s how you’d use it in your main application script:
// src/index.js
import('./worker.js').then(({ default: Worker }) => {
const myWorker = new Worker();
myWorker.onmessage = function(event) {
console.log('Received primes:', event.data);
// Update UI with primes
};
myWorker.onerror = function(error) {
console.error('Worker error:', error);
};
// Start the computation
myWorker.postMessage(100000);
console.log('Prime calculation started...');
});
When you run npx webpack, Webpack will create dist/bundle.js and a separate, automatically named worker file (e.g., dist/src_worker.js or similar, depending on your Webpack version and configuration). The import('./worker.js') syntax is dynamic import, which Webpack also handles, creating a separate chunk for the worker. The default: Worker destructuring is because Webpack, when bundling a worker file as an ES module, exports the worker constructor as the default export.
The primary benefit is responsiveness. By moving heavy computations off the main thread, your UI remains interactive. Users can scroll, click, and type without experiencing jank or freezes. This is especially important for complex data visualizations, image processing, or any task that could take more than a few milliseconds to complete. Web Workers communicate via message passing, meaning data is copied between threads, not shared directly, which avoids many common concurrency issues.
When you use new Worker(new URL('./worker.js', import.meta.url)), Webpack is able to resolve the path to your worker file relative to the current module’s location. This is the more robust and recommended way to instantiate workers with modern bundlers like Webpack 5+. It ensures that Webpack correctly identifies and bundles the worker script as a separate asset, even when your main bundle.js is in a different directory structure.
The import.meta.url part is a JavaScript feature that provides the base URL of the current module. By passing this to the URL constructor along with the relative path to your worker, you give Webpack precise information about where the worker script is located relative to your main application code, allowing it to bundle it correctly.
The next hurdle is often managing state and more complex inter-worker communication.