• How JavaScript Works: The building blocks of Web Workers + 5 cases when you should use them
  • Originally written by Alexander Zlatkov
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Liu Jiayi
  • Proofreader: Miao Yu, Mechanicia W

This is the seventh article in a series exploring JavaScript and its built-in components. In recognizing and describing these core elements, we’ll also share some rules of thumb that we followed when building SessionStack. SessionStack is a lightweight JavaScript application that helps users view and reproduce their Web application flaws in real time, so it needs to be robust and perform well on its own.

If you missed the previous posts, you can find them below:

  • An overview of the engine, runtime, and call stack
  • Dive into the V8 engine and five tips for writing better code
  • Memory management and four common solutions to memory leaks
  • The rise of event loops and asynchronous programming and 5 tips on how to better use async/await coding
  • How JavaScript works: An in-depth look at WebSockets versus HTTP/2 with SSE technology, and how to make the right choice between the two
  • Why is WebAssembly better than JavaScript in some situations

This time we will take a look at Web workers: After a brief overview, we will discuss the different types of workers and how their internal components work, as well as illustrate their strengths and weaknesses using scenarios as examples. At the end of the article, we’ll look at five scenarios that are best suited for using Web workers.

You already know that we’ve discussed JavaScript’s single-threaded execution mechanism in detail in previous articles. However, JavaScript allows developers to write asynchronous code on a single-threaded model.

The Ceiling of Asynchronous programming

We have already discussed the concept of asynchronous programming and its usage scenarios.

Asynchronous programming ensures that UI rendering is always a high priority by “putting” some of the code to execute later in the event cycle, so your UI doesn’t get stuck.

AJAX requests are one of the best practices for asynchronous programming. Usually network requests do not get a response in a short time, so asynchronous network requests allow clients to execute other business code while waiting for the response.

// If you are using jQuery, jquery. ajax({url:'https://api.example.com/endpoint',
    success: function(Response) {// Code to execute after correct response}});Copy the code

Of course, there is a problem here, the above example can make asynchronous request depends on the API provided by the browser, how to implement asynchronous execution of other code? For example, there are CPU-intensive calculations in the success callback function above:


var result = performCPUIntensiveCalculation();
Copy the code

If performCPUIntensiveCalculation not an HTTP request, but a thread can block code (ex: a giant for loop code). This would overwhelm the Event loop, and the browser UI would clog — the user would be left with an unresponsive page.

This illustrates that using asynchronous functions solves only a small part of the problem that JavaScript’s single-threaded model presents.

In some cases where UI blocks are caused by a large number of computations, using setTimeout to resolve the block works just fine. For example, we can batch a series of complex computations into a separate setTimeout, which is equivalent to splitting the continuous computations into different locations in the Event loop, thus freeing up time for UI rendering and event response.

Let’s look at a simple function that calculates the average of an array:


function average(numbers) {
    var len = numbers.length,
        sum = 0,
        i;

    if (len === 0) {
        return 0;
    } 
    
    for (i = 0; i < len; i++) {
        sum += numbers[i];
    }
   
    return sum / len;
}
Copy the code

Here is a rewrite of the above code to make it asynchronous:

function averageAsync(numbers, callback) {
    var len = numbers.length,
        sum = 0;

    if (len === 0) {
        return 0;
    } 

    function calculateSumAsync(i) {
        if(I < len) {// Place the next function call in the Event loopsetTimeout(function() {
                sum += numbers[i];
                calculateSumAsync(i + 1);
            }, 0);
        } elseCallback (sum/len); } } calculateSumAsync(0); }Copy the code

By using setTimeout, you can place each step of the calculation to a point in time after the Event loop. In between calculations, the Event Loop has enough time to perform other calculations so that the browser doesn’t freeze.

Save you from Web workers

HTML5 already offers a number of great things out of the box, including:

  • SSE (whose features were discussed and compared to WebSocket in the previous article)
  • Geographic information
  • The application cache
  • LocalStorage
  • Drag and drop gesture
  • Web Worker

A Web Worker is a lightweight thread built into the browser that executes JavaScript code without blocking the Event loop.

It’s amazing how all the paradigms in JavaScript are implemented based on a single-threaded model, but the Web Worker here (to a certain extent) breaks this limitation.

This allows developers to avoid UI blocking and make their applications more responsive by putting time-consuming, computationally intensive tasks in the background to the Web Worker. More importantly, we no longer need to apply any setTimeout black magic to the Event loop.

Here is a simple array sorting demo that compares the difference between using Web workers and not using Web workers.

An overview of the Web Worker

Web Workers allow you to perform a lot of computationally intensive tasks without blocking UI processes. In fact, the reason the two do not block is that they are executed in parallel, which shows that Web workers are truly multi-threaded.

You might want to say — “Isn’t JavaScript a language that executes on a single thread? “.

You might be surprised that JavaScript, as a programming language, doesn’t define any threading model. Therefore, Web workers are not part of the JavaScript language. They are only a feature provided by browsers, but can be accessed and invoked by JavaScript. Most browsers of the past were single-threaded programs (that used to be a given, but now it’s a bit different), and the browser has always been the primary environment for JavaScript to run. In contrast, there is no implementation of Web workers in Node.js – although Web workers correspond to the concepts of “cluster” or “child_process” in Node.js, they are different.

