特定網頁效能問題改善的筆記,這篇主要談 Debounce 和 Throttle,enjoy it!
一、背景簡介
各版本的瀏覽器實作時,為了確保滑鼠移動、滾動、改變視窗大小 (mousemove, scroll, resize) 等事件能夠及時回應維持使用者體驗,觸發的頻率會比較高。也就是說,使用者在一個正常的操作中,有可能在短時間內觸發非常多次事件處理器 (event handler)。
關於這個優化的一個知名的事件是: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/
四、簡單用法範例
<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
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