Article directory
Original link
For the browser, there are multiple threads working together, as shown in the figure below. For details, please refer to a frame analysis .
It also refers to the often-called JS single-threaded engine Main Therad
.
Note that every block of the above main thread may not be executed, it depends on the actual situation.
First, the process of Parse HTML
-> Composite
is called the rendering pipeline flow Rendering pipeline
.
There is a non-stop polling mechanism inside the browser to check whether there are tasks in the task queue, and if so, take them out and hand them over JS引擎
to execute.
For example:
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"), 500);
const baz = () => console.log("Third");
bar();
foo();
baz();
process:
Task Queue Tasks Queue
Some common webapi
will generate a task
and send it to the task queue.
script
LabelXHR
,addEventListener
and other event callbackssetTimeout
timer
Each task
execution in a poll has its own context and does not affect each other. That's why, script
the code in the tag crashes and does not affect the subsequent script
code execution.
- The polling pseudocode is as follows (used in the original video
pop
, which isJSer
convenient for the world view ofshift
)
while(true) {
task = taskQueue.shift();
execute(task);
}
- Task queues are not necessarily maintained in one queue, for example
input event
, may besetTimeout
maintained in different queues. If the code operates , the main thread will also execute the rendering pipeline flow. The pseudocode is modified as follows:callback
DOM
while(true) {
+ queue = getNextQueue();
- task = taskQueue.shift();
+ task = queue.shift();
execute(task);
+ if(isRepaintTime()) repaint();
}
- for example
button.addEventListener('click', e => {
while(true);
});
Click button
to generate one task
. When the task is executed, the main thread has been occupied and stuck, and the task cannot be exited, resulting in the inability to respond to user interaction or render dynamic graphics.
Replace and execute the following code
function loop() {
setTimeout(loop, 0);
}
loop();
Executes in a seemingly infinite loop loop
, setTimeout
yielding one when the time expires task
. loop
Exit the main thread after execution . Enables user interaction events and rendering to be performed.
Because of this, execution of setTimeout
and other webapi
spawned task
depends on the order in the task queue.
Even if there are no other tasks in the task queue, it cannot be 0秒
executed . setTimeout
When the timer cb
expires , it will be put into the task queue, and it will be taken out task
by the polling engine for execution, at least approximately 4.7ms
.
requestAnimationFrame
- For example, keep moving a box forward by 1 pixel
function callback() {
moveBoxForwardOnePixel();
requestAnimationFrame(callback);
}
callback()
replace withsetTimeout
function callback() {
moveBoxForwardOnePixel();
- requestAnimationFrame(callback);
+ setTimeout(callback, 0);
}
callback()
In contrast, it can be found that setTimeout
the movement is rAF
significantly faster than the movement (about 3.5 times).
It means that setTimeout
the callback is too frequent, which is not a good thing.
Rendering pipeline flow doesn't necessarily happen between each setTimeout
spawned by task
, but can also happen after multiple setTimeout
callbacks .
It's up to the browser to decide when to render and as efficiently as possible, it will only render if it's worth updating, and not if it doesn't.
If the browser is running in the background and nothing is displayed, the browser won't render because it doesn't make sense. In most cases, the page will be refreshed at a fixed frequency
to ensure that 60FPS
the human eye feels smooth, that is, about one frame 16ms
. If the frequency is high, the human eye cannot see meaningless, and if it is lower than the human eye, it can detect stuttering.
When the main thread is very idle, setTimeout
the callback can be executed every 4ms
about , leaving 2ms
the rendering pipeline flow, which setTimeout
can be executed within one frame 3.5次
.
3.5ms * 4 + 2ms = 16ms
.
setTimeout
There are too many calls 3-4次
, more than the user can see, and more than the browser can display, about 3/4 is wasted.
Many old animation libraries are used setTimeout(animFrame, 1000 / 60)
for optimization.
But setTimeout
it is not born for animation, the execution is unstable, it will cause drift or the heavy task will delay the rendering pipeline flow.
requestAnimationFrame
It's there to fix these issues, keeping everything neat and organized and every frame happening on time.
It is recommended to use requestAnimationFrame
Wrap animation work to improve performance. It solves the problem of setTimeout
uncertainty and performance waste, and it is up to the browser to ensure that it is executed before rendering the pipeline flow.
- A confusing question: Can the following code
0px
move to1000px
to and then to500px
to ?
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
box.style.transform = 'translateX(500px)';
});
Result: 0px
Moved 500px
to . Since the code block of the callback task is executed synchronously, the browser does not care about the intermediate state.
- Modify as follows
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(500px)';
+ });
});
Result: Still 0px
moving 500px
to .
This is addEventListener
because task
the synchronous code in the is modified to 1000px
.
Before the computed style in the rendering pipeline flow is executed, it needs to be executed rAF
, and the final style is 500px
.
- Modified correctly, before the execution of the next frame's rendering pipeline flow
500px
.
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
requestAnimationFrame(() => {
- box.style.transform = 'translateX(500px)';
+ requestAnimationFrame(() => {
+ box.style.transform = 'translateX(500px)';
+ });
});
});
- Not a good way, but it works
button.addEventListener('click', () => {
box.style.transform = 'translateX(1000px)';
box.style.transition = 'transform 1s ease-in-out';
+ getComputedStyle(box).transform;
box.style.transform = 'translateX(500px)';
});
getComputedStyle
It will lead to forced rearrangement, the rendering pipeline flow is executed in advance, and redundant operations will consume performance.
- bad news
Edge
and Safari
of rAF
do not conform to the specification, and are wrongly executed after the rendering pipeline flow.
Microtasks
DOMNodeInserted
Originally designed to listen DOM
for changes to .
- For example, the following code will trigger how many times
DOMNodeInserted
.
document.body.addEventListener('DOMNodeInserted', () => {
console.log('Stuff added to <body>!');
});
for(let i = 0; i < 100; i++) {
const span = document.createElement('span');
document.body.appendChild(span);
span.textContent = 'hello';
}
Ideally after the for loop finishes, DOMNodeInserted
the callback executes once.
Result: executed 200
times . Add span
trigger 100
times, set textContent
trigger 100
.
This DOMNodeInserted
makes a very poor performance burden.
To solve these problems, a new task queue called microtasks was created Microtasks
.
Common Microtasks
- MutationObserver - Observer of DOM mutation events.
- Promise
- process.nextTick (node 中)
The microtask is taken out in an event polling and task
the execution is completed, that is JavaScript
, there is no executable content in the running stack (stack).
The browser then fetches all microtasks
the to execute.
loop
What if you create a like the previous with microtasks ?
function loop() {
Promise.resolve().then(loop);
}
loop();
You will find that it freezes while
just like .
Now we have 3 queues of different nature
- task queue
- rAF tail
- microtask queue
- The task queue is known before, and an
task
execution , and if it is generatednew task
, it is put into the queue.task
After the execution is completed, wait for the next polling to take outnext task
. - After the microtask queue task is executed, execute all tasks in the queue
microtask
. If generatednew microtask
, enter the queue and wait for execution until the queue is emptied.
while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
+ while(microtaskQueue.hasTasks()) {
+ doMicrotask();
+ }
if(isRepaintTime()) repaint();
}
rAF queue
Before the start of each frame rendering pipeline flow, all queues are executed at one timerAF callback
, and if generatednew rAF
, wait for the next frame to be executed.
while(true) {
queue = getNextQueue();
task = queue.shift();
execute(task);
while(microtaskQueue.hasTasks()) {
doMicrotask();
}
- if(isRepaintTime()) repaint();
+ if(isRepaintTime()) {
+ animationTasks = animationQueue.copyTasks();
+ for(task in animationTasks) {
+ doAnimationTask(task);
+ }
+
+ repaint();
+ }
}
- Think, check if you understand
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 1'));
console.log('Listener 1');
});
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 2'));
console.log('Listener 2');
});
What is the sequence of clicking the buttons?
To analyze, the above code block is one task 0
.
task 0
After the execution is complete,webapi
listen to the event.- When the user clicks the button,
click
the event ,task queue
enqueuedtask 1
,task 2
. - Polling fetch
task 1
execution ,Microtask queue
enqueueMicrotask 1
.
console
outputListener 1
.task 1
Finished. - Execute all
microtask
(currently onlyMicrotask 1
), take out the execution, and console outputMicrotask 1
. - Polling fetch
task 2
execution ,Microtask queue
enqueueMicrotask 2
.
console
outputListener 2
.task 2
Finished. - Execute all
microtask
, fetch andMicrotask 2
execute , console outputMicrotask 2
.
Answer: Listener 1
-> Microtask 1
-> -> Listener 2
->Microtask 2
If you got the answer right, congratulations, the answerer 87%
who .
- What if the code triggers it?
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 1'));
console.log('Listener 1');
});
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask 2'));
console.log('Listener 2');
});
+ button.click();
same thinking analysis
task 0
Execute tobutton.click()
Wait for the event callback to complete.- Execute synchronously
Listener 1
andMicrotask queue
enqueueMicrotask 1
.console
outputListener 1
. - Execute synchronously
Listener 2
andMicrotask queue
enqueueMicrotask 2
.console
outputListener 2
. click
functionreturn
, endtask 0
.- Execute all
microtask
, fetch andMicrotask 1
execute , console outputMicrotask 1
. - Take out
Microtask 2
and execute , console outputMicrotask 2
.
Answer: Listener 1
-> Listener 2
-> -> Microtask 1
->Microtask 2
When doing automated testing, you need to be careful, sometimes it will produce results that are not the same as user interaction.
- Finally, some difficult questions
Will the following code prevent a
the link from jumping when the user clicks it?
const nextClick = new Promise(resolve => {
link.addEventListener('click', resolve, {
once: true });
});
nextClick.then(event => {
event.preventDefault();
// handle event
});
What if it's a code click?
link.click();
The answer is not announced yet, welcome to discuss in the comment area.
node
- No script parsing events (eg, parsing script in HTML)
- no user interaction events
- No
rAF
callback
- No rendering pipeline (rendering pipeline)
Node does not need to keep polling whether there are tasks, and it ends when all queues are cleared.
common task queuetask queue
- XHR requests、disk read or write queue(I/O)
- check queue (setImmediate)
- timer queue (setTimeout)
Common Microtasksmicrotask queue
- process.nextTick
- Promise
process.nextTick
Execution takes precedence over Promise
.
while(tasksAreWaiting()) {
queue = getNextQueue();
while(queue.hasTasks()) {
task = queue.shift();
execute(task);
while(nextTickQueue.hasTasks()) {
doNextTickTask();
}
while(promiseQueue.hasTasks()) {
doPromiseTask();
}
}
}
web worker
- No
script tag
- no user interaction
- Can't operate
DOM
similarnode