作為一名web開發(fā),總是希望自己開發(fā)web頁面具有交互性并且運(yùn)行順暢,為了順暢運(yùn)行,滾動應(yīng)與手指的滑動一樣快,并且動畫和交互應(yīng)如絲綢般順滑
60fps與設(shè)備刷新率
目前大多數(shù)設(shè)備的屏幕刷新率為 60 次/秒芹血。因此,如果在頁面中有一個動畫或漸變效果,或者用戶正在滾動頁面祟牲,那么瀏覽器渲染動畫或頁面的每一幀的速率也需要跟設(shè)備屏幕的刷新率保持一致隙畜。
其中每個幀的預(yù)算時間僅比 16 毫秒多一點(diǎn) (1 秒/ 60 = 16.66 毫秒)。但實(shí)際上说贝,瀏覽器有整理工作要做议惰,因此您的所有工作需要在 10 毫秒內(nèi)完成。如果無法符合此預(yù)算乡恕,幀率將下降言询,并且內(nèi)容會在屏幕上抖動。 此現(xiàn)象通常稱為卡頓傲宜,會對用戶體驗(yàn)產(chǎn)生負(fù)面影響运杭。
像素管道
web頁面其實(shí)就是把html、css函卒、js中寫的邏輯辆憔,映射到屏幕上的像素,主要包括一下五個主要區(qū)域
-
JavaScript
报嵌。一般來說虱咧,我們會使用 JavaScript 來實(shí)現(xiàn)一些視覺變化的效果。比如用 jQuery 的 animate 函數(shù)做一個動畫锚国、對一個數(shù)據(jù)集進(jìn)行排序或者往頁面里添加一些 DOM 元素等腕巡。當(dāng)然,除了 JavaScript血筑,還有其他一些常用方法也可以實(shí)現(xiàn)視覺變化效果绘沉,比如:CSS Animations、Transitions 和 Web Animation API豺总。 -
樣式計算
车伞。此過程是根據(jù)匹配選擇器(例如 .headline 或 .nav > .nav__item)計算出哪些元素應(yīng)用哪些 CSS 規(guī)則的過程。從中知道規(guī)則之后喻喳,將應(yīng)用規(guī)則并計算每個元素的最終樣式另玖。 -
布局
。在知道對一個元素應(yīng)用哪些規(guī)則之后沸枯,瀏覽器即可開始計算它要占據(jù)的空間大小及其在屏幕的位置。網(wǎng)頁的布局模式意味著一個元素可能影響其他元素赂弓,例如 <body> 元素的寬度一般會影響其子元素的寬度以及樹中各處的節(jié)點(diǎn)绑榴,因此對于瀏覽器來說,布局過程是經(jīng)常發(fā)生的盈魁。 -
繪制
翔怎。繪制是填充像素的過程。它涉及繪出文本、顏色赤套、圖像飘痛、邊框和陰影,基本上包括元素的每個可視部分容握。繪制一般是在多個表面(通常稱為層)上完成的宣脉。 -
合成
。由于頁面的各部分可能被繪制到多層剔氏,由此它們需要按正確順序繪制到屏幕上塑猖,以便正確渲染頁面。對于與另一元素重疊的元素來說谈跛,這點(diǎn)特別重要羊苟,因?yàn)橐粋€錯誤可能使一個元素錯誤地出現(xiàn)在另一個元素的上層。
整個繪制過程中感憾,頁面都有機(jī)會產(chǎn)生卡頓蜡励,因?yàn)槲覀儽仨毐WC代碼觸發(fā)鏈路上的那部分邏輯
在網(wǎng)頁合成的過程中,也許聽過“柵格化”阻桅,這是因?yàn)槔L制過程中分為兩個步驟
- 創(chuàng)建繪圖調(diào)用的列表
- 填充像素(柵格化)
不一定每一幀都是會經(jīng)過這個管道的每一個部分凉倚,實(shí)際上不管是使用js,css鳍刷,還是網(wǎng)絡(luò)動畫占遥,在實(shí)現(xiàn)視覺變化時,管道針對指定幀運(yùn)行通常有三種方式
:
1. JS/CSS >樣式 > 布局 > 繪制 > 合成
如果您修改元素的“l(fā)ayout”屬性输瓜,也就是改變了元素的幾何屬性(例如寬度瓦胎、高度、左側(cè)或頂部位置等)尤揣,那么瀏覽器將必須檢查所有其他元素搔啊,然后“自動重排”頁面。任何受影響的部分都需要重新繪制北戏,而且最終繪制的元素需進(jìn)行合成负芋。
2. JS / CSS > 樣式 > 繪制 > 合成
如果您修改“paint only”屬性(例如背景圖片、文字顏色或陰影等)嗜愈,即不會影響頁面布局的屬性旧蛾,則瀏覽器會跳過布局,但仍將執(zhí)行繪制蠕嫁。
3. JS / CSS > 樣式 > 合成
如果您更改一個既不要布局也不要繪制的屬性锨天,則瀏覽器將跳到只執(zhí)行合成。
這個最后的版本開銷最小剃毒,最適合于應(yīng)用生命周期中的高壓力點(diǎn)病袄,例如動畫或滾動搂赋。
性能在多數(shù)情況下,我們必須和瀏覽器配合益缠,也不是跟他對著干脑奠,值得注意的是,上面列出的各管道工作幅慌,在計算開銷上有所不同宋欺,一些任務(wù)比其他任務(wù)開銷要大的多。
接下來欠痴,持續(xù)分析渲染過程中每一部分
JS執(zhí)行優(yōu)化
JS會經(jīng)常觸發(fā)視覺變化迄靠,有時是直接通過樣式操作,有時是會產(chǎn)生視覺變化的計算喇辽,例如搜索數(shù)據(jù)或?qū)⑵渑判蛘浦浚瑫r機(jī)不當(dāng)或長時間運(yùn)行的JS可能導(dǎo)致性能問題的常見原因。
- 對于動畫實(shí)現(xiàn)菩咨,避免使用setTimeout或setInterval吠式,請使用requestAnimateFrame
- 將長時間運(yùn)行的javascript從主線程移動Web Worker
- 請使用微任務(wù)來執(zhí)行對多個幀的DOM更改
- 使用 chrome Devtools的Timeline 和javascript分析器來評估對Javascript的影響
使用 requestAnimationFrame 來實(shí)現(xiàn)視覺變化
使用 setTimeout 或 setInterval 來執(zhí)行動畫之類的視覺變化,但這種做法的問題是抽米,回調(diào)將在幀中的某個時點(diǎn)運(yùn)行特占,可能剛好在未尾,而這可能經(jīng)常會使我們丟失幀云茸,導(dǎo)致卡頓
requestAnimationFrame 的基本思想讓頁面重繪的頻率與這個刷新頻率保持同步是目,比如顯示器屏幕刷新率為 60Hz,使用requestAnimationFrame API标捺,那么回調(diào)函數(shù)就每1000ms / 60 ≈ 16.7ms執(zhí)行一次懊纳;如果顯示器屏幕的刷新率為 75Hz,那么回調(diào)函數(shù)就每1000ms / 75 ≈ 13.3ms執(zhí)行一次亡容。
降低復(fù)雜性或使用 Web Worker
JavaScript 在瀏覽器的主線程上運(yùn)行嗤疯,恰好與樣式計算、布局以及許多情況下的繪制一起運(yùn)行闺兢。如果 JavaScript 運(yùn)行時間過長茂缚,就會阻塞這些其他工作,可能導(dǎo)致幀丟失屋谭。
因此脚囊,您要妥善處理 JavaScript 何時運(yùn)行以及運(yùn)行多久。例如桐磁,如果在滾動之類的動畫中悔耘,最好是想辦法使 JavaScript 保持在 3-4 毫秒的范圍內(nèi)。超過此范圍所意,就可能要占用太多時間淮逊。如果在空閑期間,則可以不必那么斤斤計較所占的時間扶踊。
在許多情況下泄鹏,可以將純計算工作移到 Web Worker,例如秧耗,如果它不需要 DOM 訪問權(quán)限备籽。數(shù)據(jù)操作或遍歷(例如排序或搜索)往往很適合這種模型,加載和模型生成也是如此分井。
并非所有工作都適合此模型:Web Worker 沒有 DOM 訪問權(quán)限车猬。如果您的工作必須在主線程上執(zhí)行,請考慮一種批量方法尺锚,將大型任務(wù)分割為微任務(wù)珠闰,每個微任務(wù)所占時間不超過幾毫秒,并且在每幀的 requestAnimationFrame 處理程序內(nèi)運(yùn)行瘫辩。
了解 JavaScript 的“幀稅”
在評估一個框架伏嗜、庫或您自己的代碼時,務(wù)必逐幀評估運(yùn)行 JavaScript 代碼的開銷伐厌。當(dāng)執(zhí)行性能關(guān)鍵的動畫工作(例如變換或滾動)時承绸,這點(diǎn)尤其重要。
測量 JavaScript 開銷和性能情況的最佳方法是使用 Chrome DevTools挣轨。通常军熏,您將獲得如下的簡單記錄:
如果發(fā)現(xiàn)有長時間運(yùn)行的 JavaScript,則可以在 DevTools 用戶界面的頂部啟用 JavaScript 分析器:
以這種方式分析 JavaScript 會產(chǎn)生開銷卷扮,因此一定只在想要更深入了解 JavaScript 運(yùn)行時特性時才啟用它荡澎。啟用此復(fù)選框后,現(xiàn)在可以執(zhí)行相同的操作画饥,您將獲得有關(guān) JavaScript 中調(diào)用了哪些函數(shù)的更多信息:
有了這些信息之后衔瓮,您可以評估 JavaScript 對應(yīng)用性能的影響,并開始找出和修正函數(shù)運(yùn)行時間過長的熱點(diǎn)抖甘。如前所述热鞍,應(yīng)當(dāng)設(shè)法移除長時間運(yùn)行的 JavaScript,或者若不能移除衔彻,則將其移到 Web Worker 中薇宠,騰出主線程繼續(xù)執(zhí)行其他任務(wù)。
避免微優(yōu)化 JavaScript
知道瀏覽器執(zhí)行一個函數(shù)版本比另一個函數(shù)要快 100 倍可能會很酷艰额,比如請求元素的offsetTop比計算getBoundingClientRect()要快澄港,但是,您在每幀調(diào)用這類函數(shù)的次數(shù)幾乎總是很少柄沮,因此回梧,把重點(diǎn)放在 JavaScript 性能的這個方面通常是白費(fèi)勁废岂。您一般只能節(jié)省零點(diǎn)幾毫秒的時間。