仓库源文站点原文


layout: "../layouts/BlogPost.astro" title: "Understand the event loop" slug: understand-the-event-loop description: "" added: "Aug 26 2020" tags: [js]

updatedDate: "Sep 16 2024"

Inside look at a browser

A process can be described as an application's executing program. A thread is the one that lives inside of process and executes any part of its process's program. Chrome has a multi-process architecture and each process is heavily multi-threaded. The renderer process is responsible for everything that happens inside of a tab.

In the most simple case, you can imagine each tab has its own renderer process. Because processes have their own private memory space, they often contain copies of common infrastructure. In order to save memory, Chrome puts a limit on how many processes it can spin up. The limit varies depending on how much memory and CPU power your device has, but when Chrome hits the limit, it starts to run multiple tabs from the same site in one process.

In a renderer process, the main thread is where a browser processes user events and paints. By default, the browser uses a single thread to run all the JavaScript in your page (sometimes parts of your JavaScript is handled by worker threads if you use a web worker or a service worker), as well as to perform layout, reflows, and garbage collection. This means that long-running JavaScript functions can block the thread, leading to an unresponsive page and a bad user experience. Frame drop happens when the main thread is too busy with running our JavaScript code so it doesn’t get the chance to update the UI so the website freezes.

Compositor and raster threads are also run inside of a renderer processes to render a page efficiently and smoothly. The benefit of compositing is that it is done without involving the main thread.

Everything outside of a tab is handled by the browser process. The browser process has threads like the UI thread which draws buttons and input fields of the browser, the network thread which deals with network stack to receive data from the internet, the storage thread that controls access to the files and more. For example, in the process of a navigation flow, the network thread tells UI thread that the data is ready, UI thread then finds a renderer process to carry on rendering of the web page.

To open the Chrome Task Manager, click on the three dots icon in the top right corner, then select 'More tools' and you can see 'Task Manager’. With this tool, you can monitor all running processes (CPU, memory, and network usage of each open tab and extension) and stop processes that are not responding.

Site Isolation (per-frame renderer processes) is a feature in Chrome that runs a separate renderer process for each cross-site iframe.

Event loop

<img alt="event-loop" src="https://raw.gitmirror.com/kexiZeroing/blog-images/main/008vxvgGly1h7ivwcb19zj317a0u0jvw.jpg" width="700" style="display:block; margin:auto">

The difference between the task queue and the microtask queue is simple but very important:

setTimeout(() => console.log(1), 0)
async function async1(){
  console.log(2)
  await async2()  // just syntactic sugar on top of promise
  console.log(3)
}
async function async2(){
  console.log(4)
}
async1()
new Promise((resolve, reject) => {
  console.log(5)
  for (let i = 0; i < 1000; i++) {
    i === 999 && resolve()
  }
  console.log(6)
}).then(() => {
  console.log(7)
})
console.log(8)
/*
output:2 4 5 6 8 3 7 1
*/

You may argue that setTimeout should be logged first because a task is run first before clearing the microtask queue. Well, you are right. But, no code runs in JS unless an event has occurred and the event is queued as a task. At the execution of any JS file, the JS engine wraps the contents in a function and associates the function with an event start, and add the event to the task queue. After emits the program start event, the JavaScript engine pulls that event off the queue, executes the registered handler, and then our program runs.

Any task that takes longer than 50 milliseconds is a long task. When a user attempts to interact with a page when there are many long tasks, the user interface will feel unresponsive. To prevent the main thread from being blocked for too long, you can break up a long task into several smaller ones. One method developers have used to break up tasks into smaller ones involves setTimeout(). With this technique, you pass the function to setTimeout(). This postpones execution of the callback into a separate task, even if you specify a timeout of 0.

// blocks the rendering (freezes the webpage)
button.addEventListener('click', event => {
  while (true) {}
});

// does NOT block the rendering
function loop() {
  setTimeout(loop, 0);
}
loop();

// blocks the rendering
(function loop() {
  Promise.resolve().then(loop);
})();
function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Loop over the tasks:
while (tasks.length > 0) {
  const task = tasks.shift();
  task();

  // Yield to the main thread
  await yieldToMain();
}

scheduler.yield() (in Chrome 129) provides a method for yielding control to the browser, which can be used to break up long tasks. Awaiting the promise returned by scheduler.yield() causes the current task to yield, continuing in a new browser task. This can be useful when you want to ensure that your JavaScript code doesn't block the main thread and negatively impact the user experience.

async function blocksInChunks() {
  // Blocks for 500ms, then yields to the browser scheduler
  blockMainThread(500);

  await scheduler.yield(); // The browser scheduler can run other tasks at this point

  // Blocks for another 500ms and returns
  blockMainThread(500);
}

The api and implementation is the result of a multi year collab effort between (meta) the react team and (google) chrome, and underpins react’s concurrent mode. Now being implemented in browsers as a standard.