It is worth noting that the definition of Web Worker includes three types of workers altogether:

  • Dedicated Worker
  • Shared Worker
  • Service worker (Service worker)

Dedicated Worker

The Dedicated Worker is instantiated by the main thread and can only communicate with it.

Dedicated Worker browser compatibility overview

Shared Worker

Shared workers can be accessed by all threads in the same domain (different tabs, iframe, or other Shared Worker in the browser).

Shared Worker browser compatibility overview

Service Worker (Service Worker)

The Service Worker is an event-driven Worker whose initial registration requires the origin and path information of the web page/site. A registered Service Worker can control navigation of relevant pages/websites, resource requests, and granular resource caching operations, so you have great control over how your application performs in a given environment (e.g., when no network is available).

Service Worker browser compatibility overview

In this article, we focus on Dedicated workers, and “Web Worker” or “Worker” are used to refer to them by default.

How Web Workers work

The ultimate implementation of a Web Worker is a stack of.js files that the Web page loads through an asynchronous HTTP request. Of course, the Web Worker API already does all this, and the above loading is completely insensitive to the user.

Worker uses a thread-like message mechanism to keep it parallel to the main thread. It is the perfect person to improve your APPLICATION UI experience. Using Worker ensures real-time, high performance and fast response of UI rendering.

A Web Worker is a separate thread running inside the browser, so blocks of code that need to be run using a Web Worker must also be stored in a separate file. This needs to be kept in mind.

Let’s see how to create a base Worker:

var worker = new Worker('task.js');
Copy the code

If “task.js” exists and can be accessed, the browser creates a new thread to asynchronously download the source file. Once the download is complete, the code executes immediately and the Worker begins its work. If the supplied code file does not return 404, the Worker silently fails without throwing an exception.

To start the created Worker, you need to explicitly call the postMessage method:

worker.postMessage();
Copy the code

Web Worker communication

To enable communication between the created Worker and the page that created it, you need to use the postMessage method or Broadcast Channel.

Use the postMessage method

In newer browsers, the postMessage method supports a JSON object as the first input parameter to the function, but in older browsers it still only supports strings.

The following demo shows how the Worker communicates with the page that created it, and we’ll use JSON objects as communication bodies to make the demo look a little “complicated.” If you pass a string instead, the method also speaks for itself.

Let’s take a look at the following HTML page (or rather snippet) :

<button onclick="startComputation()">Start computation</button>

<script>
  function startComputation() {
    worker.postMessage({'cmd': 'average'.'data': [1, 2, 3, 4]});
  }
  var worker = new Worker('doWork.js');
  worker.addEventListener('message'.function(e) {
    console.log(e.data);
  }, false);
  
</script>
Copy the code

This is the content in the Worker script:

self.addEventListener('message'.function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'average': var result = calculateAverage(data); Self.postmessage (result); self.postmessage (result);break;
    default:
      self.postMessage('Unknown command'); }},false);
Copy the code

The postMessage method is invoked when the button in the main page is pressed. The worker.postMessage line passes a JSON object to the worker that contains the CMD and data keys and their corresponding values. Accordingly, the Worker gets and processes the message content passed from above through the defined message response method.

When the message arrives at the Worker, the actual calculation starts running so that the event loop is not blocked at all. During this process, the Worker simply checks the event E that was passed in and continues executing as usual with the JavaScript function. When the final execution is complete, the result is returned to the main page.

In the context of Worker execution, self and this both point to the Worker’s global scope.

There are two ways to stop the Worker: 1. Explicitly call worker.terminate() on the home page; 2. Call self.close() in the script to let the Worker terminate.

Broadcast Channel

Broadcast channels are apis that are more purely for communication. It allows us to send and receive messages in all contexts of the same domain, including browser tabs, iframe, and workers:

Var BC = new BroadcastChannel('test_channel'); // Send a simple message bc.postMessage('This is a test message.'); // This is a simple event handler. // We will receive and print messages to the terminal bc.onMessage =function(e) { console.log(e.data); } // Disconnect from Broadcast Channel bc.close()Copy the code

The following diagram will help you understand how Broadcast Channel works:

Using Broadcast channels has stricter browser compatibility restrictions:

Message size

There are two ways to send a message to a Web Worker:

  • Copy message: In this method, the message is serialized, copied, and then sent, and the receiver receives the message and deserializes it. Therefore, the page and Worker in the above example will not share the same message instance, and each time a message is sent between them, an additional message copy will be created. Most browsers use this sending method and automatically encode/decode JSON on both the sending and receiving sides. As you might expect, this data processing can be quite a burden for messaging. The larger the message, the greater the time overhead.
  • Passing the message: Using this method means that once the message sender successfully sends the message, it is no longer able to use the sent message data. The message takes almost no time to pass, but only ArrayBuffer supports sending in this way.

JavaScript features supported in Web Workers

