4

0x1: Preface

Every front-end engineer with a [senior senior] title must have his own unique insights into the overall performance optimization of the project. This is one of the capabilities that must be possessed in the direction of the front-end business architecture.

This article will introduce you to one of the performance optimization methods: Time Slicing

According to W3C Performance Team , tasks longer than 50ms are long tasks.

Serial numberTime Distributiondescribe
10 to 16 msUsers are exceptionally good at tracking motion, and they dislike it when animations aren't smooth. They perceive animations as smooth so long as 60 new frames are rendered every second. That's 16 ms per frame, including the time it takes for the browser to paint the new frame to the screen, leaving an app about 10 ms to produce a frame.
20 to 100 msRespond to user actions within this time window and users feel like the result is immediate. Any longer, and the connection between action and reaction is broken.
3100 to 1000 msWithin this window, things feel part of a natural and continuous progression of tasks. For most users on the web, loading pages or changing views represents a task.
41000 ms or moreBeyond 1000 milliseconds (1 second), users lose focus on the task they are performing.
510000 ms or moreBeyond 10000 milliseconds (10 seconds), users are frustrated and are likely to abandon tasks. They may or may not come back later.

The content of the table is excerpted from Using RAIL model to evaluate performance

According to the above table description, we can know that when the delay exceeds 100ms, the user will notice a slight delay. So in order to solve this problem, each task cannot exceed 50ms.

In order to avoid delays when more than 100ms, the user will perceive a slight delay in this case, we can use two options, one is Web Worker , the other is time slice (Time Slicing) .

0x2:web worker

test Demo code here

As we all know, the JavaScript language uses a single-threaded model, that is, all tasks can only be completed on one thread, and only one thing can be done at a time. The previous tasks have not been completed, and the following tasks can only be waited for.

In terms of our business, once we perform too many long tasks, the execution process is easily blocked, and the phenomenon of page suspended animation appears. Although we can place tasks in the task queue and execute them asynchronously, this does not change the nature of JS.

So in order to change this situation, whatwg launched Web Workers .

Regarding web worker, you don’t need to go deep, students who want to know can check MDN-web worker

  1. Web Workers provide a simple way for web content to run scripts in a background thread.
  2. Threads can perform tasks without interfering with the user interface.

<!---->

  1. You can use XMLHttpRequest perform I/O (although the responseXML and channel attributes are always empty). Once created, a worker can send a message to the JavaScript code that created it, by posting the message to the event handler specified by the code (and vice versa).

Once the Worker thread is successfully created, it will always run and will not be interrupted by activities on the main thread (such as user clicking buttons and submitting forms). This is helpful for responding to the communication of the main thread at any time. However, this also causes the Worker to consume more resources and should not be overused, and once it is used up, it should be closed.

Web Worker has the following points of attention.

  1. homology restriction: assigns to the Worker thread to run must be the same as the script file of the main thread.
  2. DOM restriction: Worker thread is located is different from the main thread. It cannot read the DOM objects of the webpage where the main thread is located, and cannot use objects such as document , window , parent . However, Worker threads can be navigator objects and location objects.

<!---->

  1. communication contact: Worker thread and main thread are not in the same context, they cannot communicate directly, and must be done through messages.
  2. script limit: Worker thread cannot execute alert() method and confirm() method, but can issue AJAX request with XMLHttpRequest

<!---->

  1. file limit: Worker thread cannot read local files, that is, it cannot open the local file system ( file:// ), and the script it loads must come from the network.

We can look at the optimization effect after Web Worker

worker.js

self.onmessage = function () {
  const start = performance.now()
  while (performance.now() - start < 1000) {}
  postMessage('done!')
}

myWorker.js

const myWorker = new Worker('./worker.js')
setTimeout(_ => {
  myWorker.postMessage({})
  myWorker.onmessage = function (ev) {
    console.log(ev.data)
  }
}, 5000)

test Demo code here , Interested friends can down to learn .

0x3: What is time slice

The core idea of the time slice is: when a group of tasks within a channel execution, if the current task can not be executed within 50 milliseconds, so in order not to block the main thread, this task should allow control of the main thread , to make browsing The processor can handle other tasks. Giving up control means stopping the execution of the current task, allowing the browser to perform other tasks, and then coming back to continue executing tasks that have not been completed.

Therefore, the purpose of time slicing is not to block the main thread, and the technical means to achieve the goal is to split a long task into many small tasks of no more than 50ms and distribute them in the macro task queue for execution.

In the figure above, you can see that there is a long task in the main thread, which will block the main thread. After using time slice to cut it into many small tasks, as shown in the figure below.

You can see that the main thread now has many densely packed small tasks. We will enlarge it as shown in the figure below.

It can be seen that there is a gap between each small task, which means that after the task is executed for a short period of time, it will give up control of the main thread and let the browser perform other tasks.

The disadvantage of using time slicing is that the total running time of the task becomes longer. This is because the main thread will be idle after each small task is processed, and there is a small delay before the next small task starts processing.

But in order to avoid stuck the browser, this trade-off is necessary.

0x4: How to practice time slicing

Time slicing makes full use of "asynchronous". In the early days, it can be implemented using timers. We call it "manual slicing", for example:

btn.onclick = function () {
  someThing(); // 执行了50毫秒
  setTimeout(function () {
    otherThing(); // 执行了50毫秒
  });
};

When the button is clicked in the above code, the task that should be executed for 100 milliseconds is now split into two tasks of 50 milliseconds.

In practical applications, we can carry out some encapsulation, and the use effect after encapsulation is similar to the following:

btn.onclick = timeSlicing([someThing, otherThing], function () {
  console.log('done~');
});

Of course, timeSlicing not the focus of this article. What I want to explain here is that the timer can be used in the early stage to realize the "time slice" of manual mode

If the granularity of the slice is not large, it is actually acceptable to modify the function manually, but if you need to cut into a logic with a very small granularity, it will be more convenient to generator

ES6 brings the concept of iterators and provides a generator function to generate iterator objects. Although the most orthodox usage of the generator function is to generate iterator objects, we might as well use its features to do some other things.

The Generator function provides the yield keyword, which allows the function to suspend execution. Then let the function continue to execute through the next

Using this feature, we can design more convenient time slices, such as:

btn.onclick = timeSlicing(function* () {
  someThing(); // 执行了50毫秒
  yield;
  otherThing(); // 执行了50毫秒
});

As you can see, we only need to use yield split the 100 millisecond task into two 50 millisecond tasks.

We can even put the yield keyword in the loop:

btn.onclick = timeSlicing(function* () {
  while (true) {
    someThing(); // 执行了50毫秒
    yield;
  }
});

We wrote an infinite loop in the above code, but it still won't block the main thread, and the browser won't get stuck.

Now we formally use Generator to start encapsulating a time slice executor. Use generator properties are placed in the yield every requestIdleCallback in execution until all is finished, you can easily achieve the effect of a time slice.

//首先我们封装一个时间切片执行器
function timeSlicing(gen) {
 if (typeof gen !== "function")
 throw new Error("TypeError: the param expect a generator function");
 var g = gen();
 if (!g || typeof g.next !== "function")
 return;
 return function next() {
 var start = performance.now();
 var res = null;
 do {
 res = g.next();
 } while (res.done !== true && performance.now() - start < 25);
 if (res.done)
 return;
 window.requestIdleCallback(next);
 };
}
//然后把长任务变成generator函数,交由时间切片执行器来控制执行
const add = function(i){
 let item = document.createElement("li");
 item.innerText = 第${i++}条;
 listDom.appendChild(item);
 }
function* gen(){
 let i=0;
 while(i<100000){
 yield add(i);
 i++
 }
}
//使用时间切片来插入10W条数据
function bigInsert(){
 timeSlice(gen)()
}

0x5: Time slice to realize Fibonacci sequence

Every time you learn a new programming language, you will be asked to re-implement the Fibonacci sequence algorithm yourself. At that time, the commonly used methods were recursion and recursion. At that time, I was only interested in the results. As long as the results came out, the others seemed to be indifferent.

After understanding the generator method, you can start to try to use the generator method to slice long task execution.

First introduce the Fibonacci sequence 0, 1, 1, 2, 3, 5, 8,... The value of each term is obtained by adding the first two items.

recursive method:

First, implement the previous recursive method again.

const fibonacci = (n) => {
    if(n === 0 || n === 1)
        return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

// 调用
console.log(fibonacci(40))

The idea of recursion is very simple, that is, keep calling one's own method until n is 1 or 0, and then start to return data layer by layer.

When using recursion to calculate large numbers, the performance will be particularly low for the following two reasons:

  1. In the recursive process, each time a new function is created, the interpreter creates a new function stack frame and presses it on the stack frame of the current function, which forms a call stack. Therefore, when the number of recursion levels is too large, it may cause the call stack to occupy too much memory or overflow.
  2. Analysis can find that recursion has caused a lot of repeated calculations.

generator generator:

Generator is a new feature of ES2015. Thanks to this feature, we can use the generator method to make a Fibonacci sequence generator.

function *fibonacci(n, current = 0, next = 1) {
    if (n === 0) {
        return current;
    }
    yield current;
    yield *fibonacci(n-1, next, current + next);
}

// 调用
const [...data] = fibonacci(num)
console.log(data);

Test Demo code here

0x6: Summary

Time slicing is not a high-level API, but an optimization solution derived from the rendering characteristics of the browser. It is an optimization idea that cuts the logic that is too computationally expensive and easily blocks the rendering into small tasks for execution. The time left for the browser to render is visible to the naked eye. Essentially, it does not optimize the computing performance of js. Therefore, the logic of some algorithms still needs to be optimized from the idea of the algorithm.

Text/Davis

Pay attention to the material technology, be the most fashionable technical person!


得物技术
846 声望1.5k 粉丝