ReactSchedule time slicing, and compare the real impact on the browser js execution thread and GUI thread.

ReactSchedule time slicing, and compare the real impact on the browser js execution thread and GUI thread.

1. the browser

First of all, we know that the browser is multi-process, there is a master process, each tab will open a single process. If there are multiple blank tab pages, they may be merged into one process management. The browser process can be divided into the following types:

  • Browser process: the main process (master control) of the browser, there is only one
  • GPU process: graphics processor, used for 3D drawing, computer hardware configuration, with the help of some 3D graphics processing.
  • Browser rendering process (kernel): By default, there is one process for each Tab page, which does not affect each other, controls page rendering, script execution, event processing, etc., GUI processing
  • Third-party plug-in process: each type of plug-in corresponds to a process, which is created only when the plug-in is used

And our article will focus on my understanding of the browser rendering process and the impact of our code efficiency on browser rendering.

2. the browser rendering process**

It has the following types of sub-threads:

  • GUI thread: The main process is to parse the dom tree, parse the css tree, combine the two trees to generate a rendering tree, and then layout and paint.

  • JS engine thread: that is, we often say that js is single-threaded, and too heavy js calculation may cause the browser to freeze. (When will I get stuck under the analysis of this article)

  • Event trigger thread: event

  • Timer thread: timer

  • Network request thread: URL resolution, DNS, handshake, TCP/IP, http message, post header and body entities, etc.

  • ...

3. js engine thread

1. let's simulate the main thread of js, the performance of each frame in the browser rendering process. Let's first render a small red slider and let it move left and right on the screen by changing its left value.//old.html

