Trong phát triển ứng dụng web, việc xử lý các sự kiện được kích hoạt với tần suất cao như cuộn trang (scroll), thay đổi kích thước cửa sổ (resize), nhập liệu (input) hoặc di chuyển chuột (mousemove) có thể dẫn đến các vấn đề về hiệu suất. Nếu không được quản lý đúng cách, mỗi lần sự kiện kích hoạt sẽ gọi một hàm xử lý, gây ra nhiều tính toán không cần thiết và làm giảm trải nghiệm người dùng. Để giải quyết vấn đề này, hai kỹ thuật phổ biến được sử dụng là Debounce và Throttle.
Kỹ thuật Debounce
Debounce là một kỹ thuật đảm bảo rằng một hàm chỉ được thực thi sau một khoảng thời gian nhất định kể từ lần kích hoạt cuối cùng. Nó hữu ích khi bạn muốn đợi cho đến khi một chuỗi các sự kiện kết thúc mới thực hiện hành động. Ví dụ, khi người dùng gõ vào ô tìm kiếm, bạn không muốn gửi yêu cầu API sau mỗi ký tự mà chỉ muốn gửi sau khi họ ngừng gõ một lúc.
Nguyên lý hoạt động của Debounce:
- Khi sự kiện được kích hoạt, một bộ đếm thời gian (timer) được thiết lập để gọi hàm xử lý sau một độ trễ nhất định.
- Nếu sự kiện được kích hoạt lại trước khi bộ đếm thời gian trước đó kết thúc, bộ đếm thời gian cũ sẽ bị hủy và một bộ đếm thời gian mới được thiết lập.
- Hàm chỉ thực sự được gọi khi không có sự kiện nào được kích hoạt trong suốt khoảng thời gian chờ đợi.
Ví dụ triển khai Debounce trong JavaScript:
function performSearch(query) {
console.log(`Đang tìm kiếm với từ khóa: "${query}" lúc ${new Date().toLocaleTimeString()}`);
// Giả lập một lệnh gọi API hoặc xử lý dữ liệu nặng
}
function debounce(func, delayMilliseconds) {
let timeoutId;
return function(...args) {
const context = this; // Giữ ngữ cảnh của `this`
clearTimeout(timeoutId); // Xóa bỏ timer trước nếu có
timeoutId = setTimeout(() => {
func.apply(context, args); // Thực thi hàm sau độ trễ
}, delayMilliseconds);
};
}
// Giả sử có một ô input với id 'searchInput'
const searchInput = document.getElementById('searchInput');
if (searchInput) {
// Áp dụng debounce cho sự kiện 'input'
const debouncedSearch = debounce((event) => {
performSearch(event.target.value);
}, 500); // Đợi 500ms sau khi người dùng ngừng gõ
searchInput.addEventListener('input', debouncedSearch);
}
// Ví dụ khác với sự kiện scroll
function logScrollPosition() {
const scrollY = window.scrollY || document.documentElement.scrollTop;
console.log(`Vị trí cuộn hiện tại: ${scrollY}px lúc ${new Date().toLocaleTimeString()}`);
}
window.addEventListener('scroll', debounce(logScrollPosition, 300)); // Log vị trí sau 300ms không cuộn
Trong đoạn mã trên, hàm debounce nhận vào một hàm (func) và một độ trễ (delayMilliseconds). Nó trả về một hàm bao bọc, mỗi khi hàm bao bọc này được gọi, nó sẽ xóa bộ đếm thời gian cũ và thiết lập một bộ đếm thời gian mới. Hàm func chỉ được thực thi khi không có lời gọi nào đến hàm bao bọc trong khoảng thời gian delayMilliseconds.
Kỹ thuật Throttle
Throttle là một kỹ thuật giới hạn tần suất thực thi của một hàm. Thay vì đợi cho đến khi một chuỗi các sự kiện kết thúc như debounce, throttle đảm bảo rằng hàm chỉ được gọi tối đa một lần trong một khoảng thời gian nhất định. Nó hữu ích khi bạn muốn thực hiện một hành động đều đặn, ngay cả khi sự kiện được kích hoạt rất nhanh.
Nguyên lý hoạt động của Throttle:
- Khi sự kiện được kích hoạt, hàm xử lý sẽ được gọi ngay lập tức nếu đã đủ thời gian kể từ lần gọi cuối cùng.
- Nếu sự kiện tiếp tục được kích hoạt trong khoảng thời gian "hạ nhiệt", các lời gọi tiếp theo sẽ bị bỏ qua.
- Hàm chỉ được thực thi lại sau khi khoảng thời gian "hạ nhiệt" kết thúc.
Triển khai Throttle bằng cờ (flag)
Phương pháp này sử dụng một biến boolean (cờ) để kiểm soát xem hàm có được phép thực thi hay không. Sau khi hàm được thực thi, cờ sẽ được đặt thành false và chỉ trở lại true sau một khoảng thời gian chờ.
function processHeavyTask(data) {
console.log(`Đang xử lý tác vụ nặng với dữ liệu: "${data}" lúc ${new Date().toLocaleTimeString()}`);
// Giả lập một tác vụ tốn tài nguyên
}
function throttleWithFlag(func, intervalMilliseconds) {
let canExecute = true;
return function(...args) {
const context = this;
if (!canExecute) {
return; // Nếu chưa đủ thời gian, bỏ qua lời gọi
}
canExecute = false; // Đặt cờ thành false để ngăn chặn các lời gọi tiếp theo
setTimeout(() => {
func.apply(context, args); // Thực thi hàm
canExecute = true; // Cho phép thực thi lại sau khi interval kết thúc
}, intervalMilliseconds);
};
}
// Ví dụ sử dụng throttle với cờ cho sự kiện mousemove
function trackMouseMovement(event) {
console.log(`Vị trí chuột: X=${event.clientX}, Y=${event.clientY} lúc ${new Date().toLocaleTimeString()}`);
}
document.body.addEventListener('mousemove', throttleWithFlag(trackMouseMovement, 100)); // Cập nhật vị trí chuột tối đa 10 lần/giây
Triển khai Throttle bằng dấu thời gian (timestamp)
Phương pháp này kiểm tra thời gian kể từ lần cuối cùng hàm được thực thi bằng cách so sánh dấu thời gian hiện tại với dấu thời gian của lần thực thi trước đó.
function updateUIElement(value) {
console.log(`Cập nhật giao diện với giá trị: "${value}" lúc ${new Date().toLocaleTimeString()}`);
// Cập nhật DOM hoặc các thành phần UI khác
}
function throttleWithTimestamp(func, limitMilliseconds) {
let lastExecutionTime = 0;
return function(...args) {
const context = this;
const currentTime = Date.now();
if (currentTime - lastExecutionTime >= limitMilliseconds) {
lastExecutionTime = currentTime; // Cập nhật thời gian thực thi cuối cùng
func.apply(context, args); // Thực thi hàm
}
};
}
// Giả sử có một nút bấm với id 'myButton'
const myButton = document.getElementById('myButton');
if (myButton) {
// Áp dụng throttle cho sự kiện 'click'
myButton.addEventListener('click', throttleWithTimestamp(() => {
updateUIElement("Button Clicked!");
}, 1000)); // Nút chỉ có thể được click và xử lý mỗi giây một lần
}
// Ví dụ khác với sự kiện resize
function adjustLayout() {
console.log(`Điều chỉnh layout theo kích thước: ${window.innerWidth}x${window.innerHeight} lúc ${new Date().toLocaleTimeString()}`);
}
window.addEventListener('resize', throttleWithTimestamp(adjustLayout, 200)); // Điều chỉnh layout tối đa 5 lần/giây
Cả hai kỹ thuật Debounce và Throttle đều là những công cụ mạnh mẽ để cải thiện hiệu suất ứng dụng web bằng cách quản lý tần suất gọi các hàm xử lý sự kiện. Việc lựa chọn kỹ thuật nào phụ thuộc vào yêu cầu cụ thể của từng trường hợp: dùng Debounce khi bạn muốn phản ứng sau một khoảng thời gian không có sự kiện, và dùng Throttle khi bạn muốn đảm bảo một hành động diễn ra đều đặn với tần suất giới hạn.