天天看點

【前端性能】高性能滾動 scroll 及頁面渲染優化

最近在研究頁面渲染及web動畫的性能問題,以及拜讀《CSS SECRET》(CSS揭秘)這本大作。

本文主要想談談頁面優化之滾動優化。

主要内容包括了為何需要優化滾動事件,滾動與頁面渲染的關系,節流與防抖,pointer-events:none 優化滾動。因為本文涉及了很多很多基礎,可以對照上面的知識點,選擇性跳到相應地方閱讀。

滾動優化其實也不僅僅指滾動(scroll 事件),還包括了例如 resize 這類會頻繁觸發的事件。簡單的看看:

輸出如下:

在綁定 scroll 、resize 這類事件時,當它發生時,它被觸發的頻次非常高,間隔很近。如果事件中涉及到大量的位置計算、DOM 操作、元素重繪等工作且這些工作無法在下一個 scroll 事件觸發前完成,就會造成浏覽器掉幀。加之使用者滑鼠滾動往往是連續的,就會持續觸發 scroll 事件導緻掉幀擴大、浏覽器 CPU 使用率增加、使用者體驗受到影響。

在滾動事件中綁定回調應用場景也非常多,在圖檔的懶加載、下滑自動加載資料、側邊浮動導航欄等中有着廣泛的應用。

當使用者浏覽網頁時,擁有平滑滾動經常是被忽視但卻是使用者體驗中至關重要的部分。當滾動表現正常時,使用者就會感覺應用十分流暢,令人愉悅,反之,笨重不自然卡頓的滾動,則會給使用者帶來極大不舒爽的感覺。

為什麼滾動事件需要去優化?因為它影響了性能。那它影響了什麼性能呢?額......這個就要從頁面性能問題由什麼決定說起。

我覺得搞技術一定要追本溯源,不要看到别人一篇文章說滾動事件會導緻卡頓并說了一堆解決方案優化技巧就如獲至寶奉為圭臬,我們需要的不是拿來主義而是批判主義,多去源頭看看。

從問題出發,一步一步尋找到最後,就很容易找到問題的症結所在,隻有這樣得出的解決方法才容易記住。

說教了一堆廢話,不喜歡的直接忽略哈,回到正題,要找到優化的入口就要知道問題出在哪裡,對于頁面優化而言,那麼我們就要知道頁面的渲染原理:

浏覽器渲染原理我在我上一篇文章裡也要詳細的講到,不過更多的是從動畫渲染的角度去講的:【Web動畫】CSS3 3D 行星運轉 && 浏覽器渲染原理 。

想了想,還是再簡單的描述下,我發現每次 review 這些知識點都有新的收獲,這次換一張圖,以 chrome 為例子,一個 Web 頁面的展示,簡單來說可以認為經曆了以下下幾個步驟:

【前端性能】高性能滾動 scroll 及頁面渲染優化

JavaScript:一般來說,我們會使用 JavaScript 來實作一些視覺變化的效果。比如做一個動畫或者往頁面裡添加一些 DOM 元素等。

Style:計算樣式,這個過程是根據 CSS 選擇器,對每個 DOM 元素比對對應的 CSS 樣式。這一步結束之後,就确定了每個 DOM 元素上該應用什麼 CSS 樣式規則。

Layout:布局,上一步确定了每個 DOM 元素的樣式規則,這一步就是具體計算每個 DOM 元素最終在螢幕上顯示的大小和位置。web 頁面中元素的布局是相對的,是以一個元素的布局發生變化,會關聯地引發其他元素的布局發生變化。比如,<body> 元素的寬度的變化會影響其子元素的寬度,其子元素寬度的變化也會繼續對其孫子元素産生影響。是以對于浏覽器來說,布局過程是經常發生的。

Paint:繪制,本質上就是填充像素的過程。包括繪制文字、顔色、圖像、邊框和陰影等,也就是一個 DOM 元素所有的可視效果。一般來說,這個繪制過程是在多個層上完成的。

