[1]概述
[2]scrollIntoView
[3]scroll-behavior
[4]sticky
[5]防抖和節流
[6]IntersectionObserver
[7]連鎖滾動
[8]慣性滾動
[9]passive
前面的話
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
好的代碼像粥一樣,都是用時間熬出來的