Tìm hiểu lượt tương tác với nội dung hiển thị tiếp theo (INP)

1. Giới thiệu

Bản minh hoạ tương tác và lớp học lập trình để tìm hiểu về Lượt tương tác với Next Paint (INP).

Sơ đồ mô tả một hoạt động tương tác trên luồng chính. Người dùng nhập thông tin đầu vào trong khi chặn chạy các tác vụ. Hoạt động nhập dữ liệu bị trì hoãn cho đến khi các tác vụ đó hoàn tất. Sau đó, các trình nghe sự kiện nhấp chuột, di chuột lên và trỏ chuột sẽ chạy, sau đó hoạt động kết xuất và tô màu được bắt đầu cho đến khi khung tiếp theo hiển thị

Điều kiện tiên quyết

  • Có kiến thức về phát triển HTML và JavaScript.
  • Khuyến nghị: đọc tài liệu về INP.

Kiến thức bạn sẽ học được

  • Tác động tương tác giữa các 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 thiểu 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ột 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 hoạt động hiệu quả cho tất cả số liệu đo lường lượt tương tác.

2. Bắt đầu thiết lập

Lấy và chạy mã

Bạn có thể tìm thấy mã này trong kho lưu trữ web-vitals-codelabs.

  1. Sao chép kho lưu trữ trong thiết bị đầu cuối của bạn: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. Chuyển vào thư mục đã sao chép: cd web-vitals-codelabs/understanding-inp
  3. Cài đặt phần phụ thuộc: npm ci
  4. Khởi động máy chủ web: npm run start
  5. Truy cập vào http://localhost:5173/understanding-inp/ trên trình duyệt của bạn

Tổng quan về ứng dụng

Nằm ở đầu trang là bộ đếm Điểm số và nút Tăng. Một bản minh hoạ kinh điển về khả năng phản ứng và khả năng phản ứng!

Ảnh chụp màn hình ứng dụng minh hoạ cho lớp học lập trình này

Bên dưới nút này có 4 giá trị đo:

  • INP: điểm INP hiện tại, thường là tương tác kém nhất.
  • Tương tác: điểm số của lần tương tác gần đây nhất.
  • FPS: số khung hình/giây của luồng chính trên trang.
  • Timer: một ảnh động bộ tính giờ đang chạy để giúp trực quan hoá hiện tượng giật.

Các mục nhập FPS và Timer 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 việc trực quan hoá khả năng phản hồi trở nên dễ dàng hơn một chút.

Dùng thử

Hãy thử tương tác với nút Tăng dần và xem điểm số tăng lên. Các giá trị INPTương tác có thay đổi theo từng mức tăng không?

INP đo lường khoả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ị bản cập nhật được hiển thị cho người dùng.

3. Đo lường hoạt động tương tác với Công cụ của Chrome cho nhà phát triển

Mở Công cụ cho nhà phát triển trong phần Công cụ khác > Trình đơn 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ẽ sử dụng để đo lường lượt tương tác.