Because of the multithreaded nature of the Web Worker, it can only use a handful of features provided by JavaScript, listed below:

  • navigatorobject
  • locationObject (read only)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • The application cache
  • useimportScripts()Introducing external scripts
  • Create other Web workers

Limitations of Web workers

Unfortunately, Web workers don’t have access to some very important JavaScript features:

  • DOM elements (access is not thread-safe)
  • windowobject
  • documentobject
  • parentobject

This means that the Web Worker cannot do any DOM manipulation (i.e., UI-level work). This can be a little tricky at first, but once you learn how to use Web Workers properly. You just use the Web Worker as a single “computing machine” and put all the UI operations into the page code. You can hand over all the dirty work to the Web Worker, upload the results of its work to the page and do the necessary UI operations there.

Exception handling

As with any JavaScript code, you want to handle any errors thrown by the Web Worker. When an error occurs while the Worker is running, it fires the ErrorEvent event. This interface contains three useful attributes that can help you locate the cause of code errors:

  • Filename – the script filename in which the error occurred
  • Lineno – Line number of code where the error occurred
  • Message – Error message

Here’s an example:

function onError(e) {
  console.log('Line: ' + e.lineno);
  console.log('In: ' + e.filename);
  console.log('Message: ' + e.message);
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('error', onError, false); worker.postMessage(); // Start the Worker without passing messagesCopy the code
self.addEventListener('message'.function(e) { postMessage(x * 2); // This line intentionally uses undeclared variables'x'
};
Copy the code

As you can see, we create a Worker here and listen for its error events.

We intentionally create an exception inside the Worker (in the workerWitheror.js file) by multiplying with a variable x that is undefined in scope. This exception is passed to the scrpit where the Worker was originally created, and onError is called.

Best practices for Web workers

Now that we’ve seen the strengths and weaknesses of Web workers, let’s take a look at some of the best scenarios for using them:

  • Ray Tracing is the Rendering technology in computer graphics, which traces and converts the path of light into pixels and finally generates a complete image. Ray tracing requires a lot of math on the CPU to simulate the path of the ray. Ray tracing includes simulating the reflection, refraction and material effects of light. All of the above calculation logic can be handed over to the Web Worker without blocking the execution of the UI thread. Or better yet, use multiple workers (and cpus) for image rendering. There is a demo of ray tracing using Web Worker – nerget.com/rayjs-mt/ra… .

  • Encryption: Protection regulations for sensitive personal data are becoming more stringent, and end-to-end data encryption is becoming more popular. Encryption becomes a time-consuming task when a large amount of data needs to be encrypted frequently in a program, such as sending data to a server. Web workers can cut into this kind of scenario very well because there is no DOM manipulation involved and only some encryption algorithms run in the Worker. Worker works diligently and silently without bothering users or affecting their experience.

  • Data prefetch: To optimize the data loading time for your website or Web application, you can use the Web Worker to pre-fetch some data and store it for later use. The Web Worker plays an important role here, because it never affects the UI experience of the app, and it would be worse without the Web Worker.

  • Progressive Web App: When the network is not perfect, you still need to make sure the PWA loads fast. This means that the PWA data needs to be persisted to the local browser. In this context, some API similar to IndexDB came into being. Basically, you need data storage capability on the client side. To ensure that access does not block the UI thread, this should be done by the Web Worker. Well, you don’t have to use the Web Worker in IndexDB, because it also provides an asynchronous API that doesn’t block the UI. Until then, however, IndexDB provides a synchronization API (which may be reintroduced), in which case using the Web Worker is still necessary.

  • Spell check: The basic process for a spell check is as follows – the program first reads a list of correctly spelled words from a dictionary file. The entire dictionary of words is parsed into a search tree for the actual text search. When the word to be tested is entered, the program checks whether the word exists in the established search tree. If the word under test is not matched in the search tree, the program will replace the characters to form a new word, and test whether the new word is expected by the user, if so, it will return the word. The entire detection process can be easily “delegated” to the Web Worker, who does all the word retrieval and word association work so that user input does not clog the UI.

Maintaining high performance and high reliability is extremely important for SessionStack. The main reason for holding this belief is that once your application integrates SessionStack, it starts recording everything from DOM changes and user interactions to network requests, uncaught exceptions, and debug messages. The collected tracking data is sent to the backend server in real time to show you the problems in the application in the form of video, helping you to recreate the error scene from the user’s point of view. All of these features need to be implemented quickly and without imposing any performance burden on your application.

That’s why we try to give the Web Worker as much business logic in the SessionStack as possible that is worth optimizing. CPU intensive tasks such as hash data integrity verification and rendering are included in core monitoring libraries and players that are worth optimizing with Web Worker.

Web technology continues to change and evolve, so we want to stay ahead of the game and make sure SessionStack is a lightweight application that doesn’t take any performance toll on users’ apps.

If you’d like to try SessionStack, there’s a free trial program.

The resources

  • www.html5rocks.com/en/tutorial…
  • Hacks.mozilla.org/2015/07/how…
  • Developer.mozilla.org/en-US/docs/…

The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.