JavaScript — 效能與最佳化
防抖與節流(debounce / throttle)
簡介
在前端開發中,我們常常需要處理大量、頻繁觸發的事件,例如 scroll、resize、keyup、mousemove 等。如果直接在每一次事件發生時都執行相同的程式碼,瀏覽器很快就會因為過度重繪或大量運算而卡頓,使用者體驗會急速下降。
防抖(debounce) 與 節流(throttle) 兩種技術,就是為了在高頻率事件中控制函式呼叫頻率,降低不必要的計算,提升頁面效能與回應速度。掌握這兩個概念,不僅能讓 UI 更流暢,也能減少伺服器端的請求量,對 SEO、使用者留存都有正面影響。
核心概念
1. 防抖(Debounce)
防抖的核心思想是「在事件最後一次觸發後,延遲一定時間才執行」;如果在延遲期間再次觸發,計時器會被重新啟動。這樣可以保證函式只會在使用者停止操作後才執行一次。
何時使用
- 搜尋框自動完成:使用者輸入完畢才送出請求,避免每輸入一個字就發送一次 API。
- 視窗尺寸變更(resize):僅在使用者調整完畢後重新計算佈局。
- 表單驗證:使用者停止輸入後才進行驗證,減少不必要的運算。
範例程式碼
/**
* debounce - 產生防抖函式
* @param {Function} fn 要防抖的函式
* @param {number} wait 延遲時間(毫秒)
* @param {boolean} immediate 是否立即執行第一次
* @return {Function}
*/
function debounce(fn, wait, immediate = false) {
let timer = null;
return function (...args) {
const context = this;
// 若 immediate 為 true,第一次立即執行,之後才防抖
if (immediate && !timer) {
fn.apply(context, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
timer = null; // 重置 timer
if (!immediate) fn.apply(context, args);
}, wait);
};
}
/* ------------------- 實作範例 ------------------- */
// 1. 搜尋框自動完成(300ms 防抖)
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function (e) {
fetch(`/api/search?q=${e.target.value}`)
.then(res => res.json())
.then(data => console.log('搜尋結果:', data));
}, 300));
// 2. 視窗 resize(200ms 防抖)
window.addEventListener('resize', debounce(() => {
console.log('視窗尺寸已變更,重新計算佈局');
// 重新計算佈局的函式
}, 200));
// 3. 表單即時驗證(即時執行第一次,之後防抖 500ms)
const emailInput = document.getElementById('email');
emailInput.addEventListener('keyup', debounce(function (e) {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.target.value);
console.log('Email 格式是否正確:', isValid);
}, 500, true));
2. 節流(Throttle)
節流則是「在固定時間間隔內最多只執行一次」的策略。無論事件觸發多頻繁,函式都會以設定的間隔(interval)被呼叫一次。這適合需要持續反饋的情境,例如即時顯示捲動位置或滑鼠座標。
何時使用
- 捲動監控(scroll):計算捲動位置、懶載入圖片或觸發動畫。
- 滑鼠移動(mousemove):即時顯示座標或繪製圖形。
- 按鈕連點防護:限制使用者在短時間內多次點擊同一個按鈕。
範例程式碼
/**
* throttle - 產生節流函式
* @param {Function} fn 要節流的函式
* @param {number} limit 間隔時間(毫秒)
* @return {Function}
*/
function throttle(fn, limit) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn.apply(this, args);
}
};
}
/* ------------------- 實作範例 ------------------- */
// 1. 捲動監控(每 150ms 更新一次捲動位置)
window.addEventListener('scroll', throttle(() => {
const scrollY = window.scrollY || document.documentElement.scrollTop;
console.log('目前捲動位置:', scrollY);
}, 150));
// 2. 滑鼠移動座標顯示(每 100ms 更新一次)
const coordEl = document.getElementById('coords');
document.addEventListener('mousemove', throttle((e) => {
coordEl.textContent = `X: ${e.clientX}, Y: ${e.clientY}`;
}, 100));
// 3. 防止連點送出表單(每 2 秒只能送出一次)
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', throttle(() => {
console.log('表單送出!');
// 送出表單的程式碼
}, 2000));
3. 防抖 vs 節流:何時選擇?
| 條件 | 防抖 (Debounce) | 節流 (Throttle) |
|---|---|---|
| 需求 | 最後一次 行為才需要執行 | 持續 監控或即時回饋 |
| 例子 | 輸入框搜尋、視窗調整 | 捲動位置、滑鼠座標、按鈕防連點 |
| 行為 | 事件停止後才觸發一次 | 固定間隔內最多觸發一次 |
常見陷阱與最佳實踐
忘記保留
this與參數
防抖與節流的回呼通常會在不同的執行上下文中被呼叫,若直接使用fn()會失去原本的this與傳入參數。上面的實作使用apply(this, args)來解決此問題。過度防抖導致延遲感
防抖的延遲時間不宜設得過長,尤其在即時搜尋時,300~500ms 是較為舒適的範圍;若超過 1 秒,使用者會感受到卡頓。節流間隔過短仍會造成效能問題
即使是節流,若limit設得太小(如 10ms),仍會頻繁觸發。根據實際需求調整,通常 100~200ms 已能平衡流暢度與效能。與
requestAnimationFrame結合
在需要與畫面渲染同步的情境(如動畫、捲動效果),建議在節流函式內部使用requestAnimationFrame,可避免不必要的重排與重繪。const throttledScroll = throttle(() => { requestAnimationFrame(() => { // 只在下一個繪製幀執行 console.log('使用 rAF 的節流捲動'); }); }, 100);避免在同一元素上同時綁定防抖與節流
兩者的行為相互衝突,會導致難以預測的結果。根據需求選擇其一即可。使用現成函式庫(lodash、underscore)
這些成熟的函式庫已對 edge case(如cancel、flush)做了完善支援,開發大型專案時可直接引用:// lodash 範例 import { debounce, throttle } from 'lodash'; const debouncedSearch = debounce((term) => { // API 呼叫 }, 300); const throttledResize = throttle(() => { // 重新排版 }, 200);
實際應用場景
1. 無限捲動(Infinite Scroll)
在長列表或社交平台的動態牆上,當使用者滾動接近底部時,需要向伺服器請求下一批資料。若直接在 scroll 事件中判斷,會因為滾動頻繁而多次發送請求。使用節流 可以控制檢查頻率,再配合防抖 防止在快速滾動時重複呼叫。
const loadMore = throttle(() => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 100) {
// 觸發載入更多資料
console.log('載入下一頁資料');
}
}, 200);
window.addEventListener('scroll', loadMore);
2. 表單自動儲存(Auto‑save)
編輯器或表單常需要在使用者暫停輸入時自動儲存草稿。防抖 能在使用者停止輸入後的短暫延遲內觸發儲存,減少頻繁的 API 呼叫。
const autoSave = debounce(() => {
const data = getFormData();
fetch('/api/save-draft', {
method: 'POST',
body: JSON.stringify(data)
});
}, 1000);
document.querySelector('form').addEventListener('input', autoSave);
3. 按鈕防止重複點擊
在結帳、提交等關鍵操作上,若使用者連點多次會導致重複訂單。節流 能保證在一定時間內只能觸發一次。
const payBtn = document.getElementById('pay');
payBtn.addEventListener('click', throttle(() => {
// 呼叫付款 API
console.log('送出付款請求');
}, 3000));
總結
- 防抖(debounce):在事件最後一次觸發後延遲執行,適合輸入、調整尺寸、表單驗證等「結束後」才需要的情境。
- 節流(throttle):在固定時間間隔內最多執行一次,適合捲動、滑鼠移動、連點防護等需要持續回饋的情況。
- 實作時務必保留
this與參數、選擇合適的延遲或間隔時間,並根據需求考慮結合requestAnimationFrame或成熟函式庫(如 lodash)。 - 正確使用防抖與節流不僅能提升前端效能,還能減少伺服器負載、改善使用者體驗,是每位前端開發者在 Performance & Optimization 章節必須熟練的基礎技巧。
透過上述概念與實作範例,你可以在日常開發中快速辨識需要優化的高頻事件,並以防抖或節流的方式為應用程式注入 效能與穩定性。祝你寫出更流暢、更省資源的 JavaScript 程式!