Ảnh chụp màn hình bảng điều khiển Hiệu suất trong Công cụ cho nhà phát triển bên cạnh ứng dụng

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.

  1. Nhấn vào nút Giữ để quay.
  2. Tương tác với trang (nhấn vào nút Increment (Tăng dần).
  3. Dừng bản ghi.

Trong tiến trình hiện ra, bạn sẽ thấy một kênh theo dõi Lượt tương tác. Mở rộng bằng cách nhấp vào hình tam giác ở bên trái.

Hình minh hoạ dạng ảnh động về việc ghi lại một hoạt động tương tác bằng bảng điều khiển hiệu suất của Công cụ cho nhà phát triển

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

Ảnh chụp màn hình bảng điều khiển Hiệu suất trong Công cụ cho nhà phát triển, con trỏ di chuột qua lượt tương tác trong bảng điều khiển và phần chú thích công cụ liệt kê thời gian ngắn của lượt tương tác đó

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 chóng, không dành thời gian cho thời lượng xử lý và một khoảng thời gian tối thiểu cho độ trễ đầu vàođộ trễ hiển thị. Khoảng thời gian chính xác 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 huỷ nhận xét về 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ể.

Theo dõi hiệu suất

Hãy quay một lần nữa trong bảng Hiệu suất để xem video này trông như thế nào.

Một lượt tương tác dài một giây trong bảng điều khiển Hiệu suất

Hành động tương tác trước đây chỉ mất trọn một giây.

Khi di chuột qua hoạt động tương tác này, bạn có thể nhận thấy rằng thời gian gần như hoàn toàn dành cho "Thời gian xử lý". Đây là lượng thời gian cần thiết để thực thi các lệnh gọi lại của trình nghe sự kiện. Vì lệnh gọi chặn blockFor hoàn toàn nằm trong trình nghe sự kiện, nên thời gian sẽ trôi qua.

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 hiệu quả đố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 lệnh gọi js—trước tiên, hãy cập nhật giao diện người dùng, sau đó chặn?

Xem mã đầy đủ: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Bạn có thấy giao diện người dùng xuất hiện trước đó không? Lệnh này có ảnh hưởng đến điểm INP không?

Hãy thử theo dõi và kiểm tra hoạt động tương tác để xem có sự khác biệt nào không.

Tách biệt người nghe

Nếu bạn di chuyển công việc sang một trình nghe sự kiện riêng biệt thì sao? 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 đủ: Two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

Hiện tại, trang hiệu suất xuất hiện như thế nào trong bảng hiệu suất?

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 nét/làm mờ và sự kiện tổng hợp (như sự kiện thay đổi trước và sự kiện trước khi nhập).

Nhiều trang thực có trình nghe nhiều sự kiện.

Điều gì 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ã đầy đủ: 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ì 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. Xử lý kết quả thử nghiệm về khoảng thời gian xử lý

Theo dõi hiệu suất: trước tiên, hãy cập nhật giao diện người dùng

Xem mã đầy đủ: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Khi xem bảng điều khiển Hiệu suất khi nhấp vào nút, bạn có thể thấy kết quả không thay đổi. Mặc dù quá trình 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 hiển thị trên màn hình cho đến khi trình nghe sự kiện hoàn tất, tức là lượt tương tác vẫn chỉ mất hơn một giây để hoàn tất.

Một hoạt động tương tác vẫn dài một giây trong bảng điều khiển Hiệu suất

Theo dõi hiệu suất: các trình nghe riêng biệt

Xem mã đầy đủ: Two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

Xin nhắc lại, về mặt chức năng thì không có sự khác biệt. Hoạt động tương tác này vẫn diễn ra trong một giây.

Nếu phóng to hoạt động tương tác nhấp chuột, bạn sẽ thấy thực sự có 2 hàm khác nhau đang được gọi do sự kiện click.

Đúng như dự kiến, phiên bản đầu tiên (cập nhật giao diện người dùng) sẽ chạy cực nhanh, trong khi phiên bản thứ hai chỉ mất trọn một giây. Tuy nhiên, tổng cộng các tác động của chúng lại dẫn đến cùng một lượt tương tác chậm đến người dùng cuối.

Chế độ xem phóng to về tương tác dài một giây trong ví dụ này, thể hiện lệnh gọi hàm đầu tiên mất chưa đến một mili giây để hoàn tất

Theo dõi hiệu suất: nhiều loại sự kiện

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

Các kết quả này rất giống nhau. Tương tác vẫn diễn ra trọn vẹn trong một giây; điểm khác biệt duy nhất là trình nghe click chỉ dùng để cập nhật giao diện người dùng ngắn hơn hiện chạy sau trình nghe chặn pointerup.

Hình ảnh phóng to về hoạt động tương tác dài một giây trong ví dụ này, thể hiện trình nghe sự kiện nhấp chuột mất chưa đến một mili giây để hoàn tất, sau trình nghe con trỏ lên

Theo dõi 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 thành phần web mặc định (nhập biểu mẫu), nhập văn bản, đánh dấu văn bản đều sẽ tiếp tục được cập nhật.

Trong trường hợp này, nút chuyển sang trạng thái đang 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 vẽ, có nghĩa là vẫn còn INP.

Vì trình nghe sự kiện đã chặn luồng chính trong một giây khiến trang không hiển thị được, nên hoạt động tương tác vẫn mất một giây trọn vẹn.

Việc ghi lại bảng điều khiển Hiệu suất cho thấy hoạt động tương tác gần như giống với các hoạt động tương tác trước đó.

Một hoạt động tương tác vẫn dài một giây trong bảng điều khiển Hiệu suất

Đồ ăn mang đi

Bất kỳ mã nào chạy trong bất kỳ trình nghe sự kiện nào cũng sẽ trì hoãn hoạt động tương tác.

  • Số liệu này bao gồm các trình nghe đã đăng ký từ nhiều tập lệnh và mã khung hoặc mã thư viện chạy trong trình nghe, chẳng hạn như yêu cầu 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 của tất cả các tập lệnh của bên thứ ba.

Đây là một vấn đề thông thường!

Cuối cùng: chỉ vì mã của bạn không kích hoạt sự kiện vẽ không có nghĩa là việc vẽ sẽ không chờ trình nghe sự kiện chậm hoàn tất.

7. Thử nghiệm: độ trễ nhập

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ụ:

  • Trường hợp bạn có một <script> tải muộn đã chặn ngẫu nhiên trang trong quá trình tải.
  • Một lệnh gọi API, chẳng hạn như setInterval, có định kỳ chặn trang?

Hãy thử xoá blockFor khỏi trình nghe sự kiện rồi 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ễ nhập

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 khi tác vụ chặn setInterval đang chạy sẽ dẫn đến một tương tác kéo dài, ngay cả khi không có tác vụ chặn nào được thực hiện trong chính tương tác đó!

Những khoảng thời gian dài này thường được gọi là tác vụ dài.

Khi di chuột qua lượt tương tác trong Công cụ cho nhà phát triển, bạn có thể thấy thời gian tương tác hiện chủ yếu được quy cho độ trễ đầu vào, chứ không phải thời lượng xử lý.

Bảng điều khiển Hiệu suất Công cụ cho nhà phát triển cho thấy tác vụ chặn một giây, một tương tác xuất hiện một phần thông qua tác vụ đó và một lượt tương tác 642 mili giây, chủ yếu là do độ trễ đầu vào

Lưu ý rằng điều này không luôn ảnh hưởng đến tương tác! Nếu bạn không nhấp vào khi tác vụ đang chạy, bạn có thể sẽ gặp may. Thật "ngẫu nhiên" hắt hơi có thể là cơn ác mộng cần gỡ lỗi vì đôi khi chúng chỉ gây ra vấn đề.

Một cách để theo dõi các chỉ số này là đo lường các tác vụ dài (hoặc Khung ảnh động dài) 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 điều gì khác ảnh hưởng đến việc hiển thị lần hiển thị tiếp theo?

Ồ, việc cập nhật trang bằng các hiệu ứng đắt đỏ!

Ngay cả khi việc cập nhật trang diễn ra nhanh chóng, trình duyệt có thể vẫn phải nỗ lực để kết xuất các trang này!

Trên luồng chính:

  • Các khung giao diện người dùng cần hiển thị bản cập nhật sau khi trạng thái thay đổi
  • Các thay đổi DOM hoặc chuyển đổi 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à Sự vẽ.

Bên ngoài chuỗi chính:

  • Sử dụng CSS để tăng cường hiệu ứng GPU
  • Thêm hình ảnh rất lớn có độ phân giải cao
  • Sử dụng SVG/Canvas để vẽ các cảnh phức tạp

Phác hoạ các thành phần kết xuất trên web

RenderingNG

Một số ví dụ thường thấy trên web:

  • Một trang web SPA xây dựng lại toàn bộ DOM sau khi nhấp vào một đường liên kết mà không cần tạm dừng để đưa ra phản hồi trực quan 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 vẫn chạy các trình nghe tốn kém.
  • 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ễ trình bày

requestAnimationFrame có tốc độ chậm

Hãy mô phỏng độ trễ khi trình bày dài bằng API requestAnimationFrame().

Di chuyển lệnh gọi blockFor vào lệnh gọi lại requestAnimationFrame để lệnh này chạy sau khi trình nghe sự kiện trả về:

Xem mã đầy đủ: layout_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 đủ: layout_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Hoạt động 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 gọi lại trước lần hiển thị tiếp theo. Vì INP đo thời gian từ khi tương tác đến lần vẽ tiếp theo, nên blockFor(1000) trong requestAnimationFrame sẽ tiếp tục chặn lớp vẽ tiếp theo trong trọn một giây.

Một hoạt động tương tác vẫn dài một giây trong bảng điều khiển Hiệu suất

Tuy nhiên, hãy chú ý hai điều:

  • Khi di chuột, bạn sẽ thấy toàn bộ thời gian tương tác hiện được dành cho "độ trễ trình bày" vì việc chặn luồng chính sẽ diễn ra sau khi trình nghe sự kiện trả về.
  • Gốc của hoạt động trong luồng chính không còn là sự kiện nhấp chuột nữa, mà là "Ảnh động đã kích hoạt khung hình".

12. Chẩn đoán lượt tương tác

Trên trang thử nghiệm này, khả năng phản hồi cực kỳ 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 thì phản hồi lại tinh tế hơn.

Khi các hoạt động tương tác kéo dài, không phải lúc nào chúng tôi cũng biết rõ nguyên nhân là gì. Đó có phải là:

  • Độ trễ khi nhập?
  • Thời lượng 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 đều có thể sử dụng Công cụ cho nhà phát triển để giúp đo lường khả năng phản hồi. Để tập thói quen, hãy thử quy trình sau:

  1. Điều hướng trên web như bạn vẫn thường làm.
  2. Không bắt buộc: để bảng điều khiển Công cụ cho nhà phát triển mở trong khi tiện ích Chỉ số quan trọng web ghi lại các hoạt động tương tác.
  3. Nếu bạn thấy một tương tác hoạt động kém, hãy thử lặp lại tương tác đó:
  • Nếu bạn không thể lặp lại, hãy sử dụng nhật ký của bảng điều khiển để thu thập thông tin chi tiết.
  • Nếu bạn có thể lặp lại, hãy ghi lại nội dung đó trong bảng hiệu suất.

Tất cả sự chậm trễ

Hãy thử thêm một chút tất cả những 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 phải hình ảnh bên trong hoạt động tương tác, chẳng hạn như tạo 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, nên điều gì sẽ xảy ra khi những hiệu ứng đó cuối cùng cập nhật trang?

Miễn là lớp hiển thị tiếp theo sau khi một lượt tương tác được cho phép hiển thị, ngay cả khi trình duyệt quyết định rằng hoạt động này không thực sự cần bản cập nhật kết xuất mới, thì quá trình đo lường Tương tác sẽ dừng lại.

Để thử thực hiện thao tác này, hãy tiếp tục cập nhật giao diện người dùng từ trình nghe lượt nhấp, nhưng chạy tác vụ 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);
});

