讓你的網頁更絲滑(一)

編者按:本文做者Berwin,W3C性能工做組成員,360導航高級前端工程師。《深刻淺出Vue.js》(正在出版)做者。javascript

前段時間,我將精力專一在Web性能領域;在這個領域下有個重要的課題是如何讓網頁更絲滑(流暢)。css

想讓網頁變得絲滑,首先,咱們須要一個標準來判斷什麼樣的網頁是絲滑的;其次,咱們要準確的測量出網頁的性能數據;最後,使用有效的方法讓網頁變得絲滑。前端

本篇文章將針對這三個方面進行詳細的介紹。java

1. RAIL

到底怎樣的網頁是絲滑的?咱們須要一個標準來輔助判斷咱們的網頁是否絲滑。瀏覽器

Chrome團隊提出了一個以用戶爲中心的性能模型被稱爲RAIL,它爲工程師提供一個目標,只要達到目標的網頁,用戶就會以爲很流暢;它將用戶體驗拆解爲一些關鍵操做,例如:點擊,加載等;並給這些操做規定一個目標,例如:點擊一個按鈕後,多長時間給反饋用戶會以爲流暢。ruby

RAIL將影響性能的行爲劃分爲四個方面,分別是:Response響應Animation動畫Idle空閒Load加載。沒錯,RAIL這個名字來自於這四個單詞的首字母,方便記憶。bash

1.1 響應Response

研究代表,100ms內對用戶的輸入操做進行響應,一般會被人類認爲是當即響應。時間再長,操做與反應之間的鏈接就會中斷,人們就會以爲它的操做有延遲。例如:當用戶點擊一個按鈕,若是100ms內給出響應,那麼用戶就會以爲響應很及時,不會察覺到絲毫延遲感。前端工程師

1.2 動畫Animation

現現在大多數設備的屏幕刷新頻率是60Hz,也就是每秒鐘屏幕刷新60次;所以網頁動畫的運行速度只要達到60FPS,咱們就會以爲動畫很流暢。函數

FFrames PPer SSecond 指的畫面每秒鐘傳輸的幀數,60FPS指的是每秒鐘60幀;換算下來每一幀差很少是16毫秒。
(1 秒 = 1000 毫秒) / 60 幀 = 16.66 毫秒/幀
複製代碼

但一般瀏覽器須要花費一些時間將每一幀的內容繪製到屏幕上(包括樣式計算、佈局、繪製、合成等工做),因此一般咱們只有10毫秒來執行JS代碼。工具

1.3 空閒Idle

爲了更好的性能,一般咱們會充分利用瀏覽器空閒週期Idle Period作一些低優先級的事情。例如:在空閒週期預請求一些接下來可能會用到的數據或上報分析數據等。

RAIL規定,空閒週期內運行的任務不得超過50ms,固然不止RAIL規定,W3C性能工做組的Longtasks標準也規定了超過50毫秒的任務屬於長任務,那麼50ms這個數字是怎麼得來的呢?

瀏覽器是單線程的,這意味着同一時間主線程只能處理一個任務,若是一個任務執行時間過長,瀏覽器則沒法執行其餘任務,用戶會感受到瀏覽器被卡死了,由於他的輸入得不到任何響應。

爲了達到100ms內給出響應,將空閒週期執行的任務限制爲50ms意味着,即便用戶的輸入行爲發生在空閒任務剛開始執行,瀏覽器仍有剩餘的50ms時間用來響應用戶輸入,而不會產生用戶可察覺的延遲。如圖1-1所示:

圖1-1
圖1-1

事實上,不管是空閒任務仍是高優先級的其餘任務,執行時間都不得超過50ms

1.4 加載Load

若是不能在1秒鐘內加載網頁並讓用戶看到內容,用戶的注意力就會分散。用戶會以爲他要作的事情被打斷,若是10秒鐘還打不開網頁,用戶會感到失望,會放棄他們想作的事,之後他們或許都不會再回來。

1.5 小結

經過RAIL,咱們能夠判斷出咱們的網頁是否絲滑。RAIL從用戶感知角度出發規定了一些指標,只要咱們的網頁符合標準,則咱們的網頁是絲滑的,用戶會以爲咱們的網頁很流暢。

RAIL 關鍵指標 用戶操做
響應(Response) 小於100ms 點擊按鈕。
動畫(Animation) 小於16ms 滾動頁面,拖動手指,播放動畫等。
空閒(Idle) 小於50ms 用戶沒有與頁面交互,但應該保證主線程足夠處理下一個用戶輸入。
加載(Load) 1000ms 用戶加載頁面並看到內容。

