Main thread work is any work that takes longer than a few milliseconds to complete. These are usually CPU-intensive tasks that cause a frame or animation on the webpage to lag.
It’s always best to minimize and reduce main thread work because it can affect the user-experience of your website and app by decreasing the responsiveness with which they loaded, decreased scrolling speed, delayed UI updates (such as form input fields), and even making animations stutter or become choppy.
There are a number of ways to ensure that the main thread stays responsive and doesn’t get overloaded. Each solution has its trade-offs, so understanding the difference between them will help you pick the right approach for your site.
The main thread is the UI thread that handles user interactions with your application. To ensure that a webpage stays responsive and doesn’t get overloaded, you should avoid work that takes a long time to complete.
The best way to do this is by accessing the main thread only when it is absolutely necessary, and then releasing any resources held as quickly as possible (and ideally, returning it back to the operating system so it can be made available and used by other processes).
There are two major ways of doing this: off-main-thread work and web workers.
Off-Main-Thread Work
The first option is to perform long running work off the main UI thread, so that you don’t block the important stuff (like UI updates) from happening. In other words, we want to perform the slow work in a different thread, and then have an event handler somewhere else on the page notify when that long-running task is done.
There are three ways to implement this: using setTimeout , a single-threaded queue system, or web workers.
Single-Threaded Queue System
Using a single-threaded queue system is the most straightforward way to handle OMT work. This technique consists of an array (or some other data structure) that functions as a queue for future events.
When new items are added to the queue, they get handled in the order they were enqueued. The problem with this approach is that it can be very inefficient, because adding an item to the queue doesn’t execute it; instead, it just adds an item to the end of the queue.
So, instead of working in a serial fashion, the items are enqueued one after the other. If you can process the items in parallel, then this approach might be appropriate for you. You can use this approach for:
- recording analytics data;
- long-running AJAX requests (such as fetching content from other servers); and/or
- CPU-intensive calculations that don’t require user interaction (like incremental background updates).
SetTimeout Loop
An alternative to using a queue is to use a single loop that continuously calls setTimeout until it’s done (or until the loop has been running for longer than a certain number of milliseconds).
This is the older of the two options, so it’s more likely to be supported in all modern browsers. If, however, you need support for IE8 or earlier versions of IE (including IE9), then you’ll need to use a third-party polyfill like this.
var totalTime = 0; var totalSoFar = 0; var index = -1; function onTimeout() { var now = new Date(); if (now - lastTime < interval) { console.log('still processing.'); } else { console.log('done processing. took: ' + (now - lastTime) + 'ms.'); } lastTime = now; index += 1; } function doAJAX(url) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = onTimeout; xhr.open("GET", url, true); xhr.send(null); } doAJAX('/some/url/that/takes/forever.php'); // ... totalTime += now - lastTime;
Workers and Message Passing
Modern browsers support web workers. A web worker is basically a thread that executes JavaScript code in the background without effecting the main thread. This means that you can do work on one or more web workers without slowing down your application.
The problem with this approach is that it’s difficult to control the child thread from the parent thread (you can’t call functions directly from the parent).
Instead, you have to pass messages back and forth: send data through a message event, and then wait for a response through an onmessage handler. This may seem clumsy but it’s actually quite efficient, and it’s supported in all modern browsers (and IE8+ with a polyfill).
var worker = new Worker('path/to/file.js'); worker.addEventListener('message', function(e) { console.log('Worker said: ', e.data); });
This approach works well for a large number of different types of tasks: including HTTP requests, image manipulation, long-running calculations, and more.