DEV Community

Performance — Throttle

Throttle: giới hạn tần suất chạy cho sự kiện bắn liên tục

Throttle đảm bảo một hàm chạy nhiều nhất một lần mỗi khoảng thời gian, bất kể sự kiện bắn dày tới đâu. Khác với debounce (đợi im lặng rồi chạy lần cuối), throttle chạy đều đặn trong lúc sự kiện đang diễn ra. Đây là công cụ chuẩn cho scroll, mousemove, resize — những sự kiện có thể bắn hàng trăm lần mỗi giây — và cho rate-limit phía client. Làm sai sẽ bỏ lỡ sự kiện cuối quan trọng, hoặc vẫn chạy quá dày vì hiểu nhầm leading/trailing edge.

Cơ chế hoạt động

Throttle ghi nhớ thời điểm chạy lần cuối và chỉ cho chạy lại khi đã qua đủ khoảng limit:

function throttle(fn, limit) {
  let last = 0
  let timer
  return function (...args) {
    const now = Date.now()
    const remaining = limit - (now - last)
    if (remaining <= 0) {
      last = now
      fn.apply(this, args) // leading edge: chạy ngay nếu đã đủ khoảng
    } else {
      clearTimeout(timer)
      timer = setTimeout(() => { last = Date.now(); fn.apply(this, args) }, remaining) // trailing edge
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Bản này có cả leading edge (chạy ngay lần đầu) lẫn trailing edge (đảm bảo lần cuối cùng cũng chạy sau khi ngừng). Trailing edge quan trọng: thiếu nó, vị trí scroll cuối cùng — thứ cần nhất — có thể bị bỏ lỡ vì rơi vào giữa hai mốc throttle.

Vấn đề gặp trong production

Use case điển hình: scroll handler nặng. Tính toán vị trí, lazy-load, hay cập nhật thanh tiến trình theo scroll mà không throttle sẽ chạy hàng trăm lần mỗi giây, làm UI giật:

window.addEventListener('scroll', throttle(() => {
  updateScrollProgress() // chạy tối đa mỗi 100ms thay vì mỗi pixel scroll
}, 100))
Enter fullscreen mode Exit fullscreen mode

Throttle giữ giao diện mượt mà vẫn cập nhật đủ thường xuyên để cảm giác liền mạch.

Failure mode: bỏ lỡ sự kiện cuối (thiếu trailing edge). Một throttle chỉ-leading-edge sẽ chạy ngay rồi bỏ qua mọi sự kiện trong limit — nhưng nếu người dùng dừng scroll giữa khoảng đó, lần cuối không bao giờ chạy, và trạng thái hiển thị kẹt ở giá trị cũ. Với scroll/resize, gần như luôn cần trailing edge để "chốt" trạng thái cuối.

Failure mode: nhầm throttle với debounce cho việc cần phản hồi liên tục. Dùng debounce cho thanh tiến trình scroll sẽ làm nó chỉ cập nhật sau khi người dùng ngừng cuộn — sai hoàn toàn, vì cần cập nhật trong lúc cuộn. Đây là lỗi chọn nhầm pattern: việc cần feedback liên tục dùng throttle, việc chỉ cần kết quả sau cùng dùng debounce.

Failure mode: throttle cho realtime với khoảng quá lớn. Với dữ liệu realtime (vị trí con trỏ trong app cộng tác, cập nhật giá), throttle ổn nhưng limit lớn làm cập nhật giật cục. Cân giữa tần suất và tải: thường 16–50ms cho cảm giác mượt, 100–250ms cho việc nặng hơn.

Cách debug và monitor

Nếu UI vẫn giật khi scroll dù đã throttle, kiểm tra limit có quá nhỏ không (gần như không throttle) và hàm bên trong có thật sự nhẹ không — throttle giảm số lần gọi chứ không làm một hàm nặng nhẹ đi. Nếu trạng thái cuối bị kẹt sai sau khi ngừng tương tác, gần như chắc chắn thiếu trailing edge. Đo bằng cách đếm số lần fn chạy trên một quãng scroll và so với kỳ vọng (quãng / limit). Trong app cộng tác realtime, theo dõi số message gửi đi mỗi giây — throttle là tuyến phòng thủ chính chống ngập kênh; con số vượt ngưỡng nghĩa là throttle chưa áp đúng chỗ.

Tradeoff

Throttle cho cập nhật đều đặn trong lúc sự kiện diễn ra, lý tưởng cho feedback liên tục (scroll, kéo thả, realtime) — đổi lại bỏ qua các sự kiện giữa hai mốc (mất độ phân giải) và, nếu thiếu trailing edge, có thể mất sự kiện cuối. Debounce ngược lại: chỉ chạy sau khi ngừng, hợp khi chỉ cần kết quả cuối (search, auto-save) nhưng không cập nhật trong lúc đang tương tác. Quy tắc thực tế: cần thấy thay đổi liên tục khi đang tương tác → throttle (có trailing edge); chỉ cần hành động một lần sau khi ngừng → debounce; và chọn limit cân bằng giữa mượt và tải, đo theo hành vi thật.

Câu hỏi phỏng vấn

Throttle dùng khi nào thay vì debounce, và vì sao trailing edge lại quan trọng?

Throttle dùng khi cần hàm chạy đều đặn trong lúc sự kiện đang diễn ra — scroll, mousemove, resize, cập nhật realtime — đảm bảo nhiều nhất một lần mỗi khoảng để giữ feedback liên tục mà không quá tải; debounce dùng khi chỉ cần một lần chạy sau khi sự kiện ngừng, như search-as-you-type hay auto-save. Trailing edge quan trọng vì throttle chỉ-leading-edge chạy ngay rồi bỏ qua mọi sự kiện trong khoảng limit; nếu người dùng dừng tương tác giữa khoảng đó, lần cuối cùng — thường là trạng thái cần nhất, như vị trí scroll cuối — không bao giờ chạy và giao diện kẹt ở giá trị cũ. Điểm ăn điểm: chọn limit cân bằng mượt/tải, biết throttle giảm số lần gọi chứ không làm hàm nặng nhẹ đi, và nhầm debounce cho việc cần cập nhật liên tục là lỗi chọn sai pattern.

Hands-on

Gắn một scroll handler thật làm việc nặng (cập nhật nhiều phần tử theo vị trí cuộn) và đếm số lần nó chạy khi cuộn nhanh qua một trang dài, quan sát UI giật. Bọc bằng throttle(100) chỉ-leading-edge và đếm lại; sau đó dừng cuộn đột ngột và kiểm tra trạng thái cuối có bị kẹt sai không — thêm trailing edge để sửa và xác nhận trạng thái chốt đúng. Cuối cùng dựng một tình huống realtime (gửi vị trí con trỏ qua một kênh) với throttle, đo số message mỗi giây ở các giá trị limit khác nhau, và so sánh trải nghiệm giữa dùng throttle và dùng nhầm debounce cho cùng việc.

Top comments (0)