本文 AI 產出,尚未審核

JavaScript — 效能與最佳化

防抖與節流(debounce / throttle)


簡介

在前端開發中,我們常常需要處理大量、頻繁觸發的事件,例如 scrollresizekeyupmousemove 等。如果直接在每一次事件發生時都執行相同的程式碼,瀏覽器很快就會因為過度重繪或大量運算而卡頓,使用者體驗會急速下降。

防抖(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)
需求 最後一次 行為才需要執行 持續 監控或即時回饋
例子 輸入框搜尋、視窗調整 捲動位置、滑鼠座標、按鈕防連點
行為 事件停止後才觸發一次 固定間隔內最多觸發一次

常見陷阱與最佳實踐

  1. 忘記保留 this 與參數
    防抖與節流的回呼通常會在不同的執行上下文中被呼叫,若直接使用 fn() 會失去原本的 this 與傳入參數。上面的實作使用 apply(this, args) 來解決此問題。

  2. 過度防抖導致延遲感
    防抖的延遲時間不宜設得過長,尤其在即時搜尋時,300~500ms 是較為舒適的範圍;若超過 1 秒,使用者會感受到卡頓。

  3. 節流間隔過短仍會造成效能問題
    即使是節流,若 limit 設得太小(如 10ms),仍會頻繁觸發。根據實際需求調整,通常 100~200ms 已能平衡流暢度與效能。

  4. requestAnimationFrame 結合
    在需要與畫面渲染同步的情境(如動畫、捲動效果),建議在節流函式內部使用 requestAnimationFrame,可避免不必要的重排與重繪。

    const throttledScroll = throttle(() => {
      requestAnimationFrame(() => {
        // 只在下一個繪製幀執行
        console.log('使用 rAF 的節流捲動');
      });
    }, 100);
    
  5. 避免在同一元素上同時綁定防抖與節流
    兩者的行為相互衝突,會導致難以預測的結果。根據需求選擇其一即可。

  6. 使用現成函式庫(lodash、underscore)
    這些成熟的函式庫已對 edge case(如 cancelflush)做了完善支援,開發大型專案時可直接引用:

    // 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 程式!