5

如何使页面交互更流畅

 3 years ago
source link: https://zhuanlan.zhihu.com/p/67013616
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何使页面交互更流畅

拼多多 前端工程师


v2-fae5bd3eaff94ae6fa3d1b818ab4de8d_720w.jpg

本篇是对 FDCon2019 上《让你的网页更丝滑》课题的复盘文。该课题也是博主感兴趣的领域, 后续对该文的细节进行进一步补充。

  • 被动交互: animation
  • 主动交互: 鼠标、键盘

当前市面上频率在 60 HZ 以上。

跑如下界面 https://code.h5jun.com/pojob

结合如下代码块, 可以看到 100ms 以下的点击是顺畅的, 而超过 100ms 的点击就会有卡顿现象。

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

让用户感觉到流畅

衡量一个网页/App 是否流畅有个比较好用的 Rail 模型, 它大概有以下几个评判标准值。

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

像素管道一般由 5 个部分组成。JavaScript、样式、布局、绘制、合成。如下图所示:

渲染性能

保证主动交互让用户感觉流畅

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

一般超过 50 ms 认为是 long task(长任务), long task 会阻塞 main thread 的运行, 如下是两种解决方案。

Web Worker

app.js 代码如下:

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" />
  );
}

worker.js 代码如下:

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

此时在输入框输入时没有卡顿的感觉。

Time Slicing

下面是另外一种使页面流畅的方法 —— Time Slicing(时间分片)。

观察 Chrome 的 Performance, 火焰图如下,

从火焰图可以看出主线程被拆分为了多个时间分片, 所以不会造成卡顿。时间分片的代码片段如下所示:

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) // ③
  })()
}

// 调用时间分片函数
timeSlicing(function* () {
  const start = performance.now()
  while (performance.now() - start < 1000) {
    console.log('执行逻辑')
    yield // ②
  }
  console.log('done') // ④
})

该函数虽然代码量不长, 但却不易理解。前置知识 Generator

下面对该函数进行分析:

  1. 往时间分片函数 timeSlicing 中传入 generator 函数;
  2. 函数的执行顺序 —— ①、②、③、① (此时有个竞赛的关系, 如果 performance.now() - start < 1000 则继续 ②、③, 如果 performance.now() - start >= 1000 则跳出循环执行 ④、⑤);

conclusion

针对 long task 会阻塞 main thread 的运行的情形, 给出两种解决方案:

  • Web Worker: 使用 Web Worker 提供的多线程环境来处理 long task;
  • Time Slicing: 将主线程上的 long task 进行时间分片;

保证被动交互让用户感觉流畅

保证 16.7ms 有新的一帧传输到界面上。除去用户的逻辑代码, 一帧内留给浏览器整合的时间大概只有 6ms 左右, 回到像素管道上来, 我们可以从这几方面进行优化:

避免 CSS 选择器嵌套过深

Style 这部分的优化在 css 样式选择器的使用, css 选择器使用的层级越多, 耗费的时间越多。以下是测试 css 选择器不同层级筛选相同元素的一次测试结果。

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

避免布局重排

// 先修改值
el.style.witdh = '100px'
// 后取值
const width = el.offsetWidth

这段代码有什么问题呢?

可以看到它会造成布局重排。

应对的策略是调整它们的执行顺序,

// 先取值
const width = el.offsetWidth
// 后修改值
el.style.witdh = '100px'

可以看到经过调换顺序后, 后执行的 el.style.width 会新开一个像素管道, 而不会在原先的像素管道进行重排。

此外不要在循环中执行如下的操作,

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

可以在火焰图中看到它发生了重绘的警告,

执行顺序是 ①②①②①②①..., 假若我们在第一个 ① 后面插入一条竖线后 ①|②①②①②①, 其就变成先修改值后取值的情景, 所以也就发生了重绘!

正确的使用姿势应该如下:

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

创建 Layers(图层) 可以避免重绘,

{
  transform: translateZ(0);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK