- Published on
Optimizing Web Worker performance
- Authors
- Name
- Khánh
Web Workers
Web Workers is Javascript scripts that run on separate threads, independently from the main thread. This allow us to offload computationally-intensive task to preventing them to block the UI.
Example real-world use case:
- Image processing
- Video encoding
- Data analysis
- 3D rendering
- the team that built the videogame PROXX wanted to leave the main thread as free as possible to take care of user input and animations. To achieve that, they used web workers to run the game logic and state maintenance on a separate thread.
- ...
Web Worker vs Service Worker
Web Worker is a lightweight thread that is not bound to the DOM. It is used to offload computationally-intensive task to prevent them to block the main thread.
Service Worker is a more powerful thread that is bound to the DOM. It is used to cache resources and provide offline capabilities.
Web Worker life-cycle
Web Worker life-cycle is managed by the browser. When a web worker is created, it is executed in a new thread. When the web worker is finished, it is terminated.
Some keys techniques to optimize performance of Web Worker
1. Data transfer Optimization
Sending a large amount of data between the main thread and the web worker can be a bottleneck. -> To address this, consider to transfer smaller chunks. Break down the data into smaller pieces and send them to the web worker. This reduce the initial transfer time and memory usage.
// Example of a large dataset
const largeDataset = Array.from({ length: 1000000 }, (_, i) => ({
id: i,
value: Math.random()
}));
// Helper function to chunk array
const chunkArray = (array, chunkSize) => {
const chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
};
// Main thread
const CHUNK_SIZE = 10000; // Process 10,000 items at a time
const chunks = chunkArray(largeDataset, CHUNK_SIZE);
const worker = new Worker('worker.js');
// Track progress
let processedChunks = 0;
const totalChunks = chunks.length;
// Send chunks sequentially
const sendNextChunk = () => {
if (processedChunks < totalChunks) {
worker.postMessage({
type: 'PROCESS_CHUNK',
chunk: chunks[processedChunks],
chunkIndex: processedChunks,
totalChunks
});
} else {
worker.postMessage({ type: 'COMPLETE' });
}
};
// Handle worker responses
worker.onmessage = (event) => {
const { type, result, chunkIndex } = event.data;
if (type === 'CHUNK_COMPLETE') {
console.log(`Processed chunk ${chunkIndex + 1}/${totalChunks}`);
processedChunks++;
sendNextChunk(); // Send next chunk
} else if (type === 'PROCESSING_COMPLETE') {
console.log('All chunks processed:', result);
}
};
// Start processing
sendNextChunk();
// Worker script (worker.js)
let processedData = [];
self.onmessage = (event) => {
const { type, chunk, chunkIndex, totalChunks } = event.data;
if (type === 'PROCESS_CHUNK') {
// Process the chunk (example: calculate sum of values)
const chunkResult = chunk.reduce((acc, item) => acc + item.value, 0);
processedData[chunkIndex] = chunkResult;
// Send back chunk completion
self.postMessage({
type: 'CHUNK_COMPLETE',
chunkIndex
});
} else if (type === 'COMPLETE') {
const finalResult = processedData.reduce((acc, val) => acc + val, 0);
console.log('All chunks processed:', finalResult);
}
};
2. Leveraging shared Web Worker
Use single shared web worker for multiple components to eliminate the overhead of creating new workers for each component.
// Main thread
if (!window.sharedWorker) {
window.sharedWorker = new SharedWorker('worker.js');
}
// in other components
function useSharedWorker() {
const [data, setData] = useState(null);
useEffect(() => {
const worker = window.sharedWorker;
worker.port.postMessage({ task: 'longRunningTask' });
}, []);
}
3. Memorization
Store results of repeated calculations to avoid redundant computations, speeding up future requests and keeping the app responsive. The purpose of this technique is avoid redundant computations.
// In this example, we have a memo object used to store the results of repeated calculations.
const memo = {}
function expensiveCalculation(data){
if (memo[data]) {
return memo[data];
}
const result = performExpensiveCalculation(data);
memo[data] = result;
return result;
}
Conclusion
Optimizing web worker performance is a complex task that requires a deep understanding of the web worker lifecycle and the techniques to optimize performance. To archive the best performance, we need to combine these techniques together.