深度剖析瀏覽器渲染性能原理,你到底知道多少
渲染卡頓是怎么回事?
網(wǎng)頁不僅應(yīng)該被快速加載,同時(shí)還應(yīng)該流暢運(yùn)行,比如快速響應(yīng)的交互,如絲般順滑的動(dòng)畫等。
大多數(shù)設(shè)備的刷新頻率是60次/秒,也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內(nèi)完成,超出這個(gè)時(shí)間,頁面的渲染就會(huì)出現(xiàn)卡頓現(xiàn)象,影響用戶體驗(yàn)。
為了保證頁面的渲染效果,需要充分了解瀏覽器是如何處理HTML/JavaScript/CSS的。
渲染流程分為幾步?

JavaScript:JavaScript實(shí)現(xiàn)動(dòng)畫效果,DOM元素操作等。Style(計(jì)算樣式):確定每個(gè)DOM元素應(yīng)該應(yīng)用什么CSS規(guī)則。Layout(布局):計(jì)算每個(gè)DOM元素在最終屏幕上顯示的大小和位置。由于web頁面的元素布局是相對的,所以其中任意一個(gè)元素的位置發(fā)生變化,都會(huì)聯(lián)動(dòng)的引起其他元素發(fā)生變化,這個(gè)過程叫reflow。Paint(繪制):在多個(gè)層上繪制DOM元素的的文字、顏色、圖像、邊框和陰影等。Composite(渲染層合并):按照合理的順序合并圖層然后顯示到屏幕上。
實(shí)際場景下,大概會(huì)有三種常見的渲染流程(也即是Layout和Paint步驟是可避免的):

結(jié)合渲染流程怎么優(yōu)化渲染性能呢?
結(jié)合上述的渲染流程,我們可以去針對性的分析并優(yōu)化每個(gè)步驟。
優(yōu)化 JavaScript 的執(zhí)行效率 降低樣式計(jì)算的范圍和復(fù)雜度 避免大規(guī)模、復(fù)雜的布局 簡化繪制的復(fù)雜度、減少繪制區(qū)域 優(yōu)先使用渲染層合并屬性、控制層數(shù)量 對用戶輸入事件的處理函數(shù)去抖動(dòng)(移動(dòng)設(shè)備)
優(yōu)化 JavaScript 的執(zhí)行效率,具體可以做什么?
用 requestAnimationFrame 實(shí)現(xiàn)動(dòng)畫
在 JS 中實(shí)現(xiàn)動(dòng)畫應(yīng)該避免使用 setTimeout 或 setInterval,盡量使用 requestAnimationFrame。
setTimeout(callback) 和 setInterval(callback) 無法保證 callback 函數(shù)的執(zhí)行時(shí)機(jī),很可能在幀結(jié)束的時(shí)候執(zhí)行,從而導(dǎo)致丟幀,如下圖:

requestAnimationFrame(callback) 可以保證 callback 函數(shù)在每幀動(dòng)畫開始的時(shí)候執(zhí)行。
// requestAnimationFrame將保證updateScreen函數(shù)在每幀的開始運(yùn)行
requestAnimationFrame(updateScreen);
注意:jQuery 的 animate 函數(shù)就是用 setTimeout 來實(shí)現(xiàn)動(dòng)畫,可以通過jquery-requestAnimationFrame這個(gè)補(bǔ)丁來用requestAnimationFrame替代setTimeout
使用 Web Workers
把耗時(shí)長的 JavaScript 代碼放到 Web Workers 中去做。
JavaScript 代碼運(yùn)行在瀏覽器的主線程上,與此同時(shí),瀏覽器的主線程還負(fù)責(zé)樣式計(jì)算、布局、繪制的工作,如果 JavaScript 代碼運(yùn)行時(shí)間過長,就會(huì)阻塞其他渲染工作,很可能會(huì)導(dǎo)致丟幀。
前面提到每幀的渲染應(yīng)該在 16ms 內(nèi)完成,但在動(dòng)畫過程中,由于已經(jīng)被占用了不少時(shí)間,所以JavaScript 代碼運(yùn)行耗時(shí)應(yīng)該控制在 3-4 毫秒。
如果真的有特別耗時(shí)且不操作 DOM 元素的純計(jì)算工作,可以考慮放到 Web Workers 中執(zhí)行。
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// 主線程不受Web Workers線程干擾
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = e.data;
// Web Workers線程執(zhí)行結(jié)束
// ...
});
批量更新 DOM
把 DOM 元素的更新劃分為多個(gè)小任務(wù),分別在多個(gè) frame 中去完成。
由于 Web Workers 不能操作 DOM 元素的限制,所以只能做一些純計(jì)算的工作,對于很多需要操作 DOM 元素的邏輯,可以考慮分步處理,把任務(wù)分為若干個(gè)小任務(wù),每個(gè)任務(wù)都放到requestAnimationFrame 中回調(diào)執(zhí)行
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var nextTask = taskList.pop();
// 執(zhí)行小任務(wù)
processTask(nextTask);
if (taskList.length > 0) {
requestAnimationFrame(processTaskList);
}
}
分析 JavaScript 的性能
使用 Chrome DevTools 的 Timeline 來分析 JavaScript 的性能
打開 Chrome DevTools > Timeline > JS Profile,錄制一次動(dòng)作,然后分析得到的細(xì)節(jié)信息,從而發(fā)現(xiàn)問題并修復(fù)問題。

