前面的話
scroll 、resize這類事件被觸發的頻次非常高,間隔很近。如果事件中涉及到大量的位置計算、DOM 操作、元素重繪等工作,且這些工作無法在下一個 scroll 事件觸發前完成,就會造成瀏覽器掉幀。加之用戶鼠標滾動往往是連續的,就會持續觸發 scroll 事件導致掉幀擴大、瀏覽器 CPU 使用率增加、用戶體驗受到影響。本文將詳細介紹滾動優化
概述
在滾動事件中綁定回調的應用場景非常多,如圖片的懶加載、下滑自動加載數據、側邊浮動導航欄等,用戶瀏覽網頁時,擁有平滑滾動經常是被忽視但卻是用戶體驗中至關重要的部分
網頁生成的時候,至少會渲染(Layout+Paint)一次。用戶訪問的過程中,還會不斷重新的重排(reflow)和重繪(repaint)。其中,用戶 scroll 和 resize 行為(即是滑動頁面和改變窗口大小)會導致頁面不斷的重新渲染
滾動頁面時,瀏覽器可能會需要繪制這些層里的一些像素。通過元素分組,當某個層的內容改變時,只需要更新該層的結構,並僅僅重繪和柵格化渲染層結構里變化的那一部分,而無需完全重繪。顯然,如果滾動時,像視差網站這樣有東西在移動時,有可能在多層導致大面積的內容調整,這會導致大量的繪制工作
scrollIntoView
元素的scrollIntoView()方法支持一傳入一個options,設置為smooth時,即可實現平滑滾動
ele.scrollIntoView({ behavior: 'smooth' })
但是,該效果的兼容性不太好,移動端和IE都不支持
<style> ul{ padding: 0; margin: 0; list-style: none; } .con{ width: 260px; display: flex; justify-content:space-around; line-height: 30px; background: #333; color: #fff; } .con li { cursor: pointer; } .showBox{ width: 260px; height: 100px; overflow: hidden; } .show li { height: 100px; text-align: center; line-height: 100px; } </style> <ul class="con" id="con"> <li>HTML</li> <li>CSS</li> <li>JS</li> </ul> <div class="showBox"> <ul class="show" id="show"> <li style="background: lightgreen;">HTML</li> <li style="background: lightblue;">CSS</li> <li style="background: pink;">JS</li> </ul> </div> <script> const con = document.getElementById('con') const show = document.getElementById('show') const showChildren = show.children Array.prototype.slice.call(con.children).map((item, index) => item.scrollTarget = showChildren[index]) con.addEventListener('click', e => { const { target} = e if (target.nodeName === 'LI') { target.scrollTarget.scrollIntoView({ behavior: 'smooth' }) } }) </script>
效果如下所示
scroll-behavior
scroll-behavior是一個新的CSS屬性,用簡單的一行代碼改變整個頁面滾動的行為
html { scroll-behavior: smooth; }
同樣地,該屬性的兼容性不太好,移動端和IE都不支持
<style> body { margin: 0; } ul{ padding: 0; margin: 0; list-style: none; } a { text-decoration: none; color: inherit; } .con{ width: 260px; display: flex; justify-content:space-around; line-height: 30px; background: #333; color: #fff; } .con li { cursor: pointer; } .showBox{ width: 260px; height: 100px; overflow: hidden; scroll-behavior: smooth; } .show li { height: 100px; text-align: center; line-height: 100px; } </style> <ul class="con" id="con"> <li><a href="#html">HTML</a></li> <li><a href="#css">CSS</a></li> <li><a href="#js">JS</a></li> </ul> <div class="showBox"> <ul class="show" id="show"> <li style="background: lightgreen;" id="html">HTML</li> <li style="background: lightblue;" id="css">CSS</li> <li style="background: pink;" id="js">JS</li> </ul> </div>
效果如下所示
sticky
以前,要實現一個“粘性”元素需要編寫復雜的滾動處理函數去計算元素的大小。該函數較難處理元素在“黏住”與“不黏住”之間微小的延遲,通常會導致元素抖動的出現
不久之前,CSS 實現了 position: sticky 屬性。只需通過指定(某方向上的)偏移量即可實現想要的效果
.element {
position: sticky;
top: 50px;
}
android4.4以下及IE瀏覽器不支持,IOS下需添加-webkit-前綴,下面是一個demo實現
<style> body { margin: 0; } main { height: 3000px; } .show{ position: sticky; top: 10px; width: 260px; height: 100px; margin-top: 100px; background: lightgreen; } </style> <main> <div class="show" id="show"></div> </main> </div>
效果如下
防抖和節流
scroll 事件本身會觸發頁面的重新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 因此事件的 handler 內部不應該有復雜操作,例如 DOM 操作就不應該放在事件處理中
針對此類高頻度觸發事件問題(例如頁面 scroll ,屏幕 resize,監聽用戶輸入等),下面介紹兩種常用的解決方法,防抖和節流
【防抖debouncing】
函數防抖,字面上來說,是利用函數來防止抖動。在執行觸發事件的情況下,元素的位置或尺寸屬性快速地發生變化,造成頁面回流,出現元素抖動的現象。通過函數防抖,使得元素的位置或尺寸屬性延遲變化,從而減少頁面回流
const debounce = (fn, wait=30) =>{ return function() { clearTimeout(fn.timer) fn.timer = setTimeout(fn.bind(this, ...arguments), wait) } }
【節流throttle】
函數節流,即限制函數的執行頻率,在持續觸發事件的情況下,間斷地執行函數
const throttle = (fn, wait=100) =>{ return function() { if(fn.timer) return fn.timer = setTimeout(() => { fn.apply(this, arguments) fn.timer = null }, wait) } }
IntersectionObserver
需要實現圖片懶加載或者無限滾動時,需要確定元素是否出現在視窗中。這可以在事件監聽器中處理,最常見的解決方案是使用 element.getBoundingClientRect() :
window.addEventListener('scroll', () => { const rect = elem.getBoundingClientRect(); const inViewport = rect.bottom > 0 && rect.right > 0 && rect.left < window.innerWidth && rect.top < window.innerHeight; });
上述代碼的問題在於每次調用 getBoundingClientRect 時都會觸發回流,嚴重地影響了性能。在事件處理函數中調用getBoundingClientRect尤為糟糕,就算使用了函數節流的技巧也可能對性能沒多大幫助
現在可以通過使用 Intersection Observer 這一 API 來解決問題。它允許追蹤目標元素與其祖先元素或視窗的交叉狀態。此外,盡管只有一部分元素出現在視窗中,哪怕只有一像素,也可以選擇觸發回調函數:
const observer = new IntersectionObserver(callback, options); observer.observe(element)
移動端及IE瀏覽器不支持同,不過可以使用polyfill
連鎖滾動
當用戶滾動到(彈框或下拉列表)末尾(后再繼續滾動時),整個頁面都會開始滾動

當滾動元素到達底部時,可以通過改變頁面的 overflow 屬性或在滾動元素的滾動事件處理函數中取消默認行為來解決這問題
function handleOverscroll(event) { const delta = -event.deltaY; if (delta < 0 && elem.offsetHeight - delta > elem.scrollHeight - elem.scrollTop) { elem.scrollTop = elem.scrollHeight; event.preventDefault(); return false; } if (delta > elem.scrollTop) { elem.scrollTop = 0; event.preventDefault(); return false; } return true; }
不幸的是,這個解決方案不太可靠。同時可能對頁面性能產生負面影響,過度滾動對移動端的影響尤為嚴重
CSS 通過 overscroll-behavior 這個新屬性解決問題。它通過控制元素滾動到盡頭時的行為來解決下拉刷新與連鎖滾動所帶來的問題,它的屬性值中也包含針對不同平台特殊值:安卓的 glow 與 蘋果系統中的 rubber band
現在,上面 GIF 中的問題,在 Chrome、Opera 或 Firefox 中可以通過以下一行代碼來解決:
.element { overscroll-behavior: contain; }
該屬性只有最新的chrome和firefox瀏覽器支持
慣性滾動
蘋果公司開創了“慣性”滾動並擁有它的專利 。它迅速地成為了用戶交互的標准並且我們對此已習以為常
這里有一個 CSS 的解決方案,但看起來更像是個 hack
.element { -webkit-overflow-scrolling: touch; }
首先,它只能在支持webkit前綴的瀏覽器上才能工作。其次,它只適用於觸屏設備。最后,如果瀏覽器不支持的話,你就這樣置之不理嗎?但無論如何,這總歸是一個解決方案
passive
瀏覽器雖然知道如何使得滾動變得平滑,但為確認滾動事件處理函數中是否執行了 Event.preventDefault() 以取消默認行為,有時仍可能需要花費500毫秒來等待事件處理函數執行完畢
即使是一個空的事件監聽器,從不取消任何行為,鑒於瀏覽器仍會期待 preventDefault 的調用,也會對性能造成負面影響
為了准確地告訴瀏覽器不必擔心事件處理函數中取消了默認行為,在 WHATWG 的 DOM 標准中存在着一個不太顯眼的特性能解決這問題。它就是Passive event listeners
IE瀏覽器、andriod4.4-、IOS9.3-不支持該特性
事件監聽函數新接受一個可選的對象作為參數,告訴瀏覽器當事件觸發時,事件處理函數永遠不會取消默認行為。當然,添加此參數后,在事件處理函數中調用 preventDefault 將不再產生效果
element.addEventListener('touchstart', e => { /* doSomething */ }, { passive: true });
針對不支持該參數的瀏覽器,可以使用polyfill