const mainEl = document .querySelector( "#box" ) let temp function mainWork () { const style = mainEl.getBoundingClientRect() if (style.left <= 0 ) { temp = 2 } else if (style.left >= 500 ) { temp = -2 } mainEl.style.left = style.left + temp + 'px' //requestAnimationFrame(mainWork) } // Smooth version //function simulateMainThread () { //mainWork() //} //Non-silky version jump block function simulateMainThread () { setInterval ( () => { mainWork() }, 16 ) } window .simulateMainThread = simulateMainThread //Execute in the script tag //The browser renders the small red block to move as the main process to observe whether the main process is stuck simulateMainThread() Copy code

At this point we can see that a small slider moves a distance of 2px to the left every 50 milliseconds. In other words, the browser will execute js every 50 milliseconds to change the left value of the slider, and make it move by redrawing. The reason why there is a feeling of frame skipping is that the time interval we set is too long, which causes the animation to be inconsistent. We all know that the Chrome browser uses 60 frames per second, that is, redrawing once in 16ms. Then we try to change the interval to 16ms to see how the effect is: At this time, we can see that the slider scrolls more smoothly , But you can still see that the slider occasionally jumps a little. why?

As we can see in the figure, sometimes the timeFired of the browser has been overlapped with painting, and sometimes it is separated. This is because although we adjusted the time interval to the browser s 16ms, the more the timer is executed, the more inaccurate the trigger timing. About 16ms, the timer will automatically execute the next simulateMainThread method. At this time, the GUI thread may be in progress. Painting or layout is embarrassing at this time, because we know that the GUI thread and the js engine thread are mutually exclusive, and the GUI will happen after each frame of js is executed. This is what we see

Semi silky state
The main reason.

What to do at this time, let's add a necessary knowledge point to understand this article.

This leads to a requestAnimationFrame for each frame of the browser s animation. This is not the focus of this article. Let s talk a little bit, we will use this api for the subsequent content. This api will receive a callback function, which will be called every frame of the browser s idle time, which means that our small slider js animation can be put into this api to be more silky. It is worth mentioning that in the requestAnimationFrame callback function, if requestAnimationFrame is called again, it will automatically be executed in the next frame. So rewrite the simulation main thread code:

function mainWork () { const style = mainEl.getBoundingClientRect() if (style.left <= 0 ) { temp = 1 } else if (style.left >= 500 ) { temp = -1 } mainEl.style.left = style.left + temp + 'px' requestAnimationFrame(mainWork) } //Smooth version function simulateMainThread () { mainWork() } Copy code

Take a look at the effect (because the gif effect is not good, I will post all the code at the end, you can try it yourself):

ok, at this point, our small slider on the main thread can slide smoothly on every frame of the browser. At this time, if our browser suddenly ushered in the time-consuming and long js code, what would happen What?

4. block the browser js thread.

We insert a piece of IO code in the script tag,

<script type = 'text/javascript' > //The browser renders the small red block to move as the main process to observe whether the main process is stuck simulateMainThread() const obj = { tagName : 'div' , innerHTML : 'hello worldhello worldhello worldhello worldhello worldhello world' } const arr =[] let num = 1 while (num < 50000 ) { arr.push({...obj}) num++ if (num === 4999 ) { console .log(num) } } setInterval ( () => { arr.forEach( item => { const el = document .createElement(item.tagName) el.innerHTML = item.innerHTML el.style.textAlign = "right" document .body.appendChild(el) }) }, 2000 ) </script> Copy code

We declare 50000 virtual dom arrays, and then start the timer. After the main thread executes every 2 seconds, start the tree-up operation of virtual dom --> real dom. Let s take a look at what happens and we can clearly see that the slider animation is stuck after 2 seconds, and there is no way to continue scrolling. After a while, it continues to scroll. After less than half a second, It stopped again. This can also be explained as because js is single-threaded, the main thread is blocked by our other js scripts, not only that, even the timer interval is not accurate, because the timer does not matter whether your main thread is blocked or not, anyway I'm just 2 seconds, execute the tree climbing.

We can see that every frame from 1000ms to 3500ms is almost completely occupied by js threads, and only 8us is needed to parse the dom.

From 3500ms to 5500ms, the time is all painting, and there is no time to execute the main thread. That is, this way of traversing the dom in batches basically brings very serious problems.

V. Introduction to ReactSchedule

Schedule is the meaning of scheduling. The main function is time slicing, returning the main thread to the browser at regular intervals to avoid occupying the main thread for a long time.

  • The core idea:

    • The React component status is updated, and a task is stored in the Scheduler, which is the React update algorithm.
    • The Scheduler schedules the task and executes the React update algorithm.
    • After React updates a Fiber during the reconciliation phase, it will ask the Scheduler if it needs to be suspended.
    • If you do not need to pause, repeat the previous step and continue to update the next Fiber.
    • If the Scheduler indicates that it needs to be suspended, React will return a function that tells the Scheduler that the task has not been completed. The Scheduler will schedule the task at some point in the future.
  • The schedule is implemented using the browser's native class, MessageChannel. The principle is: we bind the message event to port1 of channnel, we call postMessage in another port2, when port1 receives the message, it will start the next macro task to execute performWorkUntilDeadline, in order to pass the browser s eventLoop to The browser js engine thread gives up the thread.

const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = performWorkUntilDeadline Copy code

Supplementing this foundation, let's look at the code implementation

  1. First of all, we need to clarify a few concepts. 1. we must declare the yieldInterval. This stop interval is the time we leave for react to play in each frame. currentTime + yieldInterval is the deadline.
//Define the browser frame function forceFrameRate(fps) { if (fps <0 || fps> 125) { return console.error("Supports 0-125 frames, if you exceed it, it's nonsense, it's too good to support, React has no time to work") } if (fps > 0) { yieldInterval = Math.floor(1000/fps); } else { //reset the framerate yieldInterval = 6 } }
  1. getCurrentTime workLoop
let getCurrentTime const hasPerformanceNow = typeof performance === 'object' && typeof performance.now === 'function' // getCurrentTime if (hasPerformanceNow) { const localPerformance = performance getCurrentTime = () => localPerformance.now() } else { const localDate = Date const initialTime = localDate.now() getCurrentTime = () => localDate.now() - initialTime }
  1. workLoop --> getCurrentTime yieldInterval react deadline workLoop scheduledHostCallback scheduledHostCallback flushWork workLoop

workLoop currentTime >= deadline workLoop finally postMessage js

function performWorkUntilDeadline () { if (workLoop !== null) { const currentTime = getCurrentTime(); // deadline = currentTime + yieldInterval // const hasTimeRemaining = true // // try workLoop // deadline // let hasMoreWork = true try { // scheduledHostCallback workLoop ( ) //hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime) hasMoreWork = workLoop(hasTimeRemaining, currentTime) } finally { if (hasMoreWork) { console.log() port.postMessage(null) } } } }
  1. workLoop ( ) break workLoop finally reactFiber
// function workLoop (hasTimeRemaining, initialTime) { // //while (currentTask !==null) { while (taskQueue.length > 0) { // if (!hasTimeRemaining || shouldYieldToHost()) { break } // const task = pop(taskQueue) // simulateWork(task) } if (taskQueue.length > 0) {// return true } else { return false } }

ok ReactSchedule 50000 dom

react 15 react 16

// simulateMainThread() setInterval(() => { // react performWorkUntilDeadline() }, 2000)

js requestAniamtionFrame Schedule setTimeout 100ms mainThread

css

js js css3 GUI

<!-- <script src='./js/mainThread.js'></script> --> <script type='text/javascript'> // //simulateMainThread() css : animation: mymove 5s linear infinite normal; @keyframes mymove { 0% {left:0px;} 50% {left:400px;} 100% {left:0px;} }

performance Timer js GUI css painting layout

react ( )

  • react 15

  • react 16

dom css3 js GUI css3 js js css3

  • react15

js GUI

  • react16

dom js GUI

Schedule MessageChannel setTimeout(()=>{}, 0) requestAnimationFrame

  • setTimeout
let count = 0 const start = new Date().getTime() function run () { setTimeout ( () => { console .log( 'setTimeou executed the first' , count++, "time" , new Date ().getTime()-start) if (count === 50 ) { return } run() }, 0 ) } run() Copy code

Needless to say

  • requestAnimationFrame

Reason 1: This api only triggers the callback when the browser updates the page. If the browser does not trigger the update for 100ms, this 100ms is wasted and time is uncontrollable. MessageChannel can be triggered actively. Reason 2: In addition, if requestAnimationFrame is called again in the func triggered by non-requestAnimationFrame, it will be executed immediately and will not be placed in the next frame of the browser. This will be executed twice when performing the first performWorkUntilDeadline.

Finally finished, and finally attached my code implementation: portal