如何使页面交互更流畅
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.
如何使页面交互更流畅
本篇是对 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
下面对该函数进行分析:
- 往时间分片函数
timeSlicing
中传入generator
函数; - 函数的执行顺序 —— ①、②、③、① (此时有个竞赛的关系, 如果
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);
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK