本篇是基于 FDCon2019 上《让你的网页更丝滑by刘博文》的复盘文。该课题也是博主感兴趣的领域, 后续会结合 React 的 Schedule 与该文进行进一步整合, 我的博客css
当前市面上的设备频率在 60 HZ 以上。html
跑以下界面 code.h5jun.com/pojobreact
结合以下代码块, 能够看到 100ms 如下的点击是顺畅的, 而超过 100ms 的点击就会有卡顿现象。git
var observer = new PerformanceObserver(function(list) {
var perfEntries = list.getEntries()
console.log(perfEntries)
});
observer.observe({entryTypes: ["longtask"]});
复制代码
衡量一个网页/App 是否流畅有个比较好用的 Rail 模型, 它大概有如下几个评判标准值。github
Response —— 100ms
Animation —— 16.7ms
Idle —— 50ms
Load —— 1000ms
复制代码
像素管道通常由 5 个部分组成。JavaScript、样式、布局、绘制、合成。以下图所示:web
渲染性能浏览器
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
的运行, 以下是两种解决方案。bash
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
代码以下:app
const workerCode = () => {
self.onmessage = function() {
const start = performance.now()
while (performance.now() - start < 1000) { }
postMessage('done!')
}
}
复制代码
此时在输入框输入时没有卡顿的感受。
下面是另一种使页面流畅的方法 —— 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
则跳出循环执行 ④、⑤);针对 long task
会阻塞 main thread
的运行的情形, 给出两种解决方案:
Web Worker
: 使用 Web Worker
提供的多线程环境来处理 long task
;Time Slicing
: 将主线程上的 long task
进行时间分片;保证 16.7ms
有新的一帧传输到界面上。除去用户的逻辑代码, 一帧内留给浏览器整合的时间大概只有 6ms
左右, 回到像素管道上来, 咱们能够从这几方面进行优化:
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);
}
复制代码