Composite:渲染層合并,由上一步可知,對頁面中 DOM 元素的繪制是在多個層上進行的。在每個層上完成繪制過程之後,浏覽器會将所有層按照合理的順序合并成一個圖層,然後顯示在螢幕上。對于有位置重疊的元素的頁面,這個過程尤其重要,因為一旦圖層的合并順序出錯,将會導緻元素顯示異常。

這裡又涉及了層(GraphicsLayer)的概念,GraphicsLayer 層是作為紋理(texture)上傳給 GPU 的,現在經常能看到說 GPU 硬體加速,就和所謂的層的概念密切相關。但是和本文的滾動優化相關性不大,有興趣深入了解的可以自行 google 更多。

簡單來說,網頁生成的時候,至少會渲染(Layout+Paint)一次。使用者通路的過程中,還會不斷重新的重排(reflow)和重繪(repaint)。

其中,使用者 scroll 和 resize 行為(即是滑動頁面和改變視窗大小)會導緻頁面不斷的重新渲染。

當你滾動頁面時,浏覽器可能會需要繪制這些層(有時也被稱為合成層)裡的一些像素。通過元素分組,當某個層的内容改變時,我們隻需要更新該層的結構,并僅僅重繪和栅格化渲染層結構裡變化的那一部分,而無需完全重繪。顯然,如果當你滾動時,像視差網站(戳我看看)這樣有東西在移動時,有可能在多層導緻大面積的内容調整,這會導緻大量的繪制工作。

scroll 事件本身會觸發頁面的重新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 是以事件的 handler 内部不應該有複雜操作,例如 DOM 操作就不應該放在事件進行中。

針對此類高頻度觸發事件問題(例如頁面 scroll ,螢幕 resize,監聽使用者輸入等),下面介紹兩種常用的解決方法,防抖和節流。

防抖技術即是可以把多個順序地調用合并成一次,也就是在一定時間内,規定事件被觸發的次數。

通俗一點來說,看看下面這個簡化的例子:

上面簡單的防抖的例子可以拿到浏覽器下試一下,大概功能就是如果 500ms 内沒有連續觸發兩次 scroll 事件,那麼才會觸發我們真正想在 scroll 事件中觸發的函數。

上面的示例可以更好的封裝一下:

防抖函數确實不錯,但是也存在問題,譬如圖檔的懶加載,我希望在下滑過程中圖檔不斷的被加載出來,而不是隻有當我停止下滑時候,圖檔才被加載出來。又或者下滑時候的資料的 ajax 請求加載也是同理。

這個時候,我們希望即使頁面在不斷被滾動,但是滾動 handler 也可以以一定的頻率被觸發(譬如 250ms 觸發一次),這類場景,就要用到另一種技巧,稱為節流函數(throttling)。

節流函數,隻允許一個函數在 X 毫秒内執行一次。

與防抖相比,節流函數最主要的不同在于它保證在 X 毫秒内至少執行一次我們希望觸發的事件 handler。

與防抖相比,節流函數多了一個 mustRun 屬性,代表 mustRun 毫秒内,必然會觸發一次 handler ,同樣是利用定時器,看看簡單的示例:

上面簡單的節流函數的例子可以拿到浏覽器下試一下,大概功能就是如果在一段時間内 scroll 觸發的間隔一直短于 500ms ,那麼能保證事件我們希望調用的 handler 至少在 1000ms 内會觸發一次。

上面介紹的抖動與節流實作的方式都是借助了定時器 setTimeout ,但是如果頁面隻需要相容高版本浏覽器或應用在移動端,又或者頁面需要追求高精度的效果,那麼可以使用浏覽器的原生方法 rAF(requestAnimationFrame)。

window.requestAnimationFrame() 這個方法是用來在頁面重繪之前,通知浏覽器調用一個指定的函數。這個方法接受一個函數為參,該函數會在重繪前調用。

rAF 常用于 web 動畫的制作,用于準确控制頁面的幀重新整理渲染,讓動畫效果更加流暢,當然它的作用不僅僅局限于動畫制作,我們可以利用它的特性将它視為一個定時器。(當然它不是定時器)

通常來說,rAF 被調用的頻率是每秒 60 次,也就是 1000/60 ,觸發頻率大概是 16.7ms 。(當執行複雜操作時,當它發現無法維持 60fps 的頻率時,它會把頻率降低到 30fps 來保持幀數的穩定。)

