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

来源:http://www.cnblogs.com/coco1s/archive/2016/05/17/5499469.html
-Advertisement-
Play Games

最近在研究頁面渲染及web動畫的性能問題,以及拜讀《CSS SECRET》(CSS揭秘)這本大作。 本文主要想談談頁面優化之滾動優化。 主要內容包括了為何需要優化滾動事件,滾動與頁面渲染的關係,節流與防抖,pointer-events:none 優化滾動。因為本文涉及了很多很多基礎,是我自己學習記錄 ...


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

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

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

 

   滾動優化的由來

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

var i = 0;
window.addEventListener('scroll',function(){
	console.log(i++);
},false);

輸出如下:

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

在滾動事件中綁定回調應用場景也非常多,在圖片的懶載入、下滑自動載入數據、側邊浮動導航欄等中有著廣泛的應用。

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

 

   滾動與頁面渲染的關係

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

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

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

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

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

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

  • 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 行為(即是滑動頁面和改變視窗大小)會導致頁面不斷的重新渲染。

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

 

   防抖(Debouncing)和節流(Throttling)

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

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

防抖(Debouncing)

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

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

// 簡單的防抖動函數
function debounce(func, wait, immediate) {
	// 定時器變數
	var timeout;
	return function() {
		// 每次觸發 scroll handler 時先清除定時器
		clearTimeout(timeout);
		// 指定 xx ms 後觸發真正想進行的操作 handler
		timeout = setTimeout(func, wait);
	};
};

// 實際想綁定在 scroll 事件上的 handler
function realFunc(){
	console.log("Success");
}

// 採用了防抖動
window.addEventListener('scroll',debounce(realFunc,500));
// 沒採用防抖動
window.addEventListener('scroll',realFunc);

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

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

// 防抖動函數
function debounce(func, wait, immediate) {
	var timeout;
	return function() {
		var context = this, args = arguments;
		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};
		var callNow = immediate && !timeout;
		clearTimeout(timeout);
		timeout = setTimeout(later, wait);
		if (callNow) func.apply(context, args);
	};
};

var myEfficientFn = debounce(function() {
	// 滾動中的真正的操作
}, 250);

// 綁定監聽
window.addEventListener('resize', myEfficientFn);

節流(Throttling)

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

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

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

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

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

// 簡單的節流函數
function throttle(func, wait, mustRun) {
	var timeout,
		startTime = new Date();

	return function() {
		var context = this,
			args = arguments,
			curTime = new Date();

		clearTimeout(timeout);
		// 如果達到了規定的觸發時間間隔,觸發 handler
		if(curTime - startTime >= mustRun){
			func.apply(context,args);
			startTime = curTime;
		// 沒達到觸發間隔,重新設定定時器
		}else{
			timeout = setTimeout(func, wait);
		}
	};
};
// 實際想綁定在 scroll 事件上的 handler
function realFunc(){
	console.log("Success");
}
// 採用了節流函數
window.addEventListener('scroll',throttle(realFunc,500,1000));

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

 

   使用 rAF(requestAnimationFrame)觸發滾動事件

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

requestAnimationFrame

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

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

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

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

throttle(func, xx, 1000/60) //xx 代表 xx ms內不會重覆觸發事件 handler

簡單的示例如下:

var ticking = false; // rAF 觸發鎖

function onScroll(){
  if(!ticking) {
    requestAnimationFrame(realFunc);
    ticking = true;
  }
}

function realFunc(){
	// do something...
	console.log("Success");
	ticking = false;
}
// 滾動事件監聽
window.addEventListener('scroll', onScroll, false);

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

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

總結一下 

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

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

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

 

   簡化 scroll 內的操作

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

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

建議如下:

避免在scroll 事件中修改樣式屬性 / 將樣式操作從 scroll 事件中剝離

 

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

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

 

   滑動過程中嘗試使用 pointer-events: none 禁止滑鼠事件

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

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

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

那麼它有什麼用呢?

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

.disable-hover {
    pointer-events: none;
}

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

可以查看這個 demo 頁面。

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

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

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

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

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

 

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

 

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 狀態模式 允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了它的類。 狀態模式所涉及到的角色有: 環境(Context)角色,也成上下文:定義客戶端所感興趣的介面,並且保留一個具體狀態類的實例。這個具體狀態類的實例給出此環境對象的現有狀態。 抽象狀態(State)角色:定義一個介面,用以封裝 ...
  • 觀察者模式 定義了對象之間的一對多依賴,這樣一來,當一個對象狀態改變時,它的所有依賴者都會收到通知並自動更新。 觀察者模式中,分為推和拉兩種模式。 推模式,即主題對象向觀察者對象推送狀態值,不管觀察者對象是否需要,並且推送全部數據或者部分數據。 拉模式,即主題對象通知觀察者對象狀態值已改變,觀察者根 ...
  • 策略模式 定義了演算法族,分別封裝起來,讓他們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶。 說明: 1、可以動態地改變對象的行為; 2、各個策略演算法的平等性,各個策略演算法在實現上是相互獨立的,相互之間沒有任何依賴的(由此,策略模式也可以描述為“策略演算法是相同行為的不同實現”); 3、在運 ...
  • 裝飾者模式 動態的將責任附加到對象上。若要擴展功能,裝飾者模式提供了比繼承更有彈性的替代方案。 說明: 1、裝飾者和被裝飾者對象有相同的超類型; 2、可以用一個或者多個裝飾者包裝一個對象; 3、既然裝飾者和被裝飾者對象有相同的超類型,所以在任何需要原始對象(被裝飾者)的場合,可以用裝飾過的對象代替它 ...
  • WebSocket協議,是建立在TCP協議上的,而非HTTP協議。 如下: ws://127.0.0.1或wss://127.0.0.1就是WebSocket請求。 註:ws表示WebSocket協議,wss表示加密的WebSocket協議。 WebSocket的好處就是允許伺服器和客服端進行實時地 ...
  • Object.prototype.exist = function(){ if(typeof this !='undefined' && this.length>=1){ return true; } return false; }; 不解釋 Object.prototype.exist = fun ...
  • 問題 在node項目中,往往需要安裝一些依賴的包,通常我們採取全局安裝的方式,來減少一些包重覆安裝帶來的煩惱。 但是全局安裝後出現無法使用的情況,可能是你NODE_PATH沒有設置或者不正確造成的。 解決方案 那麼,什麼是NODE_PATH呢? NODE_PATH是node為模塊提供尋找路徑的一個環 ...
  • 在使用JQuery的Ajax從伺服器請求數據或者向伺服器發送數據時常常會遇到跨域無法請求的錯誤,常用的解決辦法就是在Ajax中使用JSONP。基於安全性考慮,瀏覽器會存在同源策略,然而<script/>標簽卻具有跨域訪問數據的能力,這就是JSONP工作的基本原理。有關同源策略以及什麼是JSONP,可 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...