滾動優化

前面的話

  scroll 、resize這類事件被觸發的頻次很是高,間隔很近。若是事件中涉及到大量的位置計算、DOM 操做、元素重繪等工做,且這些工做沒法在下一個 scroll 事件觸發前完成,就會形成瀏覽器掉幀。加之用戶鼠標滾動每每是連續的,就會持續觸發 scroll 事件致使掉幀擴大、瀏覽器 CPU 使用率增長、用戶體驗受到影響。本文將詳細介紹滾動優化css

 

概述

  在滾動事件中綁定回調的應用場景很是多,如圖片的懶加載、下滑自動加載數據、側邊浮動導航欄等,用戶瀏覽網頁時,擁有平滑滾動常常是被忽視但倒是用戶體驗中相當重要的部分html

  網頁生成的時候,至少會渲染(Layout+Paint)一次。用戶訪問的過程當中,還會不斷從新的重排(reflow)和重繪(repaint)。其中,用戶 scroll 和 resize 行爲(便是滑動頁面和改變窗口大小)會致使頁面不斷的從新渲染node

  滾動頁面時,瀏覽器可能會須要繪製這些層裏的一些像素。經過元素分組,當某個層的內容改變時,只須要更新該層的結構,並僅僅重繪和柵格化渲染層結構裏變化的那一部分,而無需徹底重繪。顯然,若是滾動時,像視差網站這樣有東西在移動時,有可能在多層致使大面積的內容調整,這會致使大量的繪製工做android

 

scrollIntoView

  元素的scrollIntoView()方法支持一傳入一個options,設置爲smooth時,便可實現平滑滾動web

ele.scrollIntoView({ behavior: 'smooth' })

  可是,該效果的兼容性不太好,移動端和IE都不支持chrome

<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屬性,用簡單的一行代碼改變整個頁面滾動的行爲app

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

 

連鎖滾動

  當用戶滾動到(彈框或下拉列表)末尾(後再繼續滾動時),整個頁面都會開始滾動

scroll

  當滾動元素到達底部時,能夠經過改變頁面的 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

相關文章
相關標籤/搜索