最近在研究頁面渲染及web動畫的性能問題,以及拜讀《CSS SECRET》(CSS揭祕)這本大做。javascript
本文主要想談談頁面優化之滾動優化。css
主要內容包括了爲什麼須要優化滾動事件,滾動與頁面渲染的關係,節流與防抖,pointer-events:none 優化滾動。由於本文涉及了不少不少基礎,能夠對照上面的知識點,選擇性跳到相應地方閱讀。html
滾動優化其實也不只僅指滾動(scroll 事件),還包括了例如 resize 這類會頻繁觸發的事件。簡單的看看:java
var i = 0; window.addEventListener('scroll',function(){ console.log(i++); },false);
輸出以下:git
在綁定 scroll 、resize 這類事件時,當它發生時,它被觸發的頻次很是高,間隔很近。若是事件中涉及到大量的位置計算、DOM 操做、元素重繪等工做且這些工做沒法在下一個 scroll 事件觸發前完成,就會形成瀏覽器掉幀。加之用戶鼠標滾動每每是連續的,就會持續觸發 scroll 事件致使掉幀擴大、瀏覽器 CPU 使用率增長、用戶體驗受到影響。github
在滾動事件中綁定回調應用場景也很是多,在圖片的懶加載、下滑自動加載數據、側邊浮動導航欄等中有着普遍的應用。web
當用戶瀏覽網頁時,擁有平滑滾動常常是被忽視但倒是用戶體驗中相當重要的部分。當滾動表現正常時,用戶就會感受應用十分流暢,使人愉悅,反之,笨重不天然卡頓的滾動,則會給用戶帶來極大不舒爽的感受。ajax
爲何滾動事件須要去優化?由於它影響了性能。那它影響了什麼性能呢?額......這個就要從頁面性能問題由什麼決定提及。chrome
我以爲搞技術必定要追本溯源,不要看到別人一篇文章說滾動事件會致使卡頓並說了一堆解決方案優化技巧就如獲至寶奉爲圭臬,咱們須要的不是拿來主義而是批判主義,多去源頭看看。瀏覽器
從問題出發,一步一步尋找到最後,就很容易找到問題的癥結所在,只有這樣得出的解決方法才容易記住。
說教了一堆廢話,不喜歡的直接忽略哈,回到正題,要找到優化的入口就要知道問題出在哪裏,對於頁面優化而言,那麼咱們就要知道頁面的渲染原理:
瀏覽器渲染原理我在我上一篇文章裏也要詳細的講到,不過更多的是從動畫渲染的角度去講的:【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 行爲(便是滑動頁面和改變窗口大小)會致使頁面不斷的從新渲染。
當你滾動頁面時,瀏覽器可能會須要繪製這些層(有時也被稱爲合成層)裏的一些像素。經過元素分組,當某個層的內容改變時,咱們只須要更新該層的結構,並僅僅重繪和柵格化渲染層結構裏變化的那一部分,而無需徹底重繪。顯然,若是當你滾動時,像視差網站(戳我看看)這樣有東西在移動時,有可能在多層致使大面積的內容調整,這會致使大量的繪製工做。
scroll 事件自己會觸發頁面的從新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 所以事件的 handler 內部不該該有複雜操做,例如 DOM 操做就不該該放在事件處理中。
針對此類高頻度觸發事件問題(例如頁面 scroll ,屏幕 resize,監聽用戶輸入等),下面介紹兩種經常使用的解決方法,防抖和節流。
防抖技術便是能夠把多個順序地調用合併成一次,也就是在必定時間內,規定事件被觸發的次數。
通俗一點來講,看看下面這個簡化的例子:
// 簡單的防抖動函數 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);
防抖函數確實不錯,可是也存在問題,譬如圖片的懶加載,我但願在下滑過程當中圖片不斷的被加載出來,而不是隻有當我中止下滑時候,圖片才被加載出來。又或者下滑時候的數據的 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 內會觸發一次。
上面介紹的抖動與節流實現的方式都是藉助了定時器 setTimeout ,可是若是頁面只須要兼容高版本瀏覽器或應用在移動端,又或者頁面須要追求高精度的效果,那麼可使用瀏覽器的原生方法 rAF(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 事件的 handler ,將一些變量的初始化、不依賴於滾動位置變化的計算等都應當在 scroll 事件外提早就緒。
建議以下:
輸入事件處理函數,好比 scroll / touch 事件的處理,都會在 requestAnimationFrame 以前被調用執行。
所以,若是你在 scroll 事件的處理函數中作了修改樣式屬性的操做,那麼這些操做會被瀏覽器暫存起來。而後在調用 requestAnimationFrame 的時候,若是你在一開始作了讀取樣式屬性的操做,那麼這將會致使觸發瀏覽器的強制同步佈局。
大部分人可能都不認識這個屬性,嗯,那麼它是幹什麼用的呢?
pointer-events 是一個 CSS 屬性,能夠有多個不一樣的值,屬性的一部分值僅僅與 SVG 有關聯,這裏咱們只關注 pointer-events: 的狀況,大概的意思就是禁止鼠標行爲,應用了該屬性後,譬如鼠標點擊,hover 等功能都將失效,便是元素不會成爲鼠標事件的 target。
能夠就近 F12 打開開發者工具面板,給 <body> 標籤添加上 pointer-events:
那麼它有什麼用呢?
pointer-events: 可用來提升滾動時的幀頻。的確,當滾動時,鼠標懸停在某些元素上,則觸發其上的 hover 效果,然而這些影響一般不被用戶注意,並多半致使滾動出現問題。對 body 元素應用 pointer-events: ,禁用了包括 hover 在內的鼠標事件,從而提升滾動性能。
.disable-hover { pointer-events: none; }
大概的作法就是在頁面滾動的時候, 給 <body> 添加上 .disable-hover 樣式,那麼在滾動中止以前, 全部鼠標事件都將被禁止。當滾動結束以後,再移除該屬性。
能夠查看這個 demo 頁面。
上面說 pointer-events: pointer-events-MDN ,還專門有文章講解過這個技術: 段話摘自
使用pointer-events:none實現60fps滾動 。
這就完了嗎?沒有,張鑫旭有一篇專門的文章,用來探討 pointer-events:
結論見仁見智,使用 pointer-events:
到此本文結束,若是還有什麼疑問或者建議,能夠多多交流,原創文章,文筆有限,才疏學淺,文中如有不正之處,萬望告知。
若是本文對你有幫助,請點下推薦,寫文章不容易。