降低樣式計(jì)算的范圍和復(fù)雜度,具體可以做什么?
添加或移除一個(gè) DOM 元素、修改元素屬性和樣式類、應(yīng)用動(dòng)畫效果等操作,都會(huì)引起 DOM 結(jié)構(gòu)的改變,從而導(dǎo)致瀏覽器需要重新計(jì)算每個(gè)元素的樣式,對整個(gè)頁面或部分頁面重新布局,這就是所謂的樣式計(jì)算。
樣式計(jì)算主要分為兩步:創(chuàng)建一套匹配的樣式選擇器,為匹配的樣式選擇器計(jì)算具體的樣式規(guī)則
降低樣式選擇器的復(fù)雜度
盡量保持 class 的簡短,或者使用 Web Components 框架。
.box:nth-last-child(-n+1) .title {}
// 改善后
.final-box-title {}
減少需要執(zhí)行樣式計(jì)算的元素個(gè)數(shù)
由于瀏覽器的優(yōu)化,現(xiàn)代瀏覽器的樣式計(jì)算直接對目標(biāo)元素執(zhí)行,而不是對整個(gè)頁面執(zhí)行,所以我們應(yīng)該盡可能減少需要執(zhí)行樣式計(jì)算的元素的個(gè)數(shù)
避免大規(guī)模、復(fù)雜的布局,具體可以做什么?
布局就是計(jì)算 DOM 元素的大小和位置的過程,如果你的頁面中包含很多元素,那么計(jì)算這些元素的位置將耗費(fèi)很長時(shí)間。
布局的主要消耗在于:
需要布局的DOM元素的數(shù)量; 布局過程的復(fù)雜程度
盡可能避免觸發(fā)布局
當(dāng)你修改了元素的屬性之后,瀏覽器將會(huì)檢查為了使這個(gè)修改生效是否需要重新計(jì)算布局以及更新渲染樹,對于DOM元素的“幾何屬性”修改,比如width/height/left/top等,都需要重新計(jì)算布局。
對于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看明細(xì)。

可以查看布局的耗時(shí),以及受影響的DOM元素?cái)?shù)量。
使用flexbox替代老的布局模型
老的布局模型以相對/絕對/浮動(dòng)的方式將元素定位到屏幕上。Floxbox 布局模型用流式布局的方式將元素定位到屏幕上。通過一個(gè)小實(shí)驗(yàn)可以看出兩種布局模型的性能差距,同樣對 1300 個(gè)元素布局,浮動(dòng)布局耗時(shí) 14.3ms,F(xiàn)lexbox 布局耗時(shí) 3.5ms。

避免強(qiáng)制同步布局事件的發(fā)生
前面提過,將一幀畫面渲染的屏幕上的流程是:

首先是 JavaScript 腳本,然后是 Style,然后是 Layout,但是我們可以強(qiáng)制瀏覽器在執(zhí)行JavaScript 腳本之前先執(zhí)行布局過程,這就是所謂的強(qiáng)制同步布局。
requestAnimationFrame(logBoxHeight);
// 先寫后讀,觸發(fā)強(qiáng)制布局
function logBoxHeight() {
// 更新box樣式
box.classList.add('super-big');
// 為了返回box的offersetHeight值
// 瀏覽器必須先應(yīng)用屬性修改,接著執(zhí)行布局過程
console.log(box.offsetHeight);
}
// 先讀后寫,避免強(qiáng)制布局
function logBoxHeight() {
// 獲取box.offsetHeight
console.log(box.offsetHeight);
// 更新box樣式
box.classList.add('super-big');
}
在 JavaScript 腳本運(yùn)行的時(shí)候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你在當(dāng)前幀獲取屬性之前又對元素節(jié)點(diǎn)有改動(dòng),那就會(huì)導(dǎo)致瀏覽器必須先應(yīng)用屬性修改,結(jié)果執(zhí)行布局過程,最后再執(zhí)行 JavaScript 邏輯。
避免連續(xù)的強(qiáng)制同步布局發(fā)生
如果連續(xù)快速的多次觸發(fā)強(qiáng)制同步布局,那么結(jié)果更糟糕。
比如下面的例子,獲取 box 的屬性,設(shè)置到 paragraphs 上,由于每次設(shè)置 paragraphs 都會(huì)觸發(fā)樣式計(jì)算和布局過程,而下一次獲取 box 的屬性必須等到上一步設(shè)置結(jié)束之后才能觸發(fā)。
function resizeWidth() {
// 會(huì)讓瀏覽器陷入'讀寫讀寫'循環(huán)
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
// 改善后方案
var width = box.offsetWidth;
function resizeWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
注意:可以使用FastDOM來確保讀寫操作的安全,從而幫你自動(dòng)完成讀寫操作的批處理,還能避免意外地觸發(fā)強(qiáng)制同步布局或快速連續(xù)布局
簡化繪制的復(fù)雜度、減少繪制區(qū)域,具體可以做什么?
繪制就是填充像素的過程,通常這個(gè)過程是整個(gè)渲染流程中耗時(shí)最長的一環(huán),因此也是最需要避免發(fā)生的一環(huán)。
如果Layout被觸發(fā),那么接下來元素的Paint一定會(huì)被觸發(fā)。當(dāng)然純粹改變元素的非幾何屬性,也可能會(huì)觸發(fā)Paint,比如背景、文字顏色、陰影效果等。
提升移動(dòng)或漸變元素的繪制層
繪制并非總是在內(nèi)存中的單層畫面里完成的,實(shí)際上,瀏覽器在必要時(shí)會(huì)將一幀畫面繪制成多層畫面,然后將這若干層畫面合并成一張圖片顯示到屏幕上。
這種繪制方式的好處是,使用transform來實(shí)現(xiàn)移動(dòng)效果的元素將會(huì)被正常繪制,同時(shí)不會(huì)觸發(fā)其他元素的繪制。
減少繪制區(qū)域
瀏覽器會(huì)把相鄰區(qū)域的渲染任務(wù)合并在一起進(jìn)行,所以需要對動(dòng)畫效果進(jìn)行精密設(shè)計(jì),以保證各自的繪制區(qū)域不會(huì)有太多重疊。
簡化繪制的復(fù)雜度
可以實(shí)現(xiàn)同樣效果的不同方式,我們應(yīng)該采用性能更好的那種。
通過Chrome DevTools來分析繪制復(fù)雜度和時(shí)間消耗,盡可能降低這些指標(biāo)
打開DevTools,按下鍵盤的ESC鍵,在彈出的面板中,選中rendering選項(xiàng)卡下的Enable paint flashing,這樣每當(dāng)頁面發(fā)生繪制的時(shí)候,屏幕就會(huì)閃現(xiàn)綠色的方框。通過該工具可以檢查Paint發(fā)生的區(qū)域和時(shí)機(jī)是不是可以被優(yōu)化。

通過Chrome DevTools中的 Timeline > Paint 選項(xiàng)可以查看更細(xì)節(jié)的 Paint 信息。
優(yōu)先使用渲染層合并屬性、控制層數(shù)量,具體可以做什么?
使用transform/opacity實(shí)現(xiàn)動(dòng)畫效果
使用 transform/opacity 實(shí)現(xiàn)動(dòng)畫效果,會(huì)跳過渲染流程的布局和繪制環(huán)節(jié),只做渲染層的合并。

使用 transform/opacity 的元素必須獨(dú)占一個(gè)渲染層,所以必須提升該元素到單獨(dú)的渲染層。
提升動(dòng)畫效果中的元素
應(yīng)用動(dòng)畫效果的元素應(yīng)該被提升到其自有的渲染層,但不要濫用。
在頁面中創(chuàng)建一個(gè)新的渲染層最好的方式就是使用CSS屬性winll-change,對于目前還不支持will-change屬性、但支持創(chuàng)建渲染層的瀏覽器,可以通過3D transform屬性來強(qiáng)制瀏覽器創(chuàng)建一個(gè)新的渲染層。需要注意的是,不要?jiǎng)?chuàng)建過多的渲染層,這意味著新的內(nèi)存分配和更復(fù)雜的層管理。
.moving-element {
will-change: transform;
transform: translateZ(0);
}
管理渲染層、避免過多數(shù)量的層
盡管提升渲染層看起來很誘人,但不能濫用,因?yàn)楦嗟匿秩緦右馕吨嗟念~外的內(nèi)存和管理資源,所以當(dāng)且僅當(dāng)需要的時(shí)候才為元素創(chuàng)建渲染層。
* {
will-change: transform;
transform: translateZ(0);
}
使用Chrome DevTools來了解頁面的渲染層情況
開啟Chrome DevTools > Timeline > Paint選項(xiàng),然后錄制一段時(shí)間的操作,選擇單獨(dú)的幀,看到每個(gè)幀的渲染細(xì)節(jié),在ESC彈出框有個(gè)Layers選項(xiàng),可以看到渲染層的細(xì)節(jié),有多少渲染層?為何被創(chuàng)建?