Một tương tác 27 mili giây với một tác vụ dài một giây hiện xảy ra sau đó trong dấu vết

Lượt tương tác hiện ngắ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 mất nhiều thời gian vẫn chạy, chỉ chạy đôi khi sau khi vẽ, vì vậy, người dùng sẽ nhận được phản hồi về giao diện người dùng ngay lập tức.

Bài học: nếu bạn không thể xoá thì ít nhất cũng phải di chuyển nó!

Phương thức

Chúng ta có thể làm tốt hơn một setTimeout cố định 100 mili giây không? Chúng ta vẫn muốn đoạn mã này chạy càng nhanh càng tốt, nếu không thì chúng ta nên xoá nó đi!

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ớp vẽ tiếp theo.
  • Điều này dẫn đến hành vi có thể dự đoán mà không có hiện tượng "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()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

"requestPostAnimationFrame"

Không giống như requestAnimationFrame (sẽ cố gắng chạy trước lớp vẽ tiếp theo và thường vẫn tạo ra tương tác chậm), requestAnimationFrame + setTimeout tạo một polyfill đơn giản cho requestPostAnimationFrame, chạy lệnh gọi lại sau lớp vẽ tiếp theo.

Xem mã đầy đủ: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

Đối với bài viết công thái học, bạn thậm chí có thể gói gọ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 tương tác (và nhấp chuột nổi loạn)

Việc di chuyển các công việc chặn dài có thể giúp ích, nhưng những tác vụ dài đó vẫn chặn trang, ảnh hưởng đến các tương tác trong tương lai cũng như nhiều hoạt ảnh và nội dung cập nhật khác trên trang.

Thử lại phiên bản công việc chặn không đồng bộ của trang (hoặc của riêng bạn nếu bạn đưa 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ì xảy ra nếu bạn nhấp nhanh nhiều lần?

Theo dõi hiệu suất

Đối với mỗi lượt nhấp, có một tác vụ dài một giây được đưa vào hàng đợi để đảm bảo luồng chính bị chặn trong một khoảng thời gian đáng kể.

Nhiều tác vụ dài giây trong luồng chính, gây ra các hoạt động tương tác chậm đến 800 mili giây

Khi những nhiệm vụ dài đó trùng lặp với các lượt nhấp mới xuất hiện, điều này sẽ dẫn đến tương tác chậm mặc dù chính trình nghe sự kiện sẽ trả về gần như ngay lập tức. Chúng tôi đã tạo ra trường hợp tương tự như trong thử nghiệm trước đó với độ trễ đầu vào. Chỉ lần này, độ trễ đầu vào không phải là từ setInterval mà là từ công việc do các 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 xoá hoàn toàn các tác vụ dài!

  • Xoá hoàn toàn các mã không cần thiết, đặc biệt là các tập lệnh.
  • Tối ưu hoá mã để tránh chạy các tác vụ mất nhiều thời gian.
  • Huỷ công việc cũ khi có lượt tương tác mới.

16. Chiến lược 1: gỡ cài đặt

Một chiến lược cổ điển. Bất cứ khi nào các hoạt động tương tác xuất hiện liên tiếp nhanh chóng và việc xử lý hoặc hiệu ứng mạng rất tốn kém, hãy trì hoãn bắt đầu công việc một cách cố ý để bạn có thể huỷ rồi khởi động lại. Mẫu này hữu ích cho giao diện người dùng, chẳng hạn như các 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, với bộ tính giờ, khoảng từ 500 đến 1.000 mili giây.
  • Lưu mã bộ tính giờ khi bạn thực hiện việc này.
  • Nếu có lượt tương tác mới, hãy huỷ bộ tính giờ trước đó bằng cách sử dụng clearTimeout.

Xem mã đầy đủ: debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

Theo dõi hiệu suất

Nhiều tương tác, nhưng chỉ có một nhiệm vụ công việc kéo dài từ tất cả các tương tác đó

Mặc dù có nhiều lượt nhấp, nhưng chỉ có một tác vụ blockFor kết thúc bằng cách đợi cho đến khi không có bất kỳ 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 hàng loạt (như nhập văn bản hoặc các mục tiêu mặt hàng dự kiến 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 công việc chạy trong thời gian dài

Vẫn có khả năng không may là một lượt nhấp tiếp theo sẽ xuất hiện ngay sau khi khoảng thời gian gỡ bỏ đã trôi qua, sẽ xảy ra ở giữa tác vụ dài đó và trở thành một tương tác rất chậm do độ trễ đầu vào.

Lý tưởng nhất là nếu một lượt tương tác xuất hiện giữa lúc đang thực hiện công việc, chúng ta nên tạm dừng công việc bận rộn của mình để xử lý ngay mọi tương tác mới. Chúng tôi có thể làm gì để làm được điều đó?

Có một số API như isInputPending, nhưng thường thì bạn nên chia các tác vụ dài thành nhiều phần.

Rất 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);
  });
});

Tính năng này hoạt động bằng cách cho phép trình duyệt lên lịch riêng cho từng công việc và hoạt động đầu vào có thể được ưu tiên hơn!

Nhiều lượt tương tác, nhưng tất cả công việc theo lịch đã chia nhỏ thành nhiều tác vụ nhỏ hơn

Chúng ta quay lại làm việc với toàn bộ 5 giây cho 5 lượt nhấp, nhưng mỗi nhiệm vụ 1 giây cho mỗi lượt nhấp đã được chia thành 10 nhiệm vụ 100 mili giây. Do đó, ngay cả khi có nhiều tương tác chồng chéo với các tác vụ đó, không có tương tác nào có độ trễ đầu vào quá 100 mili giây! Trình duyệt sẽ ưu tiên trình nghe sự kiện đến hơn công việc setTimeout, còn các hoạt động tương tác sẽ vẫn phản hồi.

Chiến lược này đặc biệt hiệu quả khi lập lịch các điểm truy cập riêng biệt, chẳng hạn như khi bạn có nhiều tính năng độc lập cần gọi vào thời gian tải ứng dụng. Theo mặc định, chỉ cần tải tập lệnh và chạy mọi thứ tại thời điểm bắt đầu của tập lệnh là có thể chạy mọi thứ trong một tác vụ rất lớn.

Tuy nhiên, chiến lược này cũng không hoạt động hiệu quả để chia nhỏ mã được 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.

Hiện có mặt yield()

Tuy nhiên, chúng ta có thể tận dụng asyncawait hiện đại để dễ dàng thêm "điểm lợi nhuận" vào bất kỳ hàm JavaScript nào.

Ví dụ:

Xem mã đầy đủ: intenty.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 tương tác sắp tới, nhưng giờ đây, tất cả những gì cần thiết là await schedulerDotYield() thay vì các setTimeout riêng biệt, giúp đảm bảo tính tiện dụng để sử dụng ngay cả khi đang ở giữa vòng lặp for.

Hiện có mặt AbortContoller()

Như vậy có hiệu quả, nhưng mỗi lượt tương tác lên lịch làm việc nhiều hơn, ngay cả khi có những lượt tương tác mới và có thể đã thay đổi công việc cần hoàn thành.

Với chiến lược loại bỏ, 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 tôi 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 một lượt nhấp xuất hiện, nó sẽ bắt đầu vòng lặp blockInPiecesYieldyAborty for thực hiện mọi công việc cần thiết trong khi định kỳ tạo luồng chính để trình duyệt vẫn thích ứng với các lượt tương tác mới.

Khi lượt nhấp thứ hai xuất hiện, vòng lặp đầu tiên được gắn cờ là đã huỷ bằng AbortController và vòng lặp blockInPiecesYieldyAborty mới sẽ bắt đầu. Vào lần tiếp theo vòng lặp đầu tiên được lên lịch để chạy lại, hệ thống nhận thấy signal.aborted hiện đã true và sẽ trả về ngay lập tức mà không cần làm gì thêm.

Công việc trên luồng chính hiện có nhiều phần nhỏ, hoạt động tương tác diễn ra ngắn và chỉ kéo dài khi cần

18. Kết luận

Việc chia nhỏ tất cả các tác vụ dài cho phép trang web phản hồi với các tương tác mới. Điều này giúp bạn nhanh chóng cung cấp ý kiến phản hồi ban đầu, đồng thời đưa ra các quyết định, chẳng hạn như huỷ công việc đang thực hiện. Đôi khi, điều đó có nghĩa là lên lịch 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 "lợi nhuận" các điểm khác khi thuận tiện.

Lưu ý

  • INP đo lường tất cả các tương tác.
  • Mỗi lượt tương tác được đo lường từ đầu vào cho đế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 lượng xử lý sự kiện và độ trễ khi trình bày tất cả đều ảnh hưởng đến khả năng phản hồi của tương tác.
  • Bạn có thể dễ dàng đo lường INP và bảng phân tích tương tác bằng Công cụ cho nhà phát triển!

Chiến lược

  • Không có đoạn mã chạy trong thời gian dài (tác vụ dài) 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 hiển thị tiếp theo.
  • Đảm bảo bản cập nhật kết xuất đã mang lại hiệu quả cho trình duyệt.

Tìm hiểu thêm