讓你的網(wǎng)頁更絲滑(一)

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

想讓網(wǎng)頁變得絲滑,首先,我們需要一個標準來判斷什么樣的網(wǎng)頁是絲滑的;其次,我們要準確的測量出網(wǎng)頁的性能數(shù)據(jù);最后,使用有效的方法讓網(wǎng)頁變得絲滑。

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

推薦閱讀:iOS開發(fā)——2019 最新 BAT面試題合集(持續(xù)更新中)

作為一個開發(fā)者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:638302184,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗,討論技術, 大家一起交流學習成長!

群內(nèi)提供數(shù)據(jù)結構與算法、底層進階、swift、逆向、整合面試題等免費資料
附上一份收集的各大廠面試題(附答案) ! 群文件直接獲取
各大廠面試題

1. RAIL

到底怎樣的網(wǎng)頁是絲滑的?我們需要一個標準來輔助判斷我們的網(wǎng)頁是否絲滑。

Chrome團隊提出了一個以用戶為中心的性能模型被稱為RAIL,它為工程師提供一個目標,只要達到目標的網(wǎng)頁,用戶就會覺得很流暢;它將用戶體驗拆解為一些關鍵操作,例如:點擊,加載等;并給這些操作規(guī)定一個目標,例如:點擊一個按鈕后,多長時間給反饋用戶會覺得流暢。

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

1.1 響應Response

研究表明,100ms內(nèi)對用戶的輸入操作進行響應,通常會被人類認為是立即響應。時間再長,操作與反應之間的連接就會中斷,人們就會覺得它的操作有延遲。例如:當用戶點擊一個按鈕,如果100ms內(nèi)給出響應,那么用戶就會覺得響應很及時,不會察覺到絲毫延遲感。

1.2 動畫Animation

現(xiàn)如今大多數(shù)設備的屏幕刷新頻率是60Hz,也就是每秒鐘屏幕刷新60次;因此網(wǎng)頁動畫的運行速度只要達到60FPS,我們就會覺得動畫很流暢。

FFrames PPer SSecond指的畫面每秒鐘傳輸?shù)膸瑪?shù),60FPS指的是每秒鐘60幀;換算下來每一幀差不多是16毫秒。

(1 秒 = 1000 毫秒) / 60 幀 = 16.66 毫秒/幀
復制代碼

但通常瀏覽器需要花費一些時間將每一幀的內(nèi)容繪制到屏幕上(包括樣式計算、布局、繪制、合成等工作),所以通常我們只有10毫秒來執(zhí)行JS代碼。

1.3 空閑Idle

為了更好的性能,通常我們會充分利用瀏覽器空閑周期Idle Period做一些低優(yōu)先級的事情。例如:在空閑周期預請求一些接下來可能會用到的數(shù)據(jù)或上報分析數(shù)據(jù)等。

RAIL規(guī)定,空閑周期內(nèi)運行的任務不得超過50ms,當然不止RAIL規(guī)定,W3C性能工作組的Longtasks標準也規(guī)定了超過50毫秒的任務屬于長任務,那么50ms這個數(shù)字是怎么得來的呢?

瀏覽器是單線程的,這意味著同一時間主線程只能處理一個任務,如果一個任務執(zhí)行時間過長,瀏覽器則無法執(zhí)行其他任務,用戶會感覺到瀏覽器被卡死了,因為他的輸入得不到任何響應。

為了達到100ms內(nèi)給出響應,將空閑周期執(zhí)行的任務限制為50ms意味著,即使用戶的輸入行為發(fā)生在空閑任務剛開始執(zhí)行,瀏覽器仍有剩余的50ms時間用來響應用戶輸入,而不會產(chǎn)生用戶可察覺的延遲。如圖1-1所示:

圖1-1

事實上,不論是空閑任務還是高優(yōu)先級的其他任務,執(zhí)行時間都不得超過50ms。

1.4 加載Load

如果不能在1秒鐘內(nèi)加載網(wǎng)頁并讓用戶看到內(nèi)容,用戶的注意力就會分散。用戶會覺得他要做的事情被打斷,如果10秒鐘還打不開網(wǎng)頁,用戶會感到失望,會放棄他們想做的事,以后他們或許都不會再回來。

1.5 小結

通過RAIL,我們可以判斷出我們的網(wǎng)頁是否絲滑。RAIL從用戶感知角度出發(fā)規(guī)定了一些指標,只要我們的網(wǎng)頁符合標準,則我們的網(wǎng)頁是絲滑的,用戶會覺得我們的網(wǎng)頁很流暢。

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

2. 像素管道

像素管道是制作絲滑網(wǎng)頁的靈魂,我們后面將要介紹的技術都與它有關。

上圖就是像素管道,通常我們會使用JS修改一些樣式,隨后瀏覽器會進行樣式計算,然后進行布局,繪制,最后將各個圖層合并在一起完成整個渲染的流程,這期間的每一步都有可能導致頁面卡頓。

注意,并不是所有的樣式改動都需要經(jīng)歷這五個步驟。舉例來說:如果在JS中修改了元素的幾何屬性(寬度、高度等),那么瀏覽器需要需要將這五個步驟都走一遍。但如果您只是修改了文字的顏色,則布局(Layout)是可以跳過去的,如下圖所示:


除了最后的合成,前面四個步驟在不同的場景下都可以被跳過。例如:CSS動畫就可以跳過JS運算,它不需要執(zhí)行JS。

css-triggers1給出了不同的CSS屬性被更改后會觸發(fā)像素管道的哪些步驟。

簡單來說,像素管道經(jīng)歷的步驟越多,渲染時間就越長,單個步驟內(nèi)可能也會因為某種原因而變得耗時很長;所以不管是步驟多還是單個步驟耗費的時間長,最終都會導致整體渲染時間變長。整體時間越長就越有可能超出RAIL所規(guī)定的指標。

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

當然,即便能保證每一幀的總耗時小于16ms,依然無法保證不會丟幀。關于這點后面我們會詳細介紹。

3. 如何讓動畫更絲滑

動畫需要達到60FPS才能變得絲滑,本節(jié)我們介紹如何讓動畫在不丟幀的情況下穩(wěn)定保持在60FPS。

3.1 使用Chrome開發(fā)者工具測量動畫性能

在評估動畫性能時,通常需要逐幀評估像素管道的開銷;使用 Chrome 開發(fā)者工具可以輔助我們進行精準的測量。

在Chrome開發(fā)者工具中,點擊Performance面板,然后選中Screenshots復選框,。如圖3-1所示:

圖3-1Chrome開發(fā)者工具Performance面板

然后點擊錄制按鈕,錄制完畢后點擊停止按鈕就可以捕獲當前頁面的性能數(shù)據(jù)。如圖3-2所示:

圖3-2捕獲性能數(shù)據(jù)

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


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

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

圖3-4性能面板最主要的部分

從上圖可以看到,瀏覽器每一幀渲染所執(zhí)行的任務與前面我們介紹的像素管道是相同的。上圖中因為是CSS動畫,所以沒有運行JS,但每一幀都需要計算樣式、布局、繪制與合成。

3.2 如何讓JS動畫更絲滑

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

如圖3-4所示,即便是在不執(zhí)行JS的情況下,瀏覽器計算樣式、布局、繪制等工作也是需要時間的,所以需要給瀏覽器預留出 充分的時間做這些事情,現(xiàn)在留給JS的執(zhí)行時間就只有 10ms。

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

一旦JS運行時間超過10ms,就很有可能導致這一幀的像素管道整體耗時超過16ms,從而無法達到60FPS,但你以為只要保證JS的運行時間小于10ms就一定能保證不丟幀?Naive~

3.2.1 使用requestAnimationFrame

即便你能保證每一幀的總耗時都小于16ms,也無法保證一定不會出現(xiàn)丟幀的情況,這取決于觸發(fā)JS執(zhí)行的方式。

假設使用 setTimeoutsetInterval 來觸發(fā)JS執(zhí)行并修改樣式從而導致視覺變化;那么會有這樣一種情況,因為setTimeoutsetInterval沒有辦法保證回調(diào)函數(shù)什么時候執(zhí)行,它可能在每一幀的中間執(zhí)行,也可能在每一幀的最后執(zhí)行。所以會導致即便我們能保障每一幀的總耗時小于16ms,但是執(zhí)行的時機如果在每一幀的中間或最后,最后的結果依然是沒有辦法每隔16ms讓屏幕產(chǎn)生一次變化。如圖3-6所示:

圖3-6使用定時器觸發(fā)動畫

也就是說,即便我們能保證每一幀總體時間小于16ms,但如果使用定時器觸發(fā)動畫,那么由于定時器的觸發(fā)時機不確定,所以還是會導致動畫丟幀?,F(xiàn)在整個Web只有一個API可以解決這個問題,那就是requestAnimationFrame,它可以保證回調(diào)函數(shù)穩(wěn)定的在每一幀最開始觸發(fā)。如圖3-7所示:

圖3-7使用requestAnimationFrame觸發(fā)動畫

3.2.2 避免FSL

FSL Forced Synchronous Layouts被稱為強制同步布局;前面介紹像素管道時說過,將一幀送到屏幕會通過如下順序:

先執(zhí)行JS,然后在JS中修改了樣式從而導致樣式計算,然后樣式的改動觸發(fā)了布局、繪制、合成。但JavaScript可以強制瀏覽器將布局提前執(zhí)行,這就叫F 強制 S 同步 L 布局


圖3-8強制同步布局

通常我們一不小心就造成了FSL,請看下面代碼:

box.classList.add('big');
const width = box.offsetWidth;
復制代碼

代碼中通過新增class修改了元素的樣式,隨后使用offsetWidth讀取元素的寬度。乍一看似乎沒什么問題,但這段代碼會導致FSL。

在 JavaScript 運行時,上一幀已經(jīng)渲染好的所有布局值都是已知的,我們可以使用offsetWidth這樣的語法獲得值;但這一幀剛修改完的樣式瀏覽器還沒渲染呢,這時候使用offsetWidth這樣的語法讀取元素的寬度,那么瀏覽器為了告訴我們寬度值,它必須先計算該寬度,這就需要布局。如圖3-8所示,布局跑到了樣式計算的前面。

所以正確的做法是先獲取寬度,然后再更改樣式:

const width = box.offsetWidth;
box.classList.add('big');
復制代碼

看起來,似乎即使觸發(fā)了FSL也不過就是管道的順序變了而已,影響好像并沒有那么大。??

單個FSL對性能的影響確實不大,但如果觸發(fā)了布局抖動,則影響會變得非常大。看下面代碼:

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元素的寬度;在循環(huán)中我們先獲取容器元素的寬度,隨后設置了P元素的樣式。這會導致瀏覽器去布局,然后計算樣式。每次更改樣式,都會導致剛剛執(zhí)行的布局失效,因為我們又改了新的樣式,所以下一輪循環(huán)讀取寬度時,瀏覽器又要執(zhí)行一次布局,如此反復直到循環(huán)結束。在循環(huán)期間,瀏覽器不停地執(zhí)行無效布局,這被稱為布局抖動Layout Thrashing;這種錯誤導致的性能問題非常高。

如果我們不小心觸發(fā)了FSL,Chrome開發(fā)者工具會給出紅色的線提示,如圖3-9所示:

圖3-9開發(fā)者工具提示FSL

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

圖3-10開發(fā)者工具提示FSL詳情

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

為了避免布局抖動,我們可以將讀取元素寬度的代碼放到循環(huán)的外面。代碼如下:

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開發(fā)者工具來捕獲性能數(shù)據(jù)。

圖3-11優(yōu)化后的時間

圖3-11可以看到,優(yōu)化后這一幀的總時間用了4.7ms,而優(yōu)化前的是101ms,如圖3-12所示:

圖3-12優(yōu)化前的時間

優(yōu)化后比優(yōu)化前,每幀所耗費的時間快了21.7倍,數(shù)字非常驚人。

3.3 如何讓CSS動畫更絲滑

CSS動畫通常使用@keyframetransition結合樣式的變動來實現(xiàn)視覺變化的效果。我們同樣可以通過減少像素管道的步驟和每個步驟所耗費的時間讓CSS動畫更流暢。

本節(jié)介紹的CSS動畫的優(yōu)化方式同樣適用于JS動畫,但上一節(jié)介紹的JS動畫優(yōu)化方法不適用于CSS動畫,它們是包含關系。

繪制Paint通常需要花費很長時間,我們可以通過Chrome開發(fā)者工具來觀察正在繪制的區(qū)域。打開開發(fā)者工具,按下鍵盤上的 Esc 鍵。在出現(xiàn)的面板中,切換到“rendering”標簽,然后選中“Paint flashing”。如圖3-13所示:

圖3-13開啟繪制閃爍

開啟繪制閃爍后,每當頁面發(fā)生繪制時,我們都可以在屏幕上看到繪制發(fā)生區(qū)有綠色在閃爍。如圖3-14所示:

繪制區(qū)域閃爍

圖3-14繪制區(qū)域閃爍

如圖3-14所示,當我們開啟了繪制閃爍,則會繪制區(qū)域出現(xiàn)了綠色的閃爍,可以點擊我查看Demo4。

當我們看到我們認為不應該繪制的區(qū)域時,我們應該進一步研究并取消繪制區(qū)域。

如何才能避免繪制的發(fā)生呢?答案是:圖層。

事實上瀏覽器在渲染頁面時,可以將頁面分為很多個圖層,有點類似于PhotoShop,一張圖片在PotoShop中是由多個圖層組合而成,而瀏覽器最終顯示的頁面實際也是由多個圖層構成的。如圖3-15所示:

圖3-15圖層

將原本不斷發(fā)生變化的元素提升到單獨的圖層中,就不再需要繪制了,瀏覽器只需要將兩個圖層合并在一起即可,查看Demo請狠狠的點擊我5。

如果您點擊了上面的Demo地址,并開啟了繪制閃爍,您會發(fā)現(xiàn)沒有任何閃爍發(fā)生,因為瀏覽器沒有進行繪制。如果您查看Layers面板,你會看到這樣的場景,如圖3-16:

圖層

3-16圖層

當我們使用Performance面板捕獲性能數(shù)據(jù)時會發(fā)現(xiàn)繪制已經(jīng)不見了。如圖3-17所示:

圖3-17捕獲不到繪制

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

在Chrome開發(fā)者工具“rendering”標簽中,選中“Layer borders”??梢钥吹巾撁嬷杏心男┖铣蓪?。合成層會使用橘黃色的邊框,如圖3-18所示:

圖3-18顯示合成層

為了減少繪制,可以通過新增圖層,但是圖層的管理也是需要成本的,所以要避免濫用,通常需要具體情況具體分析,做出合適的選擇。

前面我的Demo都是修改元素的left屬性讓方塊移動,這避免不了需要進行布局操作,最佳的方法是使用transform屬性,這個屬性是由合成器單獨處理的,所以使用這個屬性可以避免布局與繪制。

總結

RAIL可以幫助我們判斷什么樣的網(wǎng)頁是絲滑的,而開發(fā)者工具可以讓我們進一步準確的捕獲出網(wǎng)頁的性能數(shù)據(jù)。

JS動畫要保證預留出6ms的時間給瀏覽器處理像素管道,而自身執(zhí)行時間應該小于10ms來保證整體運行速度小于16ms。但觸發(fā)動畫的時機也很重要,定時器無法穩(wěn)定的觸發(fā)動畫,所以我們需要使用requestAnimationFrame觸發(fā)JS動畫。同時我們應該避免一切FSL,它對性能的影響非常大。

CSS動畫我們可以通過降低繪制區(qū)域并且使transform屬性來完成動畫,同時我們需要管理好圖層,因為繪制和圖層管理都需要成本,通常我們需要根據(jù)具體情況進行權衡并做出最好的選擇。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 讀這篇文章,終于讓我對頁面中幀的概念有了更清晰的了解。 幀與FPS的概念 要了解幀,首先要知道屏幕的成像原理。屏幕...
    般犀閱讀 562評論 0 0
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標準。 注意:講述HT...
    kismetajun閱讀 28,770評論 1 45
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,260評論 5 13
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,621評論 1 32
  • CSS參考手冊 一、初識CSS3 1.1 CSS是什么 CSS3在CSS2.1的基礎上增加了很多強大的新功能。目前...
    沒汁帥閱讀 4,265評論 1 13

友情鏈接更多精彩內(nèi)容