2. 像素管道

像素管道是製做絲滑網頁的靈魂,咱們後面將要介紹的技術都與它有關。

像素管道

上圖就是像素管道,一般咱們會使用JS修改一些樣式,隨後瀏覽器會進行樣式計算,而後進行佈局,繪製,最後將各個圖層合併在一塊兒完成整個渲染的流程,這期間的每一步都有可能致使頁面卡頓。

注意,並非全部的樣式改動都須要經歷這五個步驟。舉例來講:若是在JS中修改了元素的幾何屬性(寬度、高度等),那麼瀏覽器須要須要將這五個步驟都走一遍。但若是您只是修改了文字的顏色,則佈局(Layout)是能夠跳過去的,以下圖所示:

像素管道2

除了最後的合成,前面四個步驟在不一樣的場景下均可以被跳過。例如:CSS動畫就能夠跳過JS運算,它不須要執行JS。

css-triggers1給出了不一樣的CSS屬性被更改後會觸發像素管道的哪些步驟。

簡單來講,像素管道經歷的步驟越多,渲染時間就越長,單個步驟內可能也會由於某種緣由而變得耗時很長;因此無論是步驟多仍是單個步驟耗費的時間長,最終都會致使總體渲染時間變長。總體時間越長就越有可能超出RAIL所規定的指標。

舉個簡單的例子:網頁動畫的渲染如果達到60FPS,則動畫不會丟幀。假設渲染管道的佈局與繪製耗費了10ms,那麼加上樣式計算與合成的時間,則留給JS處理動畫的時間就只有幾毫秒,若是JS的執行超過了幾毫秒那麼該動畫每一幀所耗費的時間就會超過16ms,這時候動畫必定會丟幀,用戶用肉眼就能夠看到明顯的卡頓。

固然,即使能保證每一幀的總耗時小於16ms,依然沒法保證不會丟幀。關於這點後面咱們會詳細介紹。

3. 如何讓動畫更絲滑

動畫須要達到60FPS才能變得絲滑,本節咱們介紹如何讓動畫在不丟幀的狀況下穩定保持在60FPS。

3.1 使用Chrome開發者工具測量動畫性能

在評估動畫性能時,一般須要逐幀評估像素管道的開銷;使用 Chrome 開發者工具能夠輔助咱們進行精準的測量。

在Chrome開發者工具中,點擊Performance面板,而後選中Screenshots複選框,。如圖3-1所示:

Chrome Devtools Performance
圖3-1Chrome開發者工具Performance面板

而後點擊錄製按鈕,錄製完畢後點擊中止按鈕就能夠捕獲當前頁面的性能數據。如圖3-2所示:

捕獲性能數據
圖3-2捕獲性能數據

捕獲出的結果如圖3-3所示:

捕獲出的性能結果
圖3-3捕獲出的性能結果

咱們能夠放大主線程從而精準的看到每一幀瀏覽器都執行了哪些任務以及每一個任務耗費了多長時間。如圖3-4所示:

像素管道
圖3-4性能面板最主要的部分

從上圖能夠看到,瀏覽器每一幀渲染所執行的任務與前面咱們介紹的像素管道是相同的。上圖中由於是CSS動畫,因此沒有運行JS,但每一幀都須要計算樣式、佈局、繪製與合成。

3.2 如何讓JS動畫更絲滑

JS動畫是使用定時器不停的執行JS,經過在JS中修改樣式完成網頁動畫;若想保證動畫流暢,從JS的執行到最終瀏覽器顯示出畫面,每一幀總耗時最多16ms,這樣動畫才能達到60FPS。

如圖3-4所示,即使是在不執行JS的狀況下,瀏覽器計算樣式、佈局、繪製等工做也是須要時間的,因此須要給瀏覽器預留出 充分的時間6ms 作這些事情,如今留給JS的執行時間就只有 10ms

每一幀的整體耗時必須小於16ms
圖3-5每一幀整體耗時必須小於16ms,JS運行時間小於10ms

一旦JS運行時間超過10ms,就頗有可能致使這一幀的像素管道總體耗時超過16ms,從而沒法達到60FPS,但你覺得只要保證JS的運行時間小於10ms就必定能保證不丟幀?Naive~

3.2.1 使用requestAnimationFrame

即使你能保證每一幀的總耗時都小於16ms,也沒法保證必定不會出現丟幀的狀況,這取決於觸發JS執行的方式。

