How does the browser execute JS? –Message queue and event loop

After watching Toichi’s class, I feel that this content is indeed very important. It is not okay for those who write JS not to even know the execution principle of JS.

Event Loop

When writing JS, have you ever thought about the order in which JS is executed? How does the browser execute JS code? Why does code sometimes not execute in the order we think it should? As an interpreted scripting language, JS can use operations such as timers and callback functions?

In fact, there is a sophisticated and complex mechanism hidden behind the browser, which is the event loop. This mechanism allows the web page to respond to user operations while maintaining the smoothness and efficiency of the interface. The event loop is one of the crucial concepts in modern front-end development. It is responsible for managing various asynchronous operations, such as user input, network requests, timers, etc. This is at the browser level and something you must know when working on the front end.

A deep understanding of the event loop in browsers will allow us to better understand how JavaScript works in front-end development. This article will analyze the internal mechanism of the event loop in detail and provide practical examples to help you better utilize this mechanism to build an excellent interactive experience.

1. Process model

In order to understand the scope of the event loop, we have to mention some underlying concepts of the operating system – processes and threads. If you don’t understand these concepts, you will definitely not understand the event loop. I’ll try to explain it as briefly as possible, the more you understand here, the faster you can understand the event loop the better.

Of course, the concepts of processes and threads are also extremely important low-level knowledge. If you don’t understand these at all, it is best to read the detailed explanation first.

What is a process

  • A program needs its own dedicated memory space to run. This memory space can be simply understood as a process.
  • Each application has at least one process, and the processes are independent of each other. Even if they want to communicate, both parties must agree.

In the operating system, a process refers to an instance of a running program, which includes the program’s code, data, and resources required for program execution. Each process has its own independent memory space and can perform different tasks at the same time. Moreover, a program can occupy multiple processes. One of the main processes is started by the operating system when the program is running, and other processes are started by this process to share other tasks for it. It is responsible for managing system resources, scheduling the execution sequence of tasks, and providing the necessary environment for programs.

Each process has an independent memory space, and they do not share memory directly, so they are isolated from each other. This is also the reason why variables in one process cannot be directly accessed by another process; but if they reach a certain agreement and both parties agree on the transmission of messages, they can communicate with each other and transfer data.

What is a thread

  • After you have a process, you can run code. The running code can become a thread, which runs in the process environment.
  • A process has at least one thread. After the process is started, it will automatically create a thread to run the code. This thread is called the main thread.
  • If you need to execute multiple codes at the same time, that is, perform multiple operations in parallel, the main thread will start more threads to execute other codes.

Thread is the smallest unit of program execution. It is an independent execution flow in the process. A process can contain multiple threads and can perform different tasks at the same time. These threads share the same memory space and other resources, so they can easily communicate and coordinate with each other.

Browser processes and threads

  • The browser is a multi-process, multi-threaded application
  • In order to avoid mutual influence, after starting the browser, multiple processes will run (also because the browser is an extremely complex software, it is difficult to coordinate the work with only a few processes)
  • The browser has three important processes:
    • Browser process: interface display, user interaction, sub-process management, etc., in which multiple threads will be started to handle different tasks
    • Network process: Responsible for loading network resources. Multiple threads will also be started in this process to handle different network tasks.
    • Rendering process: 1 tab, one rendering process
  • Rendering process:
    • After startup, a main rendering thread will be opened, and the main thread will execute HTML, CSS, and JS codes.
    • By default, the browser will start a new rendering process for each tab to ensure that different tabs will not affect each other.
    • This model may be changed later (it takes up a lot of resources when there are too many processes)

Why one page and one process?

Because it is easy for users to open many more pages. If so many pages share a memory space, that is, they share the same process, it is easy for a page to have a bug and cause the memory to crash and the entire process to freeze. The browser has to be restarted because all pages are unusable.

But one page and one process can prevent the exception of one page from affecting other pages. For example, if you usually use a browser, the CSDN next to it will not be stuck because the Zhihu website crashes. You can still use CSDN and just reopen Zhihu.

So much about processes and threads, because this is not the focus of this article.

2. Rendering main thread

How does it work?

The main rendering thread is the thread with the largest workload. The programs that need to be processed include but are not limited to:

  • Parse HTML
  • Parse CSS
  • calculation style
  • layout
  • Work with layers
  • Refresh the page 60 times per second (rendering frames)
  • Execute global JS code
  • Execute event handler function
  • Execution timer callback function

Thinking: Why don’t browsers use multiple threads to handle the above tasks?

How to schedule so many tasks?