對用戶輸入事件的處理函數(shù)去抖動(dòng)(移動(dòng)設(shè)備),具體可以做什么?
用戶輸入事件處理函數(shù)會(huì)在運(yùn)行時(shí)阻塞幀的渲染,并且會(huì)導(dǎo)致額外的布局發(fā)生。
避免使用運(yùn)行時(shí)間過長的輸入事件處理函數(shù)
理想情況下,當(dāng)用戶和頁面交互,頁面的渲染層合并線程將接收到這個(gè)事件并移動(dòng)元素。這個(gè)響應(yīng)過程是不需要主線程參與,不會(huì)導(dǎo)致JavaScript、布局和繪制過程發(fā)生。

但是如果被觸摸的元素綁定了輸入事件處理函數(shù),比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數(shù)執(zhí)行完畢才能執(zhí)行,也就是用戶的滾動(dòng)頁面操作被阻塞了,表現(xiàn)出的行為就是滾動(dòng)出現(xiàn)延遲或者卡頓。
簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數(shù)都能夠快速的執(zhí)行完畢,以便騰出時(shí)間來讓渲染層合并線程完成他的工作。

避免在輸入事件處理函數(shù)中修改樣式屬性
輸入事件處理函數(shù),比如scroll/touch事件的處理,都會(huì)在requestAnimationFrame之前被調(diào)用執(zhí)行。
因此,如果你在上述輸入事件的處理函數(shù)中做了修改樣式屬性的操作,那么這些操作就會(huì)被瀏覽器暫存起來,然后在調(diào)用requestAnimationFrame的時(shí)候,如果你在一開始就做了讀取樣式屬性的操作,那么將會(huì)觸發(fā)瀏覽器的強(qiáng)制同步布局操作。

對滾動(dòng)事件處理函數(shù)去抖動(dòng)
通過requestAnimationFrame可以對樣式修改操作去抖動(dòng),同時(shí)也可以使你的事件處理函數(shù)變得更輕
function onScroll(evt) {
// Store the scroll value for laterz.
lastScrollY = window.scrollY;
// Prevent multiple rAF callbacks.
if (scheduledAnimationFrame) {
return;
}
scheduledAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll', onScroll);
總結(jié)點(diǎn)什么
網(wǎng)站性能優(yōu)化是一個(gè)有一定門檻的細(xì)致活,需要對瀏覽器的機(jī)制有很好的理解,同時(shí)也應(yīng)該學(xué)會(huì)利用Chrome DevTools去分析并解決實(shí)際問題。