假設使用 setTimeoutsetInterval 來觸發JS執行並修改樣式從而致使視覺變化;那麼會有這樣一種狀況,由於setTimeoutsetInterval沒有辦法保證回調函數何時執行,它可能在每一幀的中間執行,也可能在每一幀的最後執行。因此會致使即使咱們能保障每一幀的總耗時小於16ms,可是執行的時機若是在每一幀的中間或最後,最後的結果依然是沒有辦法每隔16ms讓屏幕產生一次變化。如圖3-6所示:

使用定時器觸發動畫
圖3-6使用定時器觸發動畫

也就是說,即使咱們能保證每一幀整體時間小於16ms,但若是使用定時器觸發動畫,那麼因爲定時器的觸發時機不肯定,因此仍是會致使動畫丟幀。如今整個Web只有一個API能夠解決這個問題,那就是requestAnimationFrame,它能夠保證回調函數穩定的在每一幀最開始觸發。如圖3-7所示:

使用requestAnimationFrame觸發動畫
圖3-7使用requestAnimationFrame觸發動畫

3.2.2 避免FSL

FSL (Forced Synchronous Layouts) 被稱爲強制同步佈局;前面介紹像素管道時說過,將一幀送到屏幕會經過以下順序:

像素管道

先執行JS,而後在JS中修改了樣式從而致使樣式計算,而後樣式的改動觸發了佈局、繪製、合成。但JavaScript能夠強制瀏覽器將佈局提早執行,這就叫 F 強制 S 同步 L 佈局

圖3-8強制同步佈局
圖3-8強制同步佈局

一般咱們一不當心就形成了FSL,請看下面代碼:

box.classList.add('big');
const width = box.offsetWidth;
複製代碼

代碼中經過新增class修改了元素的樣式,隨後使用offsetWidth讀取元素的寬度。乍一看彷佛沒什麼問題,但這段代碼會致使FSL。

在 JavaScript 運行時,上一幀已經渲染好的全部佈局值都是已知的,咱們可使用offsetWidth這樣的語法得到值;但這一幀剛修改完的樣式瀏覽器還沒渲染呢,這時候使用offsetWidth這樣的語法讀取元素的寬度,那麼瀏覽器爲了告訴咱們寬度值,它必須先計算該寬度,這就須要佈局。如圖3-8所示,佈局跑到了樣式計算的前面。

因此正確的作法是先獲取寬度,而後再更改樣式:

const width = box.offsetWidth;
box.classList.add('big');
複製代碼

看起來,彷佛即便觸發了FSL也不過就是管道的順序變了而已,影響好像並無那麼大。🤔

單個FSL對性能的影響確實不大,但若是觸發了佈局抖動,則影響會變得很是大。看下面代碼:

const container = document.querySelector('.container');
const boxes = document.querySelectorAll('p');

for (var i = 0; i < boxes.length; i++) {
  // Read a layout property
  const newWidth = container.offsetWidth;
    
  // Then invalidate layouts with writes.
  boxes[i].style.width = newWidth + 'px';
}
複製代碼

上面代碼的做用是批量修改N個P元素的寬度;在循環中咱們先獲取容器元素的寬度,隨後設置了P元素的樣式。這會致使瀏覽器去佈局,而後計算樣式。每次更改樣式,都會致使剛剛執行的佈局失效,由於咱們又改了新的樣式,因此下一輪循環讀取寬度時,瀏覽器又要執行一次佈局,如此反覆直到循環結束。在循環期間,瀏覽器不停地執行無效佈局,這被稱爲 佈局抖動Layout Thrashing;這種錯誤致使的性能問題很是高。

若是咱們不當心觸發了FSL,Chrome開發者工具會給出紅色的線提示,如圖3-9所示:

開發者工具提示FSL
圖3-9開發者工具提示FSL

同時任務的右上角會有紅色的三角形表示,咱們能夠放大任務進一步查看,如圖3-10所示:

開發者工具提示FSL詳情
圖3-10開發者工具提示FSL詳情

若想看Demo能夠點擊我2,在Demo中點擊按鈕可讓P標籤的寬度變長。

爲了不佈局抖動,咱們能夠將讀取元素寬度的代碼放到循環的外面。代碼以下:

const container = document.querySelector('.container');
const boxes = document.querySelectorAll('p');

// Read a layout property
const newWidth = container.offsetWidth;

for (var i = 0; i < boxes.length; i++) {    
    // Then invalidate layouts with writes.
    boxes[i].style.width = newWidth + 'px';
}
複製代碼