For example:

  • A JS function is being executed, and the user clicks the button halfway through execution. Should I immediately execute the handler function for the click event?
  • I am executing a JS function, and a timer reaches a time node halfway through execution. Should I execute the timer’s callback immediately?
  • The browser process monitors that the user clicks a button, but at the same time a timer also reaches a time node. Which one should I handle?

In order to solve some of the above scheduling problems, the main rendering thread adopts a “queuing” method.

We call all the things we need to do one by one as tasks. The action of rendering the main thread can be seen as the response and execution of tasks one after another. When the main rendering thread is executing a task, all incoming tasks that need to be executed will enter a Message Queue (Event Queue). Each time a task arrives, the task will be During the time when the previous task is not completed, it will be queued in this queue and wait.

The following is the main work of the rendering main thread:

  1. When the main rendering thread starts, it will enter an infinite loop.
  2. In each cycle, it will check whether there are unexecuted tasks waiting in the Message Queue mentioned above. If there are, the first task will be taken out, which is the earliest arriving task to start execution. , enter the next loop after execution, if not, enter sleep state
  3. All other threads can add tasks to the message queue at any time. The new task will be added to the end of the message queue. If the main thread is sleeping at this time, the main thread will be awakened, start a loop, and get tasks to execute in the loop.

The main steps of Event Loop (Message Loop) are the above three points, which ensure that the page can perform the event completion function normally.

Deeper understanding

The above describes the general concept and steps of the event loop. The following explains some more detailed things, which can give us a deeper understanding.

What is asynchronous

When writing JS on the front end, synchronization and asynchronousness are always inevitable. Synchronization is easy to understand. It means executing js code step by step, from top to bottom, line by line. So what is asynchronous?

During the execution of the code, you may encounter some tasks that cannot be executed immediately, such as:

  • The callback task is triggered after the timer ends (setTimeout, setInterval…)
  • Tasks that need to be performed after the network communication is completed (operations after sending a request to the backend…)
  • Tasks that need to be performed after user operation (addEventListener…)

If the main rendering thread is allowed to wait for each task to be executed before executing the next task, a lot of time may be wasted and the normal operation of the page will be affected. For example, if a one-minute timer is set, when the task is fetched from the message queue, it is impossible to wait until the timer task is executed one minute later before the next task is executed. Doing nothing for a minute is completely wasted, and may even cause the page to freeze.

Briefly mention how the timer works:

When the timer starts to be called, the timer will notify the timing thread and let the timing thread start timing. The main thread and the timing thread are executed in parallel and belong to the same page process.

If you use one after another, the previous task is completely executed before the next task is executed. This idea is synchronization. Although this can ensure that the timeline is single and not confusing, as the above example shows, the problem is very serious. So that’s not how the main rendering thread works.

setTimeout(() => {<!-- -->
    console.log("Timer ended")
}, 3000)
console.log(1)

If the browser is executed synchronously, then the JS code is from top to bottom, and the next line of code block will not be executed until the previous code block is executed. Then the above code will first output “timer ended” and then output 1. If it is asynchronous, the printing order should be reversed: print 1 first, then print “timer ended”. You can try the above test code yourself.

In fact, the running principle of the above test code is as follows: after the main thread triggers the timer, it will immediately obtain the next task. When the timing thread ends, the timing thread will not notify the main thread, but will directly call back The function is added to the message queue. So although the main thread cannot directly know that the timer has ended, it can still know from the message queue when to execute the timer’s callback function.

Let’s give a summary of the above knowledge:

JS is a single-threaded language because it runs in the browser’s main rendering thread, and there is only one main rendering thread.

The main thread is responsible for many tasks, such as rendering pages, executing JS, etc.

If a synchronization method is adopted, it is very likely that the main thread will be blocked, resulting in the inability to execute many tasks in the message queue, wasting a lot of time, and even causing page freezes (unable to refresh) and crashes.

Therefore, the browser uses an asynchronous method to avoid blocking problems. When certain tasks that need to be waited for occur, such as timers, networks, and event monitoring, the main thread hands the tasks to other threads for processing, and immediately ends the current task, enters the next loop, and obtains and executes the next message from the message queue. Task. When other threads complete the task, they package the callback function passed in advance into a task (the task is an object, the callback function cannot be directly added to the message queue) and add it to the end of the message queue, waiting for the main thread to execute.

In this way, the smooth operation of a single thread is guaranteed to the greatest extent.

Why does JS hinder rendering

Suppose you have a page with the following main content:

/**
 * html:
 * <h1>hello</h1>
 * <button>click</button>
 */

const h1 = document.querySelector('h1')
const btn = document.querySelector('button')

const delay = (duration) => {<!-- -->
    const start = Date.now()
    while(Date.now() - start < duration) {<!-- -->}
}

btn.onclick = () => {<!-- -->
    h1.textContent = 'hello world'
    delay(3000)
}

After opening this page, let’s analyze it from the perspective of “event loop”:

  • First, two element instances were obtained through docuemnt.querySelector
  • Set up a delay function
  • An event is bound to the button: the main rendering thread hands over the monitoring work to the interactive thread for execution, and the interactive thread waits for the button to be clicked.

After the page is loaded, when we click the button, we will find a magical phenomenon: the text content in the title does not change directly from hello to helloworld, but waits for three seconds before changing. why is that?

  • When the button is clicked, the interaction thread monitors it and immediately packages the callback of the click event into a task and adds it to the message queue.
  • The main thread executes the click callback task added by the interactive thread, starts execution, and executes to h1.textContent = 'hello world'
  • The asynchronously executed code above successfully modifies the text content of the element, but the modification is not synchronized to the page immediately. Instead, after executing this step, a “drawing” task is immediately generated and added to the message queue. Only It can only be seen after the “draw” task is executed and the page is redrawn. (But at this time, the html and the page are unified. It is not that the html content has changed, but there is a problem with the page display)
  • The click callback continues to execute, calling the delay function to delay for three seconds. This delay does not generate a new task, but is executed in the click callback task currently executed by the main thread, so you have to wait for it to finish executing before you can get the next task, which is the third step to generate The “redraw” task

Although this problem cannot be solved well by browsers, some front-end frameworks have made certain optimizations. For example, React will monitor the running time of a period of JS and will not let some useless JS last for too long.

Task priority

Do tasks have priorities? Are there any urgent tasks?

It’s a pity that tasks are not prioritized. All tasks are treated equally, and you have to queue up when you need to.

But message queues have priorities, and there is not only one queue. The latest W3C standard optimizes the architecture of previous macro tasks and micro tasks:

  • Each task has a task type. Tasks of the same type must be in the same queue. Tasks of different types can belong to different queues (for example, network tasks and interactive tasks can both be placed in queue A, but there are new Network tasks or new interactive tasks must be placed in queue A, not queue B. Pay attention to the distinction between “a queue can only hold the same kind of tasks”. This is a wrong understanding), in a time loop , tasks can be taken out from different queues according to the actual situation (this depends on the different implementations and strategies of different browsing)

  • The browser must prepare a microqueue, and the tasks in the microqueue have the highest priority

  • No longer only use macro queues and micro queues. Two queues cannot cope with the complexity of current browsers.

  • The current implementation of chrome includes at least the following queues:

    • Delay queue: used to store callback tasks after the timer arrives, priority: medium
    • Interaction queue: used to store event processing tasks generated after user operations, priority: high
    • Microqueue: Users store tasks that need to be executed fastest, priority: highest

    The main ways to add tasks to microqueues: Promise, MutationObserver

    For example:

    // Immediately add a function to the microqueue
    Promise.resolve().then(() => {<!-- -->
     console.log(1)
    })
    

    A small question, what is the output order:

    const a = () => {<!-- -->
    console.log(1)
    Promise.resolve().then(() => {<!-- -->
    console.log(2)
    })
    }
    
    setTimeout (() => {<!-- -->
    console.log(3)
    Promise.resolve().then(a)
    }, 0)
    
    Promise.resolve().then(() => {<!-- -->
    console.log(4)
    })
    
    console.log(5)
    
    • There are still many queues in the browser, but they are less related to development, so I won’t talk about them here.

3. Summary

Mainly summarizes two parts

JS event loop

The event loop, also called the message loop, is the way the browser renders the main thread.

In Chrome’s source code, the main thread starts an infinite loop for(;;). Each loop will take out the first task from the message queue and execute it, and other threads do not need to communicate with the main thread. , just add the task to the message queue to let the main thread execute the corresponding JS.

In the past, message queues were simply divided into macro queues and micro queues, but now they are no longer able to meet the complex browser environment. Today’s message queues have more classifications.

Can the JS timer accurately measure time

cannot:

  • Calculator hardware limitations
  • The operating system itself has time deviations, and the JS timer is essentially a time system that calls the operating system
  • According to W3C standards, the timer implemented by the browser will have a minimum time of 4 milliseconds if the nesting level exceeds 5 milliseconds, resulting in a certain deviation when the timing time is less than 4 milliseconds.
  • The event loop determines that the timer callback can only run when the main thread is idle, and cannot directly interrupt the task currently running by the main thread.