I. GIỚI THIỆU CHUNG

Debouncethrottle là hai kỹ thuật cho phép chúng ta kiểm soát số lần một hàm được thực thi trong một khoảng thời gian nhất định. Hai kỹ thuật này tuy có điểm tương tự nhau, nhưng không hoàn toàn giống nhau.

Hãy quan sát sự kiện scroll (cuộn) thông thường, khi chưa áp dụng debounce và throttle:

Chỉ một lần trượt trên touchpad hay cuộn con lăn chuột có thể dễ dàng trigger (kích hoạt) đến 30 event mỗi giây, còn một lần vuốt trên màn hình điện thoại có thể trigger lên tới 100 scroll event mỗi giây. Nội dung bên trong hàm xử lý các sự kiện đôi khi khá nặng do phải thực hiện nhiều hành động khác nhau, liệu rằng hàm xử lý của bạn có chuẩn bị tốt được cho tốc độ thực thi này không?

Vấn đề này đã xuất hiện trên trang Twitter vào năm 2011, khi người dùng liên tục cuộn xuống Twitter feed, trang web trở nên rất chậm và thậm chí không có phản hồi, điều này thực sự gây ảnh hưởng lớn tới trải nghiệm của người dùng.

Hai kỹ thuật debouncethrottle sẽ giúp bạn kiểm soát vấn đề này, cải thiện hiệu suất của ứng dụng. Cả hai đều làm giảm event trigger rate (tỷ lệ kích hoạt sự kiện), tức là giảm số lần mà event listener (hàm lắng nghe sự kiện) của bạn được gọi trong một khoảng thời gian nhất định. Tuy nhiên, chúng được sử dụng trong các trường hợp khác nhau.

Debounce thường được sử dụng khi bạn chỉ quan tâm đến trạng thái cuối cùng. Ví dụ: đợi cho đến khi người dùng ngừng nhập vào ô search input mới thực hiện tìm kiếm kết quả (input keyup event).

Throttle được sử dụng khi bạn muốn xử lý tất cả các trạng thái trung gian nhưng với tốc độ được kiểm soát. Ví dụ: theo dõi chiều rộng màn hình khi người dùng thay đổi kích thước cửa sổ và sắp xếp lại nội dung trang trong khi nó thay đổi thay vì đợi cho đến khi người dùng hoàn thành (window resize event).

Bây giờ chúng ta sẽ tìm hiểu chi tiết hơn từng kỹ thuật này.

II. PHÂN TÍCH CHI TIẾT

1. Debounce

1.1. Định nghĩa

Debounce đảm bảo rằng một hàm không được thực thi cho đến khi một khoảng thời gian nhất định trôi qua mà nó không được gọi.

Ví dụ như “thực thi hàm này chỉ khi 100 mili giây trôi qua mà nó không được gọi”.

Giả sử một hàm được gọi 1000 lần liên tục trong khoảng thời gian 3 giây, sau đó ngừng không được gọi nữa. Nếu bạn sử dụng debounce cho hàm đó với thời gian debounce là 100 mili giây, thì hàm sẽ chỉ kích hoạt một lần duy nhất ở giây thứ 3,1, sau khi kết thúc quá trình gọi 1000 lần liên tục. Mỗi khi hàm được gọi, thì thời gian debounce sẽ được reset lại từ đầu.

1.2. Ví dụ

Xét ví dụ keyup event dưới đây:

Bạn có nhận thấy rằng khi bạn nhập vào ô tìm kiếm, sẽ có một khoảng thời gian trễ trước khi kết quả phù hợp được trả về, làm được điều này là do áp dụng debounce. Debounce sẽ hoãn quá trình xử lý sự kiện keyup cho đến khi người dùng ngừng nhập trong một khoảng thời gian định trước.

Debounce giúp ngăn việc giao diện người dùng phải xử lý mọi sự kiện và cũng làm giảm đáng kể số lần gọi API được gửi đến server. Việc xử lý mọi ký tự khi được nhập có thể làm giảm hiệu suất và thêm tải không cần thiết cho server backend.

Các trường hợp phổ biến sử dụng debounce là resize, scroll, keyup, keydown.

Quay trở lại với ví dụ scroll event ở phần giới thiệu, khi được áp dụng debounce, event listener được trigger sau khoảng thời gian 100 mili giây khi không scroll nữa.

1.3. Cách thực hiện

Cách 1. Dùng JavaScript thuần

// ES6
function debounce(func, delay) {
    let timeout;

    return function executedFunc(...args) {
        if (timeout) {
            clearTimeout(timeout);
        }

        timeout = setTimeout(() => {
            func(...args);
            timeout = null;
        }, delay);
    };
}

Ví dụ: Áp dụng debounce cho keyup event của ô search input có id là search

const eventHandler = (event) => {
    // Do something with the event
};
const dHandler = debounce(eventHandler, 200);
document.getElementById("search").addEventListener("keyup", dHandler);

Giải thích hàm debounce:

Hàm debounce là một higher-order function (hàm thứ bậc cao), là một hàm trả về một hàm khác (ở đây trả về hàm executedFunc). Việc này nhằm để tạo nên một closure cho các tham số của hàm debounce: func, delay, và biến cục bộ (local variable) timeout.

  • func: Hàm bạn muốn thực thi sau thời gian debounce
  • delay (đơn vị ms): Khoảng thời gian bạn muốn hàm debounce đợi sau hành động nhận được cuối cùng trước khi thực thi func. Ví dụ đối với keyup event ở trên, delay sẽ là khoảng thời gian chờ đợi sau lần nhấn phím cuối cùng.
  • timeout: Giá trị được sử dụng để biểu thị debounce đang chạy.
  • ...args: Cho phép truyền vào số lượng đối số bất kỳ.

Cách 2: Dùng thư viện Lodash

Nếu không muốn tự code từ đầu, bạn có thể sử dụng thư viện Lodash, bằng cách dùng link CDN hoặc cài đặt Lodash bằng lệnh:

npm install lodash

hoặc

yarn add lodash

Nếu thấy cài toàn bộ thư viện nặng quá, trong khi bạn chỉ cần sử dụng hàm _.debounce_.throttle, bạn có thể sử dụng Lodash custom builds để xuất ra thư viện thu nhỏ chỉ 2KB bằng cách dùng lệnh sau:

npm i -g lodash-cli
lodash include=debounce,throttle

Lệnh này sẽ tạo ra file lodash.custom.jslodash.custom.min.js trong thư mục làm việc hiện tại của bạn.

Cú pháp debounce với Lodash và jQuery như sau:

$("#search").on("keyup", _.debounce(function() {
  // Do something with the event
}, 200));

1.4. Leading và trailing call trong debounce

Có một vấn đề khi sử dụng hàm debounce có thể sẽ gây khó chịu do mặc định hàm debounce chỉ được thực thi một khi chuỗi event liên tục đã dừng.

Nếu muốn tự viết code JavaScript thuần, cần thêm tham số immediate (kiểu Boolean) cho hàm debounce, code sẽ phức tạp hơn, bạn tham khảo chi tiết hơn tại đây.

Nếu sử dụng Lodash, bạn có thể kích hoạt chức năng thực thi hàm ngay lập tức, bằng cách dùng thêm option {leading: true, trailing: true}, mặc định là {leading: false, trailing: true}.

$("#search").on("keyup", _.debounce(function() {
  // Do something with the event
}, 200, {leading:true, trailing:true}));

Như vậy, hàm debounce sẽ thực thi một lần khi chuỗi event liên tục bắt đầu, thực thi một lần nữa khi chuỗi event liên tục ngừng lại sau 200 mili giây.

Để hiểu rõ hơn về leadingtrailing, hãy quan sát 2 ví dụ dưới đây:

VD1: {leading:false, trailing:true}

VD2: {leading:true, trailing:false}

2. Throttle

2.1. Định nghĩa

Throttle giới hạn số lần tối đa một hàm được gọi trong một khoảng thời gian nhất định.

Ví dụ như “thực thi hàm này nhiều nhất mỗi 100 mili giây một lần”.

Giả sử trong trường hợp thông thường, bạn sẽ gọi hàm event listener 1000 lần trong 10 giây. Nếu bạn throttle hàm đó thành 100 mili giây một lần, hàm đó sẽ chỉ được thực thi 100 lần trong mỗi 10 giây.

(10s * 1000) = 10000ms
10000ms / 100ms throttle = 100 maximum calls
(maximum calls: số lần gọi tối đa)

2.2. Ví dụ

Tiếp tục trở lại với ví dụ scroll event ở phần giới thiệu, khi được áp dụng throttle, event listener được trigger 200 mili giây một lần:

2.3. Cách thực hiện

Cách 1: Dùng JavaScript thuần

// ES6 code
function throttle(func, delay) {
    let lastCall = 0;

    return function (...args) {
        const now = new Date().getTime();

        if (now - lastCall < delay) {
            return;
        }

        lastCall = now;
        return func(...args);
    };
}

Ví dụ áp dụng throttle cho document mousemove event:

const eventHandler = (event) => {
    // Do something with the event
};
const tHandler = throttle(eventHandler, 200);
document.addEventListener("mousemove", tHandler);

Theo ví dụ trên, sự kiện mousemove trên document chỉ kích hoạt hàm eventHandler mỗi 200 mili giây một lần. Tất cả các sự kiện xảy ra trong thời gian đó sẽ bị bỏ qua.

Khi sử dụng throttle, điều quan trọng là phải đạt được sự cân bằng giữa sự mượt mà và khả năng phản hồi. Event listener phải đảm bảo được kích hoạt liên tục nhất có thể mà không làm ảnh hưởng giao diện người dùng.

Cách 2. Dùng thư viện Lodash

Cách cài đặt thư viện Lodash giống phần Debounce ở trên.

Cú pháp throttle với Lodash và jQuery:

$( document).on("scroll", _.throttle(() => {
  // Do something with the event
}, 200));

Throttle trong Lodash mặc định là {leading: true, trailing: true}.

Thực hiện debounce, throttle là một câu hỏi phỏng vấn phổ biến. Nó kiểm tra sự hiểu biết của bạn về các khái niệm JavaScript trung cấp và nâng cao như: lập trình không đồng bộ, callback, scope và closure. Đây cũng là một giải pháp hữu ích được sử dụng trong các ứng dụng thực tế để cải thiện hiệu suất và chứng minh rằng bạn hiểu các công cụ để viết code tốt cho người dùng thực.

THAM KHẢO

Nếu cần tìm hiểu chi tiết hơn về cách sử dụng debounce và throttle của thư viện Lodash, bạn có thể tham khảo tại đây: Debounce, Throttle.

Xem thêm nhiều ví dụ cụ thể về debounce, throttle tại đây.

Nguồn bài viết