若想看Demo能夠點擊我3,能夠看到這個Demo與前一個demo如出一轍,甚至咱們沒法用肉眼分辨出哪一個更快,這是由於DOM元素少,因此整體時間都比較少,但咱們能夠經過Chrome開發者工具來捕獲性能數據。

優化後的時間
圖3-11優化後的時間

圖3-11能夠看到,優化後這一幀的總時間用了4.7ms,而優化前的是101ms,如圖3-12所示:

優化前的時間
圖3-12優化前的時間

優化後比優化前,每幀所耗費的時間快了21.7倍,數字很是驚人。

3.3 如何讓CSS動畫更絲滑

CSS動畫一般使用@keyframetransition結合樣式的變更來實現視覺變化的效果。咱們一樣能夠經過減小像素管道的步驟和每一個步驟所耗費的時間讓CSS動畫更流暢。

本節介紹的CSS動畫的優化方式一樣適用於JS動畫,但上一節介紹的JS動畫優化方法不適用於CSS動畫,它們是包含關係。

繪製Paint一般須要花費很長時間,咱們能夠經過Chrome開發者工具來觀察正在繪製的區域。打開開發者工具,按下鍵盤上的 Esc 鍵。在出現的面板中,切換到「rendering」標籤,而後選中「Paint flashing」。如圖3-13所示:

圖3-13開啓繪製閃爍
圖3-13開啓繪製閃爍

開啓繪製閃爍Paint flashing後,每當頁面發生繪製時,咱們均可以在屏幕上看到繪製發生區有綠色在閃爍。如圖3-14所示:

繪製區域閃爍
圖3-14繪製區域閃爍

如圖3-14所示,當咱們開啓了繪製閃爍,則會繪製區域出現了綠色的閃爍,能夠點擊我查看Demo4

當咱們看到咱們認爲不該該繪製的區域時,咱們應該進一步研究並取消繪製區域。

如何才能避免繪製的發生呢?答案是:圖層。

事實上瀏覽器在渲染頁面時,能夠將頁面分爲不少個圖層,有點相似於PhotoShop,一張圖片在PotoShop中是由多個圖層組合而成,而瀏覽器最終顯示的頁面實際也是由多個圖層構成的。如圖3-15所示:

圖層
圖3-15圖層

將本來不斷髮生變化的元素提高到單獨的圖層中,就再也不須要繪製了,瀏覽器只須要將兩個圖層合併在一塊兒便可,查看Demo請狠狠的點擊我5

若是您點擊了上面的Demo地址,並開啓了繪製閃爍,您會發現沒有任何閃爍發生,由於瀏覽器沒有進行繪製。若是您查看Layers面板,你會看到這樣的場景,如圖3-16:

圖層
3-16圖層

當咱們使用Performance面板捕獲性能數據時會發現繪製Paint已經不見了。如圖3-17所示:

捕獲不到繪製
圖3-17捕獲不到繪製

建立圖層的最佳方式是使用will-change,但某些不支持這個屬性的瀏覽器可使用3D 變形(transform: translateZ(0))來強制建立一個新層。

在Chrome開發者工具「rendering」標籤中,選中「Layer borders」。能夠看到頁面中有哪些合成層。合成層會使用橘黃色的邊框,如圖3-18所示:

顯示合成層
圖3-18顯示合成層

爲了減小繪製,能夠經過新增圖層,可是圖層的管理也是須要成本的,因此要避免濫用,一般須要具體狀況具體分析,作出合適的選擇。

前面個人Demo都是修改元素的left屬性讓方塊移動,這避免不了須要進行佈局操做,最佳的方法是使用transform屬性,這個屬性是由合成器單獨處理的,因此使用這個屬性能夠避免佈局與繪製。

總結

RAIL能夠幫助咱們判斷什麼樣的網頁是絲滑的,而開發者工具可讓咱們進一步準確的捕獲出網頁的性能數據。

JS動畫要保證預留出6ms的時間給瀏覽器處理像素管道,而自身執行時間應該小於10ms來保證總體運行速度小於16ms。但觸發動畫的時機也很重要,定時器沒法穩定的觸發動畫,因此咱們須要使用requestAnimationFrame觸發JS動畫。同時咱們應該避免一切FSL,它對性能的影響很是大。

CSS動畫咱們能夠經過下降繪製區域而且使transform屬性來完成動畫,同時咱們須要管理好圖層,由於繪製和圖層管理都須要成本,一般咱們須要根據具體狀況進行權衡並作出最好的選擇。

相關文章
相關標籤/搜索