Web workers: Why and how we use them

The content analysis is a staple of our SEO plugin. We do our best to provide you with the best insights on how you could make your texts more readable and search-engine-friendly. In order to be able to give you this valuable feedback, we need to do a lot of computations. A logical solution would be to send your data to one of our own servers and analyze it there. However, we have chosen to do the computations in your browser. For one simple reason - this keeps your data as close to where it belongs: with you. But, choosing to analyze your data locally poses a challenge. We do not want to hog all of your browser’s resources and make your post-editing experience worse. To prevent this, we use a technique called web workers.

Computations in the browser: pros and cons

Many online apps choose to process users’ data on the company’s own servers. This allows them to run elaborated analysis without having to rely on the capacity of a user’s computer. More importantly, being able to access the data of many users gives a company endless possibilities. The possibility to research the performance of their products in detail, for example. Or to broadly apply innovative Big Data algorithms in their programs.

We at Yoast believe that you have a right to keep your data to yourself. We respect your privacy. That is why we choose to keep your data as close as possible to where it is created, in your browser. We do not transfer your website’s data to our servers, but we run all necessary calculations on your machine. Besides being friendly to your privacy, this method allows us to make sure the analysis will complete. Even if your internet connection suddenly breaks. Furthermore, since the data do not need to travel between your computer and our server and back, no extra lag gets introduced. This means that we are able to render some results almost immediately.

The most important drawback of such a browser-based setup is obvious: Because the creators of an app you are using do not know how powerful your computer is, they can unknowingly overload your browser and make your page freeze. Imagine that you are busy creating or optimizing a blog post and you run a plugin on the background that is meant to help you with that. But, instead of pointing out the problems, the plugin freezes your cursor and makes the buttons on your page unresponsive. Pretty annoying, right?

To combat this, we at Yoast chose to use a technology called web workers to optimize browser-based computations for UX.

What are web workers?

Web workers run JavaScript files for you in the background. This frees up valuable resources in your browser’s main thread, the default ‘worker’ that handles user input, among other things.

You can create a web worker by making a new instance of Worker. You should supply the path of the script you want the worker to execute as its argument:

const worker = new Worker( ‘/path/to/worker-script.js’ );

A web worker can communicate with the main thread, and other web workers, using messages. Messages can consist of anything that can easily be serialized and unserialized. This means that things like functions or HTML elements cannot be sent in a message. Instead, messages consisting of numbers, strings, plain objects, and plain arrays can be sent safely.

Sending messages to web workers

You can post a message to a worker using its postMessage method. This method requires just one argument: the message itself.

// Create the message.
const message = { type: ‘analyze’, payload: ‘Hello, world!’ };

// Post the message to the worker.
worker.postMessage( message );

You can handle this message in the worker by implementing an onmessage event handler:

/* 
 * In ‘/path/to/worker-script.js’
 * Note that this function is set on the worker’s global scope.
 * It does not need to be bound to a local variable or `this`,  
 * `window` or `document`.
 */
onmessage = function( event ) {
	const message = event.data; 
	console.log( `Received message from main thread of type ’${message.type}’ with payload`, message.payload );
}

Sending messages from web workers

You can send a message from a web worker by calling postMessage in the worker script:

/*
 * Note that this is again a function on the worker’s global scope,
 * but this time it is a function call, instead of a definition.
 */
postMessage( { type: ‘analysis:done’, result: ‘AWESOME’ } );

Receiving messages from web workers works by implementing an `onmessage` event handler on the worker object:

worker.onmessage = function( event ) {
	const message = event.data; 
	console.log( `Received message from web worker of type ’${message.type}’ with payload`, message.payload );
};

With this functionality, you can implement your own basic web worker pretty fast. However, it can become quite complex when the web worker needs to handle multiple types of tasks and needs to do different things when these tasks have been completed.

How do we use web workers?

We created our own interface to handle the added complexity of web workers. This interface handles three things:

  • Requesting an analysis, or other computation, from the web worker.
  • Scheduling this analysis.
  • Communicating the results of the analysis back to the main thread.

Communicating with the web worker

To make it easier to communicate with the web worker, we added a class we called the AnalysisWorkerWrapper. This class wraps a web worker and provides a nice and clean functional interface for interacting with it. This means that, instead of hassling with messages to and from the web worker, a programmer can call a function on the wrapper instead. However, because of the asynchronous nature of web workers, the function returns promises instead of giving the result directly.

We can run the analysis, for example, by calling:

analysisWorkerWrapper.analyze( content ).then( ( { result } ) => {
	console.log( ‘Results from analysis:’, result );
} );

The wrapper does two things when it receives a method call like the one above. 

First of all, it sends an appropriate message to the web worker, using its postMessage method. In this case the message looks something like this:

{
  type:	‘analyze’,
  id: 1,
  payload: { content }	
}

Second of all, it creates a promise and keeps track of it. Once the web worker posts a message back to the wrapper with the results of a request, the wrapper looks up the promise (using its id) and resolves it. If there was an error during the analysis, the promise gets rejected instead.

Scheduling computations

We also added a way to schedule computations. This way we can control how often an expensive computation like our content analysis can be invoked. 

For example, we throttle the analysis such that it runs once every 50 milliseconds. On top of that, we only compute the latest analysis request sent within the last 50 milliseconds and discard the others. This way we can give you the latest results, without the extra calculations needed for analyses. It is safe to discard the other analyses since they would give outdated results anyways.

Conclusion

Web workers allow to free the browser’s main thread from heavy computations by running them in the background. This is a really nice solution if your app performs a serious analysis of large data in the user’s browser. Whether to stick to this browser-based approach or a server-based approach is totally up to you, of course. But, don’t forget that technologies such as web workers can allow great UX even if your computation power is limited.