本篇是基於 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);
}
複製代碼