Last week I mentioned “time slicing” in FDConf’s share “Make Your Web Smoother”, but due to time limitations I didn’t discuss time slicing in more detail. So WHEN I came back, I thought I’d write an article about “time slicing” in detail.

If the process from user input to visual output from the display to the user is longer than 100ms, the user will notice that the web page is slow. To solve this problem, each task should not exceed 50ms. The W3C Performance Working Group defines tasks that exceed 50ms as long tasks in the LongTask specification.

I explained these 50 milliseconds in detail in the FDConf sharing. If you haven’t heard it, don’t worry. I will write a follow-up article about the shared content.

Online PPT address: ppt.baomitu.com/d/b267a4a3

Therefore, in order to avoid long tasks, one solution is to use Web Worker, which puts long tasks in Worker threads and fails to access DOM. Another solution is to use time slicing.

What is time slice

The core idea of time slicing is that if a task cannot be executed within 50 milliseconds, then in order not to block the main thread, the task should cede control of the main thread so that the browser can handle other tasks. Ceding control means stopping the current task, letting the browser do something else, and then coming back to the unfinished task.

Therefore, the purpose of time slice is not to block the main thread, and the technical means to achieve this goal is to divide a long task into many small tasks with no more than 50ms and disperse them in the macro task queue.

In the figure above, you can see that the main thread has a long task that blocks the main thread. Using time slices, it is cut into many small tasks, as shown below.

You can see that the main thread now has a lot of small tasks, which we can zoom in to see below.

You can see that there is a gap between each small task, which means that after the task has been executed for a short period of time, it cedes control of the main thread and lets the browser perform other tasks.

The disadvantage of using time slicing is that the total time it takes for a task to run is longer because the main thread is free after each small task and there is a small delay before the next small task starts processing.

But this trade-off is necessary to avoid blocking the browser.

How do I use time slices

Time slicing is a concept that can also be understood as a technical solution. It is not the name of an API or a tool.

In fact, time slicing took full advantage of “asynchrony” and in the early days could be implemented using timers, for example:

btn.onclick = function () {
  someThing(); // Executed for 50 milliseconds
  setTimeout(function () {
    otherThing(); // Executed for 50 milliseconds
  });
};
Copy the code

In the code above, the task that should have executed for 100 milliseconds when the button was clicked is now split into two 50-millisecond tasks.

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

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

Of course, the design of the TS API is not the focus of this article. It is intended to show that timers can be used to achieve “time slicing” in the early days.

ES6 introduced the concept of iterators and provided Generator functions to generate iterator objects. Although the most orthodox use of Generator functions is to generate iterator objects, there are other things we can do with its features.

Generator functions provide the yield keyword, which allows the function to suspend execution. We then let the function continue through the next method of the iterator object.

For those unfamiliar with Generator functions, learn how to use them first.

Using this feature, we can design time slices that are more convenient to use, for example:

btn.onclick = ts(function* () {
  someThing(); // Executed for 50 milliseconds
  yield;
  otherThing(); // Executed for 50 milliseconds
});
Copy the code

As you can see, we can split a 100-millisecond task into two 50-millisecond tasks using only the yield keyword.

We can even put the yield keyword in the loop:

btn.onclick = ts(function* () {
  while (true) {
    someThing(); // Executed for 50 milliseconds
    yield; }});Copy the code

We’ve written an infinite loop, but it still doesn’t block the main thread and the browser doesn’t freeze.

Ts implementation principle based on generator

From the previous examples, we saw that generator-based time slicing is very useful, but the implementation principle of ts function is very simple, the simplest TS function requires only nine lines of code.

function ts (gen) {
  if (typeof gen === 'function') gen = gen()
  if(! gen ||typeofgen.next ! = ='function') return
  return function next() {
    const res = gen.next()
    if (res.done) return
    setTimeout(next)
  }
}
Copy the code

There are only nine lines of code in all, with only three or four lines of key code, but these lines take full advantage of the event loop mechanism and the features of the Generator functions.

I’m happy to have created this code.

The main idea is that you can use the yield keyword to suspend the execution of a task, thus ceding control to the main thread. The timer can be used to re-place the “unfinished task” in the task queue to continue execution.

Avoid breaking tasks into small pieces

Using yield to cut tasks is convenient, but inefficient if the cut is very fine-grained. Let’s say we have a task that runs 100ms, it’s best to split it into two tasks that run 50ms, rather than 100 tasks that run 1ms. Assuming that the interval between the tasks to be cut is 4ms, the total execution time of 100 tasks to be cut into 1ms is:

(1 + 4) * 100 = 500ms
Copy the code

If it is cut into two 50ms tasks, the total execution time is:

(50 + 4) * 2 = 108ms
Copy the code

As you can see, the total execution time below is 4.6 times less than the previous one without affecting the user experience.

To ensure that the cut task is just close to 50ms, you can evaluate it yourself when the user uses yield, or you can determine whether multiple tasks should be executed at once based on the task execution time in the ts function.

Let’s improve the ts function slightly:

function ts (gen) {
  if (typeof gen === 'function') gen = gen()
  if(! gen ||typeofgen.next ! = ='function') return
  return function next() {
    const start = performance.now()
    let res = null
    do {
      res = gen.next()
    } while(! res.done && performance.now() - start <25);

    if (res.done) return
    setTimeout(next)
  }
}
Copy the code

Now let’s test:

ts(function* () {
  const start = performance.now()
  while (performance.now() - start < 1000) {
    console.log(11)
    yield
  }
  console.log('done! ')
})();
Copy the code

This code printed 215 times 11 on my computer in the previous version, and 6300 times 11 in the later version, indicating that more tasks could be performed with the same total time.

Here’s another example:

ts(function* () {
  for (let i = 0; i < 10000; i++) {
    console.log(11)
    yield
  }
  console.log('done! ')
})();
Copy the code

On my computer, this code was cut into 10,000 small tasks with a total execution time of 46 seconds in the previous version, and 52 small tasks with a total execution time of 1.5 seconds in the later version.

conclusion

I have put the time slice code on my Github, if you are interested, you can visit: github.com/berwin/time…