瀏覽器渲染基本原理(五):優化渲染性能css
網頁不只應該被快速加載,同時還應該流暢運行,好比快速響應的交互,如絲般順滑的動畫等。
大多數設備的刷新頻率是60次/秒,也就說是瀏覽器對每一幀畫面的渲染工做要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。
爲了保證頁面的渲染效果,須要充分了解瀏覽器是如何處理HTML/JavaScript/CSS的。jquery
JavaScript
:JavaScript實現動畫效果,DOM元素操做等。
Style(計算樣式)
:肯定每一個DOM元素應該應用什麼CSS規則。
Layout(佈局)
:計算每一個DOM元素在最終屏幕上顯示的大小和位置。因爲web頁面的元素佈局是相對的,因此其中任意一個元素的位置發生變化,都會聯動的引發其餘元素髮生變化,這個過程叫reflow。
Paint(繪製)
:在多個層上繪製DOM元素的的文字、顏色、圖像、邊框和陰影等。
Composite(渲染層合併)
:按照合理的順序合併圖層而後顯示到屏幕上。git
實際場景下,大概會有三種常見的渲染流程(也便是Layout
和Paint
步驟是可避免的):
github
結合上述的渲染流程,咱們能夠去針對性的分析並優化每一個步驟。web
setTimeout(callback)
和setInterval(callback)
沒法保證callback函數的執行時機,極可能在幀結束的時候執行,從而致使丟幀,以下圖:
瀏覽器
注意:jQuery的animate函數就是用setTimeout來實現動畫,能夠經過jquery-requestAnimationFrame這個補丁來用requestAnimationFrame替代setTimeout安全
JavaScript代碼運行在瀏覽器的主線程上,與此同時,瀏覽器的主線程還負責樣式計算、佈局、繪製的工做,若是JavaScript代碼運行時間過長,就會阻塞其餘渲染工做,極可能會致使丟幀。
前面提到每幀的渲染應該在16ms內完成,但在動畫過程當中,因爲已經被佔用了很多時間,因此JavaScript代碼運行耗時應該控制在3-4毫秒。
若是真的有特別耗時且不操做DOM元素的純計算工做,能夠考慮放到Web Workers中執行。性能優化
var dataSortWorker = new Worker("sort-worker.js"); dataSortWorker.postMesssage(dataToSort); // 主線程不受Web Workers線程干擾 dataSortWorker.addEventListener('message', function(evt) { var sortedData = e.data; // Web Workers線程執行結束 // ... });
因爲Web Workers不能操做DOM元素的限制,因此只能作一些純計算的工做,對於不少須要操做DOM元素的邏輯,能夠考慮分步處理,把任務分爲若干個小任務,每一個任務都放到requestAnimationFrame中回調執行框架
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList); function processTaskList(taskStartTime) { var nextTask = taskList.pop(); // 執行小任務 processTask(nextTask); if (taskList.length > 0) { requestAnimationFrame(processTaskList); } }
打開Chrome DevTools > Timeline > JS Profile
,錄製一次動做,而後分析獲得的細節信息,從而發現問題並修復問題。dom
添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操做,都會引發DOM結構的改變,從而致使瀏覽器須要從新計算每一個元素的樣式,對整個頁面或部分頁面從新佈局,這就是所謂的樣式計算。
樣式計算主要分爲兩步:建立一套匹配的樣式選擇器,爲匹配的樣式選擇器計算具體的樣式規則
儘可能保持class的簡短,或者使用Web Components框架。
因爲瀏覽器的優化,現代瀏覽器的樣式計算直接對目標元素執行,而不是對整個頁面執行,因此咱們應該儘量減小須要執行樣式計算的元素的個數
佈局就是計算DOM元素的大小和位置的過程,若是你的頁面中包含不少元素,那麼計算這些元素的位置將耗費很長時間。
佈局的主要消耗在於:1. 須要佈局的DOM元素的數量;2. 佈局過程的複雜程度
當你修改了元素的屬性以後,瀏覽器將會檢查爲了使這個修改生效是否須要從新計算佈局以及更新渲染樹,對於DOM元素的「幾何屬性」修改,好比width/height/left/top等,都須要從新計算佈局。
對於不能避免的佈局,可使用Chrome DevTools工具的Timeline查看明細。
老的佈局模型以相對/絕對/浮動的方式將元素定位到屏幕上
Floxbox佈局模型用流式佈局的方式將元素定位到屏幕上
經過一個小實驗能夠看出兩種佈局模型的性能差距,一樣對1300個元素佈局,浮動佈局耗時14.3ms,Flexbox佈局耗時3.5ms
前面提過,將一幀畫面渲染的屏幕上的流程是:
首先是JavaScript腳本,而後是Style,而後是Layout,可是咱們能夠強制瀏覽器在執行JavaScript腳本以前先執行佈局過程,這就是所謂的強制同步佈局。
requestAnimationFrame(logBoxHeight); // 先寫後讀,觸發強制佈局 function logBoxHeight() { // 更新box樣式 box.classList.add('super-big'); // 爲了返回box的offersetHeight值 // 瀏覽器必須先應用屬性修改,接着執行佈局過程 console.log(box.offsetHeight); } // 先讀後寫,避免強制佈局 function logBoxHeight() { // 獲取box.offsetHeight console.log(box.offsetHeight); // 更新box樣式 box.classList.add('super-big'); }
在JavaScript腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。所以,若是你在當前幀獲取屬性以前又對元素節點有改動,那就會致使瀏覽器必須先應用屬性修改,結果執行佈局過程,最後再執行JavaScript邏輯。
若是連續快速的屢次觸發強制同步佈局,那麼結果更糟糕。
好比下面的例子,獲取box的屬性,設置到paragraphs上,因爲每次設置paragraphs都會觸發樣式計算和佈局過程,而下一次獲取box的屬性必須等到上一步設置結束以後才能觸發。
function resizeWidth() { // 會讓瀏覽器陷入'讀寫讀寫'循環 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來確保讀寫操做的安全,從而幫你自動完成讀寫操做的批處理,還能避免意外地觸發強制同步佈局或快速連續佈局
繪製就是填充像素的過程,一般這個過程是整個渲染流程中耗時最長的一環,所以也是最須要避免發生的一環。
若是Layout被觸發,那麼接下來元素的Paint必定會被觸發。固然純粹改變元素的非幾何屬性,也可能會觸發Paint,好比背景、文字顏色、陰影效果等。
繪製並不是老是在內存中的單層畫面裏完成的,實際上,瀏覽器在必要時會將一幀畫面繪製成多層畫面,而後將這若干層畫面合併成一張圖片顯示到屏幕上。
這種繪製方式的好處是,使用transform來實現移動效果的元素將會被正常繪製,同時不會觸發其餘元素的繪製。
瀏覽器會把相鄰區域的渲染任務合併在一塊兒進行,因此須要對動畫效果進行精密設計,以保證各自的繪製區域不會有太多重疊。
能夠實現一樣效果的不一樣方式,咱們應該採用性能更好的那種。
打開DevTools,按下鍵盤的ESC鍵,在彈出的面板中,選中rendering
選項卡下的Enable paint flashing
,這樣每當頁面發生繪製的時候,屏幕就會閃現綠色的方框。經過該工具能夠檢查Paint發生的區域和時機是否是能夠被優化。
經過Chrome DevTools中的Timeline > Paint
選項能夠查看更細節的Paint信息
使用transform/opacity實現動畫效果,會跳過渲染流程的佈局和繪製環節,只作渲染層的合併。
儘管提高渲染層看起來很誘人,但不能濫用,由於更多的渲染層意味着更多的額外的內存和管理資源,因此當且僅當須要的時候才爲元素建立渲染層。
* { will-change: transform; transform: translateZ(0); }
開啓Chrome DevTools > Timeline > Paint
選項,而後錄製一段時間的操做,選擇單獨的幀,看到每一個幀的渲染細節,在ESC彈出框有個Layers
選項,能夠看到渲染層的細節,有多少渲染層?爲什麼被建立?
用戶輸入事件處理函數會在運行時阻塞幀的渲染,而且會致使額外的佈局發生。
理想狀況下,當用戶和頁面交互,頁面的渲染層合併線程將接收到這個事件並移動元素。這個響應過程是不須要主線程參與,不會致使JavaScript、佈局和繪製過程發生。
可是若是被觸摸的元素綁定了輸入事件處理函數,好比touchstart/touchmove/touchend,那麼渲染層合併線程必須等待這些被綁定的處理函數執行完畢才能執行,也就是用戶的滾動頁面操做被阻塞了,表現出的行爲就是滾動出現延遲或者卡頓。
簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數都可以快速的執行完畢,以便騰出時間來讓渲染層合併線程完成他的工做。
輸入事件處理函數,好比scroll/touch事件的處理,都會在requestAnimationFrame以前被調用執行。
所以,若是你在上述輸入事件的處理函數中作了修改樣式屬性的操做,那麼這些操做就會被瀏覽器暫存起來,而後在調用requestAnimationFrame的時候,若是你在一開始就作了讀取樣式屬性的操做,那麼將會觸發瀏覽器的強制同步佈局操做。
經過requestAnimationFrame能夠對樣式修改操做去抖動,同時也可使你的事件處理函數變得更輕
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);
網站性能優化是一個有必定門檻的細緻活,須要對瀏覽器的機制有很好的理解,同時也應該學會利用Chrome DevTools去分析並解決實際問題,關於Chrome DevTools的學習我會專門開一篇博客來說解,同時會結合具體的性能問題來分析。