最近在研究頁面渲染及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 頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。例如,6c04bd5ca3fcae76e30b72ad730ca86d 元素的寬度的變化會影響其子元素的寬度,其子元素寬度的變化也會繼續對其孫子元素產生影響。因此對於瀏覽器來說,佈局過程是經常發生的。 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 / touch 事件的处理,都会在 requestAnimationFrame 之前被调用执行。 因此,如果你在 scroll 事件的处理函数中做了修改样式属性的操作,那么这些操作会被浏览器暂存起来。然后在调用 requestAnimationFrame 的时候,如果你在一开始做了读取样式属性的操作,那么这将会导致触发浏览器的强制同步布局。 大部分人可能都不认识这个属性,嗯,那么它是干什么用的呢? pointer-events 是一个 CSS 属性,可以有多个不同的值,属性的一部分值仅仅与 SVG 有关联,这里我们只关注 pointer-events: 的情况,大概的意思就是禁止鼠标行为,应用了该属性后,譬如鼠标点击,hover 等功能都将失效,即是元素不会成为鼠标事件的 target。 可以就近 F12 打开开发者工具面板,给 6c04bd5ca3fcae76e30b72ad730ca86d 标签添加上 pointer-events: 那么它有什么用呢? pointer-events: 可用来提高滚动时的帧频。的确,当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,然而这些影响通常不被用户注意,并多半导致滚动出现问题。对 body 元素应用 pointer-events: ,禁用了包括 hover 在内的鼠标事件,从而提高滚动性能。 大概的做法就是在页面滚动的时候, 给 6c04bd5ca3fcae76e30b72ad730ca86d 添加上 .disable-hover 样式,那么在滚动停止之前, 所有鼠标事件都将被禁止。当滚动结束之后,再移除该属性。 可以查看这个 demo 页面。 上面说 pointer-events: 段话摘自 pointer-events-MDN ,还专门有文章讲解过这个技术: 使用pointer-events:none实现60fps滚动 。 这就完了吗?没有,张鑫旭有一篇专门的文章,用来探讨 pointer-events:
结论见仁见智,使用 pointer-events:
实例解析防抖动(Debouncing)和节流阀(Throttling) 无线性能优化:Composite Javascript高性能动画与页面渲染 Google Developers--渲染性能 Web高性能动画
防手震(Debouncing)與節流(Throttling)
防抖(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);
// 防抖动函数
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)
// 简单的节流函数
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));
使用 rAF(requestAnimationFrame)触发滚动事件
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);
简化 scroll 内的操作
避免在scroll 事件中修改样式属性 / 将样式操作从 scroll 事件中剥离
滑动过程中尝试使用 pointer-events: 禁止鼠标事件
.disable-hover {
pointer-events: none;
}
以上是高效能滾動 scroll 及頁面渲染優化的詳細內容。更多資訊請關注PHP中文網其他相關文章!