簡單而言,使用 requestAnimationFrame 來觸發滾動事件,相當于上面的:

簡單的示例如下:

上面簡單的使用 rAF 的例子可以拿到浏覽器下試一下,大概功能就是在滾動的過程中,保持以 16.7ms 的頻率觸發事件 handler。

使用 requestAnimationFrame 優缺點并存,首先我們不得不考慮它的相容問題,其次因為它隻能實作以 16.7ms 的頻率來觸發,代表它的可調節性十分差。但是相比 throttle(func, xx, 16.7) ,用于更複雜的場景時,rAF 可能效果更佳,性能更好。

總結一下 

防抖動:防抖技術即是可以把多個順序地調用合并成一次,也就是在一定時間内,規定事件被觸發的次數。

節流函數:隻允許一個函數在 X 毫秒内執行一次,隻有當上一次函數執行後過了你規定的時間間隔,才能進行下一次該函數的調用。

rAF:16.7ms 觸發一次 handler,降低了可控性,但是提升了性能和精确度。

上面介紹的方法都是如何去優化 scroll 事件的觸發,避免 scroll 事件過度消耗資源的。

但是從本質上而言,我們應該盡量去精簡 scroll 事件的 handler ,将一些變量的初始化、不依賴于滾動位置變化的計算等都應當在 scroll 事件外提前就緒。

建議如下:

【前端性能】高性能滾動 scroll 及頁面渲染優化

輸入事件處理函數,比如 scroll / touch 事件的處理,都會在 requestAnimationFrame 之前被調用執行。

是以,如果你在 scroll 事件的處理函數中做了修改樣式屬性的操作,那麼這些操作會被浏覽器暫存起來。然後在調用 requestAnimationFrame 的時候,如果你在一開始做了讀取樣式屬性的操作,那麼這将會導緻觸發浏覽器的強制同步布局。

大部分人可能都不認識這個屬性,嗯,那麼它是幹什麼用的呢?

pointer-events 是一個 CSS 屬性,可以有多個不同的值,屬性的一部分值僅僅與 SVG 有關聯,這裡我們隻關注 pointer-events: none 的情況,大概的意思就是禁止滑鼠行為,應用了該屬性後,譬如滑鼠點選,hover 等功能都将失效,即是元素不會成為滑鼠事件的 target。

可以就近 F12 打開開發者工具面闆,給 <body> 标簽添加上 pointer-events: none 樣式,然後在頁面上感受下效果,發現所有滑鼠事件都被禁止了。

那麼它有什麼用呢?

pointer-events: none 可用來提高滾動時的幀頻。的确,當滾動時,滑鼠懸停在某些元素上,則觸發其上的 hover 效果,然而這些影響通常不被使用者注意,并多半導緻滾動出現問題。對 body 元素應用 pointer-events: none ,禁用了包括 hover 在内的滑鼠事件,進而提高滾動性能。

大概的做法就是在頁面滾動的時候, 給 <body> 添加上 .disable-hover 樣式,那麼在滾動停止之前, 所有滑鼠事件都将被禁止。當滾動結束之後,再移除該屬性。

可以檢視這個 demo 頁面。

上面說 pointer-events: none 可用來提高滾動時的幀頻 的這段話摘自 pointer-events-MDN ,還專門有文章講解過這個技術:

使用pointer-events:none實作60fps滾動 。

這就完了嗎?沒有,張鑫旭有一篇專門的文章,用來探讨 pointer-events: none 是否真的能夠加速滾動性能,并提出了自己的質疑:

pointer-events:none提高頁面滾動時候的繪制性能?

結論見仁見智,使用 pointer-events: none 的場合要依據業務本身來定奪,拒絕拿來主義,多去源頭看看,動手實踐一番再做定奪。

其他參考文獻(都是好文章,值得一讀):

執行個體解析防抖動(Debouncing)和節流閥(Throttling)

無線性能優化:Composite

Javascript高性能動畫與頁面渲染

Google Developers--渲染性能

Web高性能動畫

到此本文結束,如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,請點下推薦,寫文章不容易。