2017年12月15日 星期五

網頁 DOM 事件的效能優化:Debounce 和 Throttle

 

特定網頁效能問題改善的筆記,這篇主要談 Debounce 和 Throttle,enjoy it!




一、背景簡介


各版本的瀏覽器實作時,為了確保滑鼠移動、滾動、改變視窗大小 (mousemove, scroll, resize) 等事件能夠及時回應維持使用者體驗,觸發的頻率會比較高。也就是說,使用者在一個正常的操作中,有可能在短時間內觸發非常多次事件處理器 (event handler)。

如果為這些短時間內觸發非常多次的事件處理器綁定一些 DOM 節點操作,就會引發大量消耗效能的 DOM 計算,不斷重新計算 DOM 元素的絕對位置,造成頁面緩慢,甚至瀏覽器直接崩潰。

關於這個優化的一個知名的事件是:2011 年時,Twitter 頁面 scroll 時會變得緩慢:

John Resig - Learning from Twitter
https://johnresig.com/blog/learning-from-twitter/


當時用的解法相對簡單(設定計算量大的事件函數每 250 ms 執行一次),而目前已有眾多可行的解法處理這個問題,較常見的解法有 throttling 和 debouncing 等等。




二、解決方法


開始之前,先看一下兩種方法 debounce 和 throttle 的視覺化模擬,直觀就能感受兩種方法的區別。

debounce and throttle




1. 去抖動 debounce


模仿電器開關處理的方法,把多個訊號合併成一個訊號。讓一個函式在連續觸發時只執行一次。一個常見的用法是使用者連續輸入基本資訊後,才觸發事件處理器進行格式確認。

大部分的實作會加上 immediate 參數,意思是多個訊號合併成一個訊號時,是要在最開始時執行 (immediate=true) 或是最後執行。

function debounce(func, delay) {
  var timer = null;
  return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      func.apply(context, args)
    }, delay);
  }
}


程式碼很直觀,先設一個計時器 (timer),保存當下脈絡後 (context, args),只要太早進來 (小於 delay) 就會重置計時器,直到成功執行 setTimeout 內的函式後結束。

注意這裡 debounce 回傳的是一個閉包 (closure),是 js 的一個重要特性,不這樣寫的話 timer 就必須是全域變數,以防止每次呼叫 timer 都被重置產生錯誤。



2. 函數節流 throttle


函數節流讓一個函數不要執行得太頻繁,也就是控制函數最高呼叫頻率,減少一些過快的呼叫來節流。一個常見的用法是減少 scroll 的觸發頻率,因為 scroll 常常綁定一些消耗資源的 render 的事件。

function throttle(func, threshhold) {
  var last, timer;
  if (threshhold) threshhold = 250;
  return function () {
    var context = this
    var args = arguments
    var now = +new Date()
    if (last && now < last + threshhold) {
      clearTimeout(timer)
      timer = setTimeout(function () {
        last = now
        func.apply(context, args)
      }, threshhold)
    } else {
      last = now
      fn.apply(context, args)
    }
  }
}

與 debouncing 的程式邏輯相似,只多了一個時間間隔的判斷。



補充:requestAnimationFrame (rAF)


requestAnimationFrame 是一個瀏覽器原生的 API,不是一種優化方法論,因此可以和上面兩種方法一起使用,rAF 大致可以視為 16 ms 的 throttle,但其內部的機制是由瀏覽器直接控制,因此有更好的精準度,常用於與動畫有關的控制。

  • 優點:原生,易維護執行,精度高。
  • 缺點:不支援 IE 9,後端的 node.js 不支援,太頻繁的呼叫 rAF 仍需要自製 throttle 調節。




三、Debounce 和 Throttle 的 library


俗話說「不要自己製造輪子」,建議直接使用 underscore 或 Lodash 的實作比較穩定。

Lodash
https://lodash.com/

Underscore
http://underscorejs.org/




四、簡單用法範例


確認使用者輸入是否符合格式 (debounce)




<head>
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js"></script>
</head>

<body>
 <h3>Enter yout email address:</h3><input/>
 <p class="email"><span class="text"></span></p>
</body>

<script>

$('input').on('keyup', _.debounce(
 function(e) {
  var regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  if (regex.test(this.value)) {
   $('.email .text').text("valid email").css("color", "green")
  } else {
   $('.email .text').text("invalid email").css("color", "red")
  }
 },500)
)

</script>





References


David Corbacho - Debouncing and Throttling Explained Through Examples
https://css-tricks.com/debouncing-throttling-explained-examples/

中文版:實例解析防抖動(Debouncing)和節流閥(Throttling)
https://jinlong.github.io/2016/04/24/Debouncing-and-Throttling-Explained-Through-Examples/

Paul Lewis - Leaner, Meaner, Faster Animations with requestAnimationFrame
https://www.html5rocks.com/en/tutorials/speed/animations/

Paul Lewis - Debounce Your Input Handlers
https://developers.google.com/web/fundamentals/performance/rendering/debounce-your-input-handlers?hl=en

drupal motion - Debounce and Throttle: a visual explanation
http://drupalmotion.com/article/debounce-and-throttle-visual-explanation

Debounce 和 Throttle 的原理及實現
http://hackll.com/2015/11/19/debounce-and-throttle/

風雨後見彩虹 - js 的函數節流(throttle)
http://www.cnblogs.com/moqiutao/p/6875955.html

aFarkasn - scroll-perf,一些 Debounce 和 Throttle 範例
https://github.com/aFarkas/scroll-perf

google WebFundamentals - Improve "Debounce your scroll handlers" and establish better best practice for "Avoid layout thrashing",debouncing 的一些觀點討論。
https://github.com/google/WebFundamentals/issues/2227

wilsonpage - fastdom, batching DOM read/write operations
https://github.com/wilsonpage/fastdom






技術提供:Blogger.