1. Giới thiệu
Đây là một bản minh hoạ và lớp học lập trình giàu tính tương tác, giúp bạn tìm hiểu về Lượt tương tác đến nội dung hiển thị tiếp theo (INP).
Điều kiện tiên quyết
- Kiến thức về phát triển HTML và JavaScript.
- Nên làm: đọc tài liệu về INP.
Kiến thức bạn sẽ học được
- Cách hoạt động tương tác của người dùng và cách bạn xử lý các hoạt động tương tác đó ảnh hưởng đến khả năng phản hồi của trang.
- Cách giảm và loại bỏ độ trễ để mang lại trải nghiệm mượt mà cho người dùng.
Bạn cần có
- Máy tính có khả năng sao chép mã từ GitHub và chạy các lệnh npm.
- Trình chỉnh sửa văn bản.
- Phiên bản Chrome mới nhất để tất cả các phép đo lường lượt tương tác đều hoạt động.
2. Bắt đầu thiết lập
Lấy và chạy mã
Bạn có thể tìm thấy mã trong kho lưu trữ web-vitals-codelabs.
- Sao chép kho lưu trữ trong dòng lệnh:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git - Chuyển vào thư mục đã sao chép:
cd web-vitals-codelabs/understanding-inp - Cài đặt phần phụ thuộc:
npm ci - Khởi động máy chủ web:
npm run start - Truy cập vào http://localhost:5173/understanding-inp/ trong trình duyệt
Tổng quan về ứng dụng
Ở đầu trang là bộ đếm Score (Điểm) và nút Increment (Tăng). Một bản minh hoạ kinh điển về khả năng phản ứng và thích ứng!

Bên dưới nút này có 4 chỉ số đo lường:
- INP: điểm INP hiện tại, thường là lượt tương tác tệ nhất.
- Lượt tương tác: điểm số của lượt tương tác gần đây nhất.
- FPS: số khung hình trên giây của luồng chính trên trang.
- Bộ tính giờ: ảnh động bộ tính giờ đang chạy để giúp hình dung hiện tượng giật.
Các mục FPS và Bộ hẹn giờ hoàn toàn không cần thiết để đo lường lượt tương tác. Các thành phần này được thêm vào chỉ để giúp bạn dễ dàng hình dung khả năng thích ứng hơn một chút.
Dùng thử
Hãy thử tương tác với nút Increment (Tăng) và xem điểm số tăng lên. Giá trị INP và Tương tác có thay đổi theo mỗi lần tăng không?
INP đo lường thời gian từ thời điểm người dùng tương tác cho đến khi trang thực sự hiển thị nội dung cập nhật đã kết xuất cho người dùng.
3. Đo lường lượt tương tác bằng Chrome DevTools
Mở Công cụ cho nhà phát triển trong trình đơn Công cụ khác > Công cụ cho nhà phát triển, bằng cách nhấp chuột phải vào trang rồi chọn Kiểm tra hoặc sử dụng phím tắt.
Chuyển sang bảng điều khiển Hiệu suất mà bạn sẽ dùng để đo lường các lượt tương tác.

Tiếp theo, hãy ghi lại một lượt tương tác trong bảng điều khiển Hiệu suất.
- Nhấn vào nút Giữ để quay.
- Tương tác với trang (nhấn nút Increment (Tăng)).
- Dừng ghi.
Trong dòng thời gian thu được, bạn sẽ thấy một bản nhạc Tương tác. Mở rộng mục này bằng cách nhấp vào hình tam giác ở bên trái.

Hai lượt tương tác xuất hiện. Phóng to hình thứ hai bằng cách cuộn hoặc giữ phím W.

Khi di chuột qua lượt tương tác, bạn có thể thấy lượt tương tác diễn ra nhanh, không tốn thời gian trong thời lượng xử lý và tốn một khoảng thời gian tối thiểu trong độ trễ đầu vào và độ trễ hiển thị, thời lượng chính xác sẽ phụ thuộc vào tốc độ của máy.
4. Trình nghe sự kiện chạy trong thời gian dài
Mở tệp index.js rồi bỏ ghi chú hàm blockFor bên trong trình nghe sự kiện.
Xem mã đầy đủ: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Lưu tệp. Máy chủ sẽ thấy thay đổi và làm mới trang cho bạn.
Hãy thử tương tác lại với trang. Giờ đây, các lượt tương tác sẽ chậm hơn đáng kể.
Dấu vết hiệu suất
Hãy ghi lại một bản ghi khác trong bảng điều khiển Hiệu suất để xem bản ghi đó trông như thế nào.

Một lượt tương tác từng diễn ra nhanh chóng giờ đây mất cả giây.
Khi bạn di chuột qua lượt tương tác, hãy lưu ý rằng thời gian gần như hoàn toàn dành cho "Thời lượng xử lý", tức là khoảng thời gian cần thiết để thực thi lệnh gọi lại của trình nghe sự kiện. Vì lệnh gọi blockFor chặn hoàn toàn nằm trong trình nghe sự kiện, nên đó là nơi tốn thời gian.
5. Thử nghiệm: thời gian xử lý
Hãy thử các cách sắp xếp lại công việc của trình nghe sự kiện để xem tác động đối với INP.
Cập nhật giao diện người dùng trước
Điều gì xảy ra nếu bạn hoán đổi thứ tự của các lệnh gọi js – cập nhật giao diện người dùng trước, sau đó chặn?
Xem mã đầy đủ: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Bạn có nhận thấy giao diện người dùng xuất hiện sớm hơn không? Thứ tự có ảnh hưởng đến điểm INP không?
Hãy thử lấy dấu vết và kiểm tra hoạt động tương tác để xem có điểm khác biệt nào không.
Trình nghe riêng biệt
Điều gì sẽ xảy ra nếu bạn chuyển công việc sang một trình nghe sự kiện riêng biệt? Cập nhật giao diện người dùng trong một trình nghe sự kiện và chặn trang khỏi một trình nghe riêng biệt.
Xem mã đầy đủ: hai_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Bây giờ, bảng điều khiển hiệu suất trông như thế nào?
Các loại sự kiện khác nhau
Hầu hết các lượt tương tác sẽ kích hoạt nhiều loại sự kiện, từ sự kiện con trỏ hoặc sự kiện chính, đến sự kiện di chuột, sự kiện lấy tiêu điểm/làm mờ và các sự kiện tổng hợp như beforechange và beforeinput.
Nhiều trang thực tế có trình nghe cho nhiều sự kiện khác nhau.
Điều gì sẽ xảy ra nếu bạn thay đổi loại sự kiện cho trình nghe sự kiện? Ví dụ: thay thế một trong các trình nghe sự kiện click bằng pointerup hoặc mouseup?
Xem mã hoàn chỉnh: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Không có bản cập nhật giao diện người dùng
Điều gì sẽ xảy ra nếu bạn xoá lệnh gọi để cập nhật giao diện người dùng khỏi trình nghe sự kiện?
Xem mã đầy đủ: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
6. Kết quả thử nghiệm về thời gian xử lý
Dấu vết hiệu suất: cập nhật giao diện người dùng trước
Xem mã đầy đủ: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Khi xem bản ghi của bảng điều khiển Hiệu suất về việc nhấp vào nút, bạn có thể thấy kết quả không thay đổi. Mặc dù bản cập nhật giao diện người dùng được kích hoạt trước mã chặn, nhưng trình duyệt không thực sự cập nhật nội dung được vẽ lên màn hình cho đến khi trình nghe sự kiện hoàn tất, nghĩa là lượt tương tác vẫn mất hơn một giây để hoàn tất.

Dữ liệu theo dõi hiệu suất: trình nghe riêng biệt
Xem mã đầy đủ: hai_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Xin nhắc lại, không có sự khác biệt về chức năng. Lượt tương tác vẫn mất trọn một giây.
Nếu phóng to vào hoạt động tương tác nhấp, bạn sẽ thấy có hai hàm khác nhau được gọi do sự kiện click.
Đúng như dự kiến, tác vụ đầu tiên (cập nhật giao diện người dùng) chạy cực kỳ nhanh, trong khi tác vụ thứ hai mất trọn một giây. Tuy nhiên, tổng tác động của các vấn đề này dẫn đến cùng một lượt tương tác chậm cho người dùng cuối.

Dấu vết hiệu suất: các loại sự kiện khác nhau
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Những kết quả này rất giống nhau. Hoạt động tương tác vẫn mất trọn một giây; điểm khác biệt duy nhất là trình nghe click ngắn hơn chỉ cập nhật giao diện người dùng hiện chạy sau trình nghe pointerup chặn.

Dấu vết hiệu suất: không có bản cập nhật giao diện người dùng
Xem mã đầy đủ: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- Điểm số không cập nhật, nhưng trang vẫn cập nhật!
- Ảnh động, hiệu ứng CSS, thao tác mặc định của thành phần web (nhập biểu mẫu), nhập văn bản, đánh dấu văn bản đều tiếp tục cập nhật.
Trong trường hợp này, nút sẽ chuyển sang trạng thái hoạt động và quay lại khi được nhấp vào. Điều này yêu cầu trình duyệt phải hiển thị, tức là vẫn có INP.
Vì trình nghe sự kiện đã chặn luồng chính trong một giây để ngăn trang hiển thị, nên lượt tương tác vẫn mất một giây.
Khi ghi lại bảng điều khiển Hiệu suất, bạn sẽ thấy hoạt động tương tác gần như giống hệt với những hoạt động trước đó.

Takeaway
Bất kỳ mã nào chạy trong bất kỳ trình nghe sự kiện nào cũng sẽ làm chậm lượt tương tác.
- Điều này bao gồm cả trình nghe được đăng ký từ các tập lệnh và mã khung hoặc thư viện khác nhau chạy trong trình nghe, chẳng hạn như bản cập nhật trạng thái kích hoạt quá trình kết xuất thành phần.
- Không chỉ mã của riêng bạn mà còn tất cả tập lệnh của bên thứ ba.
Đây là một vấn đề thường gặp.
Cuối cùng: chỉ vì mã của bạn không kích hoạt một lượt vẽ không có nghĩa là một lượt vẽ sẽ không chờ trình nghe sự kiện chậm hoàn tất.
7. Thử nghiệm: độ trễ đầu vào
Còn mã chạy trong thời gian dài bên ngoài trình nghe sự kiện thì sao? Ví dụ:
- Nếu bạn có một
<script>tải chậm và chặn trang một cách ngẫu nhiên trong khi tải. - Một lệnh gọi API, chẳng hạn như
setInterval, chặn trang theo định kỳ?
Hãy thử xoá blockFor khỏi trình nghe sự kiện và thêm vào setInterval():
Xem mã đầy đủ: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Điều gì sẽ xảy ra?
8. Kết quả thử nghiệm về độ trễ đầu vào
Xem mã đầy đủ: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Việc ghi lại một lượt nhấp vào nút xảy ra trong khi tác vụ chặn setInterval đang chạy sẽ dẫn đến một lượt tương tác kéo dài, ngay cả khi không có công việc chặn nào được thực hiện trong chính lượt tương tác đó!
Những khoảng thời gian chạy dài này thường được gọi là tác vụ cần nhiều thời gian.
Khi di chuột qua lượt tương tác trong Công cụ cho nhà phát triển, bạn sẽ thấy thời gian tương tác hiện chủ yếu được tính cho độ trễ đầu vào chứ không phải thời gian xử lý.

Lưu ý rằng điều này không luôn ảnh hưởng đến các lượt tương tác! Nếu không nhấp vào khi tác vụ đang chạy, bạn có thể gặp may. Những "cơn hắt hơi" ngẫu nhiên như vậy có thể là một cơn ác mộng khi gỡ lỗi, vì đôi khi chúng mới gây ra vấn đề.
Một cách để theo dõi những vấn đề này là đo lường các tác vụ cần nhiều thời gian (hoặc khung hoạt hoạ cần nhiều thời gian) và Tổng thời gian chặn.
9. Bản trình bày chậm
Cho đến nay, chúng ta đã xem xét hiệu suất của JavaScript thông qua độ trễ đầu vào hoặc trình nghe sự kiện, nhưng còn yếu tố nào khác ảnh hưởng đến việc hiển thị lượt vẽ tiếp theo?
Cập nhật trang bằng các hiệu ứng tốn kém!
Ngay cả khi trang được cập nhật nhanh chóng, trình duyệt vẫn có thể phải hoạt động vất vả để hiển thị trang!
Trên luồng chính:
- Khung giao diện người dùng cần hiển thị nội dung cập nhật sau khi trạng thái thay đổi
- Các thay đổi về DOM hoặc việc bật/tắt nhiều bộ chọn truy vấn CSS tốn kém có thể kích hoạt nhiều Kiểu, Bố cục và Vẽ.
Ngoài luồng chính:
- Sử dụng CSS để tăng cường hiệu ứng GPU
- Thêm hình ảnh có độ phân giải cao và kích thước rất lớn
- Sử dụng SVG/Canvas để vẽ các cảnh phức tạp

Một số ví dụ thường thấy trên web:
- Một trang web SPA tạo lại toàn bộ DOM sau khi nhấp vào một đường liên kết mà không tạm dừng để cung cấp phản hồi hình ảnh ban đầu.
- Một trang tìm kiếm cung cấp các bộ lọc tìm kiếm phức tạp với giao diện người dùng động, nhưng chạy trình nghe tốn kém để làm việc đó.
- Nút bật/tắt chế độ tối kích hoạt kiểu/bố cục cho toàn bộ trang
10. Thử nghiệm: độ trễ khi trình bày
requestAnimationFrame có tốc độ chậm
Hãy mô phỏng độ trễ trình bày dài bằng requestAnimationFrame() API.
Di chuyển lệnh gọi blockFor vào lệnh gọi lại requestAnimationFrame để lệnh gọi này chạy sau khi trình nghe sự kiện trả về:
Xem mã đầy đủ: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Điều gì sẽ xảy ra?
11. Kết quả thử nghiệm về độ trễ khi trình bày
Xem mã đầy đủ: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Lượt tương tác vẫn kéo dài một giây, vậy điều gì đã xảy ra?
requestAnimationFrame yêu cầu một lệnh gọi lại trước lần vẽ tiếp theo. Vì INP đo lường thời gian từ lượt tương tác đến lần hiển thị tiếp theo, nên blockFor(1000) trong requestAnimationFrame tiếp tục chặn lần hiển thị tiếp theo trong một giây.

Tuy nhiên, hãy lưu ý hai điều:
- Khi di chuột, bạn sẽ thấy tất cả thời gian tương tác hiện đang được dành cho "độ trễ hiển thị" vì hoạt động chặn luồng chính đang diễn ra sau khi trình nghe sự kiện trả về.
- Gốc của hoạt động luồng chính không còn là sự kiện nhấp chuột nữa mà là "Khung ảnh động được kích hoạt".
12. Chẩn đoán hoạt động tương tác
Trên trang thử nghiệm này, khả năng phản hồi rất trực quan, với điểm số, bộ tính giờ và giao diện người dùng của bộ đếm...nhưng khi thử nghiệm trang trung bình, khả năng phản hồi sẽ tinh tế hơn.
Khi các lượt tương tác diễn ra trong thời gian dài, không phải lúc nào bạn cũng biết rõ nguyên nhân là gì. Có phải là:
- Độ trễ đầu vào?
- Thời gian xử lý sự kiện?
- Độ trễ khi trình bày?
Trên bất kỳ trang nào bạn muốn, bạn có thể sử dụng Công cụ cho nhà phát triển để đo lường khả năng thích ứng. Để tạo thói quen, hãy thử quy trình sau:
- Duyệt web như bình thường.
- Theo dõi Nhật ký tương tác trong chế độ xem chỉ số theo thời gian thực của bảng điều khiển Hiệu suất trong DevTools.
- Nếu bạn thấy một lượt tương tác có hiệu suất kém, hãy thử lặp lại lượt tương tác đó:
- Nếu bạn không thể lặp lại, hãy sử dụng Nhật ký tương tác để có thông tin chi tiết.
- Nếu bạn có thể lặp lại, hãy ghi lại dấu vết trong bảng điều khiển Hiệu suất.
Tất cả các chuyến bay bị hoãn
Hãy thử thêm một chút tất cả các vấn đề này vào trang:
Xem mã đầy đủ: all_the_things.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Sau đó, hãy sử dụng bảng điều khiển và bảng điều khiển hiệu suất để chẩn đoán vấn đề!
13. Thử nghiệm: công việc không đồng bộ
Vì bạn có thể bắt đầu các hiệu ứng không trực quan bên trong các lượt tương tác, chẳng hạn như đưa ra yêu cầu mạng, bắt đầu bộ tính giờ hoặc chỉ cập nhật trạng thái chung, điều gì sẽ xảy ra khi các hiệu ứng đó cuối cùng cập nhật trang?
Miễn là lần hiển thị tiếp theo sau một lượt tương tác được phép hiển thị, ngay cả khi trình duyệt quyết định rằng không thực sự cần cập nhật kết xuất mới, thì quá trình đo lường lượt tương tác sẽ dừng lại.
Để thử cách này, hãy tiếp tục cập nhật giao diện người dùng từ trình theo dõi lượt nhấp, nhưng chạy công việc chặn từ thời gian chờ.
Xem mã đầy đủ: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
Điều gì đang xảy ra?
14. Kết quả thử nghiệm công việc không đồng bộ
Xem mã đầy đủ: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});

Lượt tương tác hiện đã ngắn hơn vì luồng chính có sẵn ngay sau khi giao diện người dùng được cập nhật. Tác vụ chặn dài vẫn chạy, chỉ là chạy sau khi hiển thị, vì vậy, người dùng sẽ nhận được phản hồi ngay lập tức về giao diện người dùng.
Bài học: nếu bạn không thể xoá, ít nhất hãy di chuyển nó!
Phương thức
Chúng ta có thể làm tốt hơn setTimeout 100 mili giây cố định không? Chúng ta có thể vẫn muốn mã chạy nhanh nhất có thể, nếu không thì chúng ta chỉ cần xoá mã đó!
Mục tiêu:
- Hoạt động tương tác sẽ chạy
incrementAndUpdateUI(). blockFor()sẽ chạy sớm nhất có thể, nhưng không chặn lần hiển thị tiếp theo.- Điều này dẫn đến hành vi có thể dự đoán được mà không có "thời gian chờ kỳ diệu".
Một số cách để thực hiện việc này bao gồm:
setTimeout(0)Promise.then()requestAnimationFramerequestIdleCallbackscheduler.postTask()
"requestPostAnimationFrame"
Không giống như requestAnimationFrame (sẽ cố gắng chạy trước lần hiển thị tiếp theo và thường vẫn khiến lượt tương tác bị chậm), requestAnimationFrame + setTimeout tạo ra một polyfill đơn giản cho requestPostAnimationFrame, chạy lệnh gọi lại sau lần hiển thị tiếp theo.
Xem mã đầy đủ: raf+task.html
function afterNextPaint(callback) {
requestAnimationFrame(() => {
setTimeout(callback, 0);
});
}
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
afterNextPaint(() => {
blockFor(1000);
});
});
Để thuận tiện, bạn thậm chí có thể gói nó trong một lời hứa:
Xem mã đầy đủ: raf+task2.html
async function nextPaint() {
return new Promise(resolve => afterNextPaint(resolve));
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await nextPaint();
blockFor(1000);
});
15. Nhiều lượt tương tác (và lượt nhấp do bực bội)
Việc di chuyển các tác vụ cần nhiều thời gian có thể giúp ích, nhưng những tác vụ cần nhiều thời gian đó vẫn chặn trang, ảnh hưởng đến các hoạt động tương tác trong tương lai cũng như nhiều ảnh động và nội dung cập nhật khác trên trang.
Hãy thử lại phiên bản trang có công việc chặn không đồng bộ (hoặc phiên bản của riêng bạn nếu bạn đã nghĩ ra biến thể của riêng mình về việc trì hoãn công việc ở bước cuối cùng):
Xem mã đầy đủ: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
Điều gì sẽ xảy ra nếu bạn nhấp nhiều lần liên tiếp?
Dấu vết hiệu suất
Mỗi lượt nhấp sẽ có một tác vụ cần nhiều thời gian dài một giây được đưa vào hàng đợi tác vụ, đảm bảo luồng chính bị chặn trong một khoảng thời gian đáng kể.

Khi các tác vụ cần nhiều thời gian đó trùng lặp với các lượt nhấp mới, thì các lượt tương tác sẽ diễn ra chậm, ngay cả khi trình nghe sự kiện trả về kết quả gần như ngay lập tức. Chúng tôi đã tạo ra tình huống tương tự như trong thử nghiệm trước đó với độ trễ đầu vào. Chỉ có điều lần này, độ trễ đầu vào không đến từ setInterval, mà đến từ công việc do trình nghe sự kiện trước đó kích hoạt.
Chiến lược
Tốt nhất là chúng ta nên loại bỏ hoàn toàn các tác vụ cần nhiều thời gian!
- Xoá hoàn toàn mã không cần thiết, đặc biệt là tập lệnh.
- Tối ưu hoá mã để tránh chạy các tác vụ cần nhiều thời gian.
- Huỷ công việc cũ khi có hoạt động tương tác mới.
16. Chiến lược 1: debounce
Một chiến lược kinh điển. Bất cứ khi nào các lượt tương tác diễn ra liên tiếp và hiệu ứng xử lý hoặc hiệu ứng mạng tốn kém, hãy trì hoãn việc bắt đầu công việc một cách có chủ đích để bạn có thể huỷ và khởi động lại. Mẫu này rất hữu ích cho các giao diện người dùng như trường tự động hoàn thành.
- Sử dụng
setTimeoutđể trì hoãn việc bắt đầu công việc tốn kém, bằng bộ hẹn giờ, có thể là 500 đến 1000 mili giây. - Lưu mã nhận dạng của đồng hồ hẹn giờ khi bạn làm như vậy.
- Nếu có một lượt tương tác mới, hãy huỷ bộ hẹn giờ trước đó bằng
clearTimeout.
Xem mã đầy đủ: debounce.html
let timer;
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
blockFor(1000);
}, 1000);
});
Dấu vết hiệu suất

Mặc dù có nhiều lượt nhấp, nhưng chỉ có một tác vụ blockFor chạy, chờ cho đến khi không có lượt nhấp nào trong một giây trước khi chạy. Đối với các lượt tương tác diễn ra dồn dập (chẳng hạn như nhập văn bản hoặc các mục tiêu dự kiến sẽ nhận được nhiều lượt nhấp nhanh), đây là chiến lược lý tưởng để sử dụng theo mặc định.
17. Chiến lược 2: làm gián đoạn tác vụ chạy trong thời gian dài
Vẫn có khả năng không may là một lượt nhấp khác sẽ xuất hiện ngay sau khi khoảng thời gian chống dội phím trôi qua, rơi vào giữa tác vụ cần nhiều thời gian đó và trở thành một lượt tương tác rất chậm do độ trễ đầu vào.
Lý tưởng nhất là nếu có một hoạt động tương tác xảy ra giữa chừng trong tác vụ, chúng ta nên tạm dừng công việc đang bận để xử lý ngay mọi hoạt động tương tác mới. Làm cách nào để làm được như vậy?
Có một số API như isInputPending, nhưng thường thì bạn nên chia các tác vụ cần nhiều thời gian thành nhiều phần.
Nhiều setTimeout
Lần thử đầu tiên: làm điều gì đó đơn giản.
Xem mã đầy đủ: small_tasks.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
});
});
Cách này hoạt động bằng cách cho phép trình duyệt lên lịch cho từng tác vụ riêng lẻ và hoạt động đầu vào có thể có mức độ ưu tiên cao hơn!

Chúng ta quay lại với 5 giây làm việc cho 5 lượt nhấp, nhưng mỗi tác vụ 1 giây cho mỗi lượt nhấp đã được chia thành 10 tác vụ 100 mili giây. Do đó, ngay cả khi có nhiều lượt tương tác trùng lặp với các tác vụ đó, thì không có lượt tương tác nào có độ trễ đầu vào vượt quá 100 mili giây! Trình duyệt ưu tiên trình nghe sự kiện sắp tới hơn công việc setTimeout và các hoạt động tương tác vẫn phản hồi nhanh.
Chiến lược này đặc biệt hiệu quả khi lên lịch cho các điểm truy cập riêng biệt, chẳng hạn như khi bạn có một loạt tính năng độc lập cần gọi tại thời điểm tải ứng dụng. Theo mặc định, việc chỉ tải tập lệnh và chạy mọi thứ tại thời điểm đánh giá tập lệnh có thể chạy mọi thứ trong một tác vụ cần nhiều thời gian theo mặc định.
Tuy nhiên, chiến lược này không hiệu quả trong việc phân tách mã liên kết chặt chẽ, chẳng hạn như vòng lặp for sử dụng trạng thái dùng chung.
Giờ đây, bạn có thể dùng yield()
Tuy nhiên, chúng ta có thể tận dụng async và await hiện đại để dễ dàng thêm "điểm dừng" vào bất kỳ hàm JavaScript nào.
Ví dụ:
Xem mã đầy đủ: yieldy.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldy(ms) {
const ms_per_part = 10;
const parts = ms / ms_per_part;
for (let i = 0; i < parts; i++) {
await schedulerDotYield();
blockFor(ms_per_part);
}
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await blockInPiecesYieldy(1000);
});
Như trước đây, luồng chính được tạo ra sau một phần công việc và trình duyệt có thể phản hồi mọi hoạt động tương tác sắp tới, nhưng giờ đây, tất cả những gì cần thiết là một await schedulerDotYield() thay vì setTimeout riêng biệt, giúp luồng này đủ tiện dụng để sử dụng ngay cả khi đang ở giữa vòng lặp for.
Giờ đây, bạn có thể dùng AbortContoller()
Cách này có hiệu quả, nhưng mỗi lượt tương tác sẽ lên lịch cho nhiều công việc hơn, ngay cả khi có lượt tương tác mới và có thể đã thay đổi công việc cần thực hiện.
Với chiến lược chống dội phím, chúng tôi đã huỷ thời gian chờ trước đó với mỗi lượt tương tác mới. Chúng ta có thể làm điều tương tự ở đây không? Một cách để thực hiện việc này là sử dụng AbortController():
Xem mã đầy đủ: aborty.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldyAborty(ms, signal) {
const parts = ms / 10;
for (let i = 0; i < parts; i++) {
// If AbortController has been asked to stop, abandon the current loop.
if (signal.aborted) return;
await schedulerDotYield();
blockFor(10);
}
}
let abortController = new AbortController();
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
abortController.abort();
abortController = new AbortController();
await blockInPiecesYieldyAborty(1000, abortController.signal);
});
Khi có một lượt nhấp, vòng lặp blockInPiecesYieldyAborty for sẽ bắt đầu thực hiện bất kỳ công việc nào cần làm, đồng thời định kỳ nhường luồng chính để trình duyệt tiếp tục phản hồi các lượt tương tác mới.
Khi có lượt nhấp thứ hai, vòng lặp đầu tiên được gắn cờ là đã huỷ bằng AbortController và một vòng lặp blockInPiecesYieldyAborty mới được bắt đầu. Lần tiếp theo vòng lặp đầu tiên được lên lịch chạy lại, vòng lặp này sẽ nhận thấy rằng signal.aborted hiện là true và ngay lập tức trả về mà không thực hiện thêm công việc nào.

18. Kết luận
Việc chia nhỏ tất cả tác vụ cần nhiều thời gian giúp trang web phản hồi các lượt tương tác mới. Điều này giúp bạn nhanh chóng đưa ra ý kiến phản hồi ban đầu, cũng như đưa ra các quyết định như huỷ bỏ công việc đang thực hiện. Đôi khi, điều đó có nghĩa là bạn phải lên lịch cho các điểm truy cập dưới dạng các tác vụ riêng biệt. Đôi khi, điều đó có nghĩa là thêm các điểm "yield" (nhường) ở nơi thuận tiện.
Ghi nhớ
- INP đo lường tất cả các lượt tương tác.
- Mỗi lượt tương tác được đo lường từ hoạt động đầu vào đến lần hiển thị tiếp theo – cách người dùng nhìn thấy khả năng phản hồi.
- Độ trễ đầu vào, thời gian xử lý sự kiện và độ trễ hiển thị đều ảnh hưởng đến khả năng phản hồi của hoạt động tương tác.
- Bạn có thể dễ dàng đo lường INP và thông tin chi tiết về lượt tương tác bằng Công cụ cho nhà phát triển!
Chiến lược
- Đừng sử dụng mã chạy trong thời gian dài (tác vụ cần nhiều thời gian) trên các trang của bạn.
- Di chuyển mã không cần thiết ra khỏi trình nghe sự kiện cho đến sau lần vẽ tiếp theo.
- Đảm bảo rằng bản thân việc cập nhật kết xuất là hiệu quả cho trình duyệt.