How to solve the problem that the JS execution time is too long and the page freezes

background

As we all know, JavaScript (JS for short) is a single-threaded language. The browser only assigns one main thread to JS, and executes a task from the task queue each time until the task queue is empty. This will inevitably cause some browsers to freeze. For example, the back-end feedback this time, in the file upload task, JS needs to cut the file and upload it to the server in pieces, but when encountering large files (hundreds of gigabytes) Sometimes, hundreds of thousands of copies need to be fragmented, and then these fragments are gradually uploaded to the server. I guess it is because it takes a little longer for JS to process these file fragments in this process, resulting in low page rendering fps, so the page freezes. So I began to think, what are the ways to solve the problem of page freeze caused by long JS execution time?

Solutions

According to my personal understanding of JS, in the face of this kind of stuck problem, the solution is as follows:

  • Use Web Worker to open up new threads and process complex long tasks through new threads, so as to avoid page freeze caused by main thread being blocked.
  • Use the generator function to divide long tasks into multiple macro tasks, so as not to squeeze them all into the micro task queue and affect page rendering.
  • requestAnimationFrame

long task

Tasks that require JS to execute continuously within a certain period of time.
The following code simulates the situation of JS executing a long process

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    @keyframes move {
      
      
        from {
      
      
            left: 0;
        }

        to {
      
      
            left: 100%;
        }
    }

    .move {
      
      
        position: absolute;
        animation: move 5s linear infinite;
    }
</style>

<body>
    <div class="move">LHX-FLY</div>
</body>
<script>
    function longFun() {
      
      
        let i = 0
        const start = performance.now()
        // 5秒内不停执行 i++ 操作,实现长任务效果
        while (performance.now() - start <= 5000) {
      
      
            i++
        }
        return i
    }
    // 1秒后才执行js长任务
    setTimeout(() => {
      
      
        longFun()
    }, 1000)

</script>

The effect is as follows:
insert image description here
We can see that after I refresh the page, the animation effect is still smooth before 1 second, but after 1 second, because the JS has to process a long task, the page cannot be rendered, and the animation is stuck.

insert image description here
It can also be seen from the performance of the console that the total execution time is more than 5 seconds, and from 1 second onwards, the CPU main thread is basically occupied, that is, the interface stops animation rendering during this time period.

Web Worker

For the long task lag problem above, the solution code of Web Worker is as follows:

// 创建线程函数
function createWorker(f) {
    
    
    var blob = new Blob(['(' + f.toString() + ')()']);
    var url = window.URL.createObjectURL(blob);
    var worker = new Worker(url);
    return worker;
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     createWorker(longFun); // 用新的线程去执行这个长任务
     // longFun()
}, 1000)   

The execution results are as follows: insert image description here
insert image description here
We can see that after refreshing the page, the animation effect of the entire interface is still very smooth. Although the total execution time remains unchanged, it is still more than 5 seconds, but the execution time of the main thread is only 54 ms, and the execution performance of the new thread is attributed to Idle, and the CPU is not always occupied.

generator function

Before starting to use the generator function to solve the problem, we must first understand the execution process of JS, that is, the general process of the event loop, as shown in the figure below:
insert image description here
From the event loop process, we can know that, except for special cases, the rendering of the page will be in the microtask queue After clearing, before executing the macro task. So we can let the function pushed into the main execution stack execute for a certain period of time to go to sleep, and then wake him up in the macro task after rendering, so that rendering or user interaction will not be stuck!
Microtask
Promise
process.nextTick
Object.observe
MutaionObserver
...
Macrotask
including overall code script
setTimeout
setIntervcal
I/O
postMessage
MessageChannel
...

In fact, the generator function is nothing more than dividing long tasks into steps, and there will be a pause between each step (one step is a microtask, when the generator function does not execute next, the next step will not be performed, and the microtask queue will be cleared at this time , the next task is not executed until the next step). Converting the appeal problem into a generator function has the following form:

// 将原来的长任务改成 generator 函数
function* fnc_() {
    
    
    let i = 0
    const start = performance.now()
    while (performance.now() - start <= 5000) {
    
    
        yield i++
    }
    return i
}

// 简易时间分片 
function timeSlice (fnc) {
    
     
    if(fnc.constructor.name !== 'GeneratorFunction') return fnc() 
    
    return async function (...args) {
    
     
        const fnc_ = fnc(...args) 
        let data 
        do {
    
     
            data = fnc_.next()
        } while (!data.done) 
        return data.value 
    } 
}
 // 1秒后才执行js长任务
 setTimeout(() => {
    
    
     const fnc = timeSlice(fnc_)
     const start = performance.now()
     console.log('开始')
     const num = await fnc()
     console.log('结束', `${
      
      (performance.now() - start) / 1000}s`)
     console.log(num)
}, 1000)  

From the following performance results, it can be seen that the appeal code is exactly the same as the original Caton. The appeal code just turns the long task into a generator iterator, and the entire fnc_ function is relative to a macro task, and then uses the timeSlice function to make it Non-stop next to execute the next step (each step is a microtask, but if the next step is not executed immediately, the microtask queue of the current step will be executed and wait for the next instruction) until done is completed, but the total time will be longer than It turns out that the execution takes a long time. insert image description here
insert image description here
But add the following code in the next line:

do {
    
    
    data = fnc_.next()
    // 每执行一步就休眠,注册一个宏任务 setTimeout 来叫醒他
    await new Promise(resolve => setTimeout(resolve))
} while (!data.done)

In fact, the next of the generator is an operation performed after being woken up. await new Promise(resolve => setTimeout(resolve)) generates a setTimeout macro task, which is to let him execute next later (sleep), during this short sleep During this period, the microtask queue can be considered to be cleared, and other tasks will be executed first, and the page will also be rendered. After executing an event loop, let him execute next, so that the continuous execution time will not be so long, but will be executed Divided into execution step by step, it will continue to execute in a loop until the execution of the task queue is completed.
insert image description here
It can be seen that the animation effect is always smooth
insert image description here
, but the time slicing above actually goes to sleep at each step, so the execution efficiency is relatively low, and the time slicing function can be optimized:

// 精准时间分片 
function timeSlice_(fnc, time = 25) {
    
    
    if (fnc.constructor.name !== 'GeneratorFunction') return fnc()
    return function (...args) {
    
    
        const fnc_ = fnc(...args)
        function go() {
    
    
            const start = performance.now()
            let data
            do {
    
    
                data = fnc_.next()
            } while (!data.done && performance.now() - start < time)
            if (data.done) return data.value
            return new Promise((resolve, reject) => {
    
    
                setTimeout(() => {
    
    
                    try {
    
     resolve(go()) } catch (e) {
    
     reject(e) }
                })
            })
        }
        return go()
    }
}    

The performance results after optimization are shown in the figure below (the animation effect is smooth):
insert image description here
We compare the performance of the time-slicing function before and after optimization. It can be seen that the execution of the CPU is divided into sections before optimization, but after optimization, it is divided into sections. Yes, other than that, there isn't much difference.
But let’s compare the print results of the two of them:
before optimization:
insert image description here
after optimization:
insert image description here
from this we can see that the value of i is very different, 1037 before optimization and 6042162 after optimization, both of which are executed in 5 seconds The number of JS executions within the time period is reflected. It is obvious that only 1037 executions were performed before optimization, and 6,042,162 executions can be achieved after optimization. The
insert image description here
number of executions at the same time can be regarded as efficiency, and the efficiency after optimization is 5826 times that before optimization.

Note that
after understanding how the generator function optimizes page rendering through time slicing, we must pay attention to the transformation of the generator for long tasks (this is not like a web worker directly executing a piece of code, it depends on how the individual modifies the original code) , the position of yield is very critical, and it needs to be placed in a time-consuming execution place. For example, the long task in the above example is non-stop i++ within 5 seconds.

requestAnimationFrame

This method is mainly used to solve the JS animation freeze, so the above long task that causes the browser css animation to block is not applicable.

Introduction

requestAnimationFrame is an animation API provided in HTML5, referred to as rAF . In web applications, there are many ways to achieve animation effects. In Javascript , it can be realized by timer setTimeout or setInterval. In css3 , transition and animation can be used. Canvas in html5 can also be achieved. In addition, html5 also provides an API dedicated to requesting animation, that is, requestAnimationFrame , which, as the name implies, requests animation frames .

Related concepts

Screen refresh rate
The refresh rate of the screen can be viewed in the "Advanced Display Settings" of the computer, and it is generally 60Hz. The "visual stay effect" does not feel changes or jitters, and what you see is still a continuous picture. In fact, the intermediate interval is 16.7ms (ie 1000/60).

The page is visible
When the page is minimized or switched to a background tab page, the page is invisible, and the browser will trigger a visibilitychange event, and set the document.hidden property to true; when switching to the display state, the page is visible, and the same Trigger a visibilitychange event, setting the document.hidden property to false.

Animation frame request callback function list
Each Document has an animation frame request callback function list, which can be regarded as a set composed of <handlerId, callback> tuples. Among them, handlerId is an integer that uniquely identifies the position of the tuple in the list; callback is a callback function.

Why use requestAnimationFrame?

However, when some animation effects are realized through JS, for example, with setTimeout, setTimeout is actually an animation effect formed by continuously updating images through a time interval. However, setTimeout may freeze in some models or complex applications, which is often referred to as "frame loss". This is because setTimeout can only set a fixed time interval, and different screens and models will have different resolutions, and the setTimeout task is put into an asynchronous queue, so the actual execution time will be later than the set time. These reasons lead to the stuck phenomenon of setTimeout animation.
Knowing the shortcomings of setTimeout, the emergence of rAF is a matter of course. The execution timing of the rAF callback function is determined by the system, that is to say, the system will actively call the callback function in rAF before each drawing. If the system drawing frequency is 60Hz, the callback function will be executed once every 16.7ms. If the system drawing frequency is 75Hz, then the time interval is 1000/75=13.3ms, which ensures that the callback function can be executed once in the middle of each drawing, and there will be no frame loss or stuttering.
In addition, requestAnimationFrame has the following two advantages:

  • CPU energy saving : when the animation implemented by using setTimeout, when the page is hidden or minimized, setTimeout still executes the animation task in the background. Since the page is invisible or unavailable at this time, it is meaningless to refresh the animation, which is a waste of CPU resources. . The requestAnimationFrame is completely different. When the page processing is not activated, the screen refresh task of the page will also be suspended by the system, so the requestAnimationFrame that follows the system will also stop rendering. When the page is activated, the animation will start from the last time. Continue to execute where it stays, effectively saving CPU overhead.
  • Function throttling : In high-frequency events (resize, scroll, etc.), in order to prevent multiple function executions within a refresh interval, use requestAnimationFrame to ensure that the function is only executed once within each refresh interval, which can ensure smoothness performance, and can better save the overhead of function execution. It is meaningless when the function is executed multiple times within a refresh interval, because the display is refreshed every 16.7ms, and multiple drawing will not be reflected on the screen.

use

The most common simple example on the Internet (rAF is not necessarily used for animation, it is also good to solve some frequently executed JS code):

var progress = 0;
//回调函数
function render() {
    
    
  progress += 1; //修改图像的位置
  if (progress < 100) {
    
    
    //在动画没有结束前,递归渲染
    window.requestAnimationFrame(render);
  }
}
//第一帧渲染
window.requestAnimationFrame(render);

This method is asynchronous, the function passed in is called before the animation is redrawn.

// 传入一个callback函数,即动画函数;
// 返回值handlerId为浏览器定义的、大于0的整数,唯一标识了该回调函数在列表中位置。
handlerId = requestAnimationFrame(callback)

Execution process:
(1) First of all, it is necessary to judge whether the document.hidden property is true, that is, it will be executed only when the page is visible; (
2) The browser clears the previous round of animation functions;
(3) The handlerId value returned by this method will be and animation function callback, use <handlerId , callback> to enter the animation frame request callback function list; (
4) The browser will traverse the animation frame request callback function list, and execute corresponding animation functions in turn according to the value of handlerId.

specific example

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    .move {
    
    
        position: absolute;
    }
</style>

<body>
    <div class="move" id="box">LHX-FLY</div>
</body>
<script>
    var box = document.getElementById('box')
    var flag = false
    var left = 0
    function render() {
    
    
        if (flag) {
    
    
            if (left >= 100) {
    
    
                flag = false
            }
            box.style.left = `${
      
      left++}px`
        } else {
    
    
            if (left <= 0) {
    
    
                flag = true
            }
            box.style.left = `${
      
      left--}px`
        }
        window.requestAnimationFrame(render)
    }
    render()
</ script>    

The code execution effect is as shown in the figure:
insert image description here

In fact, from the example code of the appeal, it is very dangerous to call render in render, the method is recursive, but no judgment is added. Usually, the browser will report an error, but it will not be executed with the window.requestAnimationFrame callback (but it is not recommended Write it like this)
How to cancel the callback in requestAnimationFrame and keep executing it?
Just call the cancelAnimationFrame method:

 var rAFId = null;
 function render() {
    
    
    if (flag) {
    
    
        if (left >= 100) {
    
    
            flag = false
        }
        box.style.left = `${
      
      left++}px`
    } else {
    
    
        if (left <= 0) {
    
    
            flag = true
        }
        box.style.left = `${
      
      left--}px`
    }
    rAFId =  window.requestAnimationFrame(render)
    // 5 秒后取消动画效果
    setTimeout(function () {
    
    
        cancelAnimationFrame(rAFId)
    }, 5000)
}

The effect is as shown in the figure:
insert image description here
we can see that after 5 seconds, the animation effect is indeed cancelled.

Graceful downgrade

Because requestAnimationFrame still has compatibility issues, and different browsers need to carry different prefixes. Therefore, it is necessary to encapsulate requestAnimationFrame through graceful degradation, give priority to the use of advanced features, and then roll back according to the situation of different browsers until only setTimeout can be used.

;(function () {
    
    
	var lastTime = 0
	var vendors = ['ms', 'moz', 'webkit', 'o']
	for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
    
    
		window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
		window.cancelAnimationFrame =
			window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']
	}

	if (!window.requestAnimationFrame)
		window.requestAnimationFrame = function (callback, element) {
    
    
			var currTime = new Date().getTime()
			var timeToCall = Math.max(0, 16 - (currTime - lastTime))
			var id = window.setTimeout(function () {
    
    
				callback(currTime + timeToCall)
			}, timeToCall)
			lastTime = currTime + timeToCall
			return id
		}

	if (!window.cancelAnimationFrame)
		window.cancelAnimationFrame = function (id) {
    
    
			clearTimeout(id)
		}
})()

Reference article:
Time slicing technology (to solve the page freeze caused by long js tasks)
requestAnimationFrame usage

Guess you like

Origin blog.csdn.net/weixin_43589827/article/details/122496049