How to Make Page Interaction Smoother

Keywords: Javascript React

Fluency

This article is based on the replica of "Make Your Website Smoother by Liu Bowen" on FDCon 2019. This topic is also an area of interest to bloggers, and will be further integrated with React's Schedule. Personal blog

  • Passive interaction: animation
  • Active Interaction: Mouse, Keyboard

Passive interaction

At present, the frequency of equipment on the market is over 60 HZ.

Active interaction

Run the following interface https://code.h5jun.com/pojob

Combining the following code blocks, you can see that clicks below 100ms are smooth, and clicks over 100ms will have cartoon phenomenon.

var observer = new PerformanceObserver(function(list) {
  var perfEntries = list.getEntries()
  console.log(perfEntries)
});
observer.observe({entryTypes: ["longtask"]});

Make users feel fluent

There's a good way to measure whether a web page / App is fluent or not. Rail model It has the following criteria.

Response - 100ms
Animation - 16.7ms
Idle - 50ms
Load - 1000ms

Pixel pipeline

Pixel pipes generally consist of five parts. JavaScript, Style, Layout, Drawing, Composition. As shown in the following figure:

Rendering performance

Ensure active interaction to make users feel fluent

function App() {
  useEffect(() => {
    setTimeout(_ => {
      const start = performance.now()
      while (performance.now() - start < 1000) { }
      console.log('done!')
    }, 5000)
  })
  return (
    <input type="text" />
  );
}

Generally over 50 ms is considered a long task. Long task blocks the operation of main thread. Here are two solutions.

Web Worker

The app.js code is as follows:

import React, {useEffect} from 'react'
import WorkerCode from './worker'

function App() {
  useEffect(() => {
    const testWorker = new Worker(WorkerCode)
    setTimeout(() => {
      testWorker.postMessage({})
      testWorker.onmessage = function(ev) {
        console.log(ev.data)
      }
    }, 5000)
  })
  return (
    <input type="text" />
  );
}

The worker.js code is as follows:

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

At this time, there is no sense of Carton when the input box is entered.

Time Slicing

Here's another way to make the page flow smoothly: Time Slicing.

Look at the Performance of Chrome. The flame diagram is as follows.

As can be seen from the flame diagram, the main thread is split into several time slices, so it does not cause a jamming. The code snippet for time slicing is as follows:

function timeSlicing(gen) {
  if (typeof gen === 'function') gen = gen()
  if (!gen || typeof gen.next !== 'function') return

  (function next() {
    const res = gen.next() // ①
    if (res.done) return // ⑤
    setTimeout(next) // ③
  })()
}

// Calling time slicing function
timeSlicing(function* () {
  const start = performance.now()
  while (performance.now() - start < 1000) {
    console.log('Execution logic')
    yield // ②
  }
  console.log('done') // ④
})

Although the code size of this function is not long, it is not easy to understand. Pre knowledge Generator

Following is an analysis of the function:

  1. The generator function is passed into the time slicing function timeSlicing.
  2. Function execution sequence - 1, 2, 3, 1 (there is a competition relationship at this time, if performance. now () - start < 1000 then continue to 2, 3, if performance. now () - start >= 1000 then jump out of the loop execution 4, _);

conclusion

In view of the situation that long task will block the operation of main thread, two solutions are given:

  • Web Worker: Use the multi-threaded environment provided by Web Worker to handle long task s;
  • Time Slicing: Time slicing the long task on the main thread;

Ensure that passive interaction makes users feel fluent

Ensure a new frame of 16.7ms is transmitted to the interface. Apart from the user's logic code, the integration time of browsers in a frame is only about 6 ms. When we return to the pixel pipeline, we can optimize from these aspects:

Avoid too deep nesting of CSS selectors

The optimization of Style is used in the css style selector. The more levels the css selector uses, the more time it takes. The following is a test result of screening the same elements at different levels of the css selector.

div.box:not(:empty):last-of-type span         2.25ms
index.html:85 .box--last span                 0.28ms
index.html:85 .box:nth-last-child(-n+1) span  2.51ms

Avoid layout rearrangement

// Modify value first
el.style.witdh = '100px'
// Post value
const width = el.offsetWidth

What's wrong with this code?

You can see that it causes layout rearrangement.

The strategy is to adjust their execution order.

// Prefix value
const width = el.offsetWidth
// Post modification value
el.style.witdh = '100px'

You can see that the el.style.width executed after the conversion sequence will open a new pixel pipeline instead of rearranging the original pixel pipeline.

In addition, do not perform the following operations in a loop.

for (var i = 0; i < 1000; i++) {
  const newWidth = container.offsetWidth; // ①
  boxes[i].style.width = newWidth + 'px'; // ②
}

You can see in the flame diagram that it has been redrawn.

If we insert a vertical line after the first one, then it will become a situation of modifying the value first and then taking the value, so a redrawing will take place!

The correct posture should be as follows:

const newWidth = container.offsetWidth;
for (var i = 0; i < 1000; i++) {
  boxes[i].style.width = newWidth + 'px';
}

Avoid repainting

Creating Layers avoids redrawing.

{
  transform: translateZ(0);
}

Posted by mustatin on Sat, 25 May 2019 15:43:19 -0700