Thông tin về lớp học lập trình này
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
- Có 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ý những 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.
- Một trình chỉnh sửa văn bản.
- Phiên bản Chrome mới nhất để tất cả các chỉ số đ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ã này trong kho lưu trữ web-vitals-codelabs
.
- Sao chép kho lưu trữ trong thiết bị đầu cuối:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- Chuyển đến 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 có một bộ đếm Điểm và nút Tăng. Một bản minh hoạ kinh điển về tính phản ứng và khả năng phản hồi!
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.
- 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à Timer hoàn toàn không cần thiết để đo lường lượt tương tác. Các đường này được thêm vào chỉ để giúp bạn dễ dàng hình dung khả năng phản hồi hơn.
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 khoảng thời gian tính từ thời điểm người dùng tương tác cho đến khi trang thực sự cho người dùng thấy nội dung cập nhật đã hiển thị.
3. Đo lường các hoạt động tương tác bằng Công cụ của Chrome cho nhà phát triển
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 bằng cách sử dụng tổ hợp phím.
Chuyển sang bảng 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 vào nút Tăng).
- Dừng ghi.
Trong dòng thời gian kết quả, bạn sẽ thấy một bản ghi Tương tác. Mở rộng bằng cách nhấp vào biểu tượng hình tam giác ở bên trái.
Hai lượt tương tác sẽ xuất hiện. Phóng to hình thứ hai bằng cách di chuyển hoặc giữ phím W.
Khi di chuột lên hoạt động tương tác, bạn có thể thấy hoạt động tương tác diễn ra nhanh chóng, không mất thời gian trong thời gian xử lý và mất một khoảng thời gian tối thiểu trong độ trễ đầu vào và độ trễ hiển thị, độ dài chính xác của các khoảng thời gian này sẽ phụ thuộc vào tốc độ của máy.
4. Trình xử lý sự kiện chạy trong thời gian dài
Mở tệp index.js
và bỏ chú thí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ẽ nhận thấy thay đổi này 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 hoạt động tương tác sẽ diễn ra 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.
Trước đây, một lượt tương tác ngắn chỉ mất 1 giây.
Khi bạn di chuột lên hoạt động tương tác, hãy lưu ý rằng thời gian gần như hoàn toàn được dành cho "Thời gian xử lý", đây là khoả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 blockFor
chặn hoàn toàn nằm trong trình nghe sự kiện, nên đó là nơi thời gian trôi qua.
5. Thử nghiệm: thời gian xử lý
Thử sắp xếp lại công việc của trình nghe sự kiện để xem ảnh hưởng đến INP.
Cập nhật giao diện người dùng trước
Điều gì sẽ 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 toàn bộ mã: 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ó sự khác biệt nào không.
Tách riêng các trình nghe
Điều gì sẽ xảy ra nếu bạn chuyển thao tác này 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 từ một trình nghe riêng biệt.
Xem toàn bộ mã: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Bây giờ, chỉ số này trông như thế nào trong bảng điều khiển hiệu suất?
Các loại sự kiện
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/mất tiêu điểm và sự kiện tổng hợp như beforechange và beforeinput.
Nhiều trang thực 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 các loại sự kiện cho trình nghe sự kiện? Ví dụ: bạn có thể thay thế một trong các trình nghe sự kiện click
bằng pointerup
hoặc mouseup
không?
Xem toàn bộ mã: 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: trước tiên, hãy cập nhật giao diện người dùng
Xem toàn bộ mã: 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ề thao tác nhấp vào nút này, bạn có thể thấy rằng 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 thực sự không 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. Điều này có nghĩa là tương tác vẫn mất hơn một giây để hoàn tất.
Dấu vết hiệu suất: các trình nghe riêng biệt
Xem toàn bộ mã: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Xin nhắc lại, về chức năng thì không có gì khác biệt. Tương tác vẫn mất một giây.
Nếu phóng to tương tác nhấp, bạn sẽ thấy rằng thực sự có 2 hàm khác nhau được gọi do sự kiện click
.
Như dự kiến, thao tác đầu tiên (cập nhật giao diện người dùng) chạy cực kỳ nhanh, trong khi thao tác thứ hai mất trọn một giây. Tuy nhiên, tổng các hiệu ứng này sẽ dẫn đến cùng một tương tác chậm đối với 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 diễn ra trong một giây đầy đủ; điểm khác biệt duy nhất là trình nghe click
chỉ cập nhật giao diện người dùng ngắn hơn hiện chạy sau trình nghe pointerup
chặn.
Dấu vết hiệu suất: không có nội dung 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, các thao tác mặc định của thành phần web (đầu vào biểu mẫu), mục nhập văn bản, việc làm nổi bật văn bản đều tiếp tục đượ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 rồi quay lại khi được nhấp vào. Điều này đòi hỏi 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 được vẽ, nên lượt tương tác vẫn mất một giây.
Việc ghi lại một bảng điều khiển Hiệu suất cho thấy tương tác gần như giống hệt với những tương tác trước đó.
Thông tin nổi bật
Mọi mã chạy trong bất kỳ trình nghe sự kiện nào sẽ làm chậm hoạt động tương tác.
- Điều này bao gồm cá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ư một 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 cả tất cả cá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 thao tác kết xuất không có nghĩa là thao tác kết xuất sẽ không chờ các trình nghe sự kiện chậm hoàn tất.
7. Thử nghiệm: độ trễ khi 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ụ:
- Nếu bạn có một
<script>
tải muộn chặn trang một cách ngẫu nhiên trong quá trình 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 blockFor
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 độ trễ khi 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 trong khi tác vụ chặn setInterval
đang chạy sẽ dẫn đến một lượt tương tác diễn ra trong thời gian dài, ngay cả khi không có tác vụ 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 dài này thường được gọi là tác vụ dài.
Khi di chuột lên 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 là do độ trễ đầu vào, chứ không phải thời gian xử lý.
Xin lưu ý rằng điều này không phải lúc nào cũng ả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, có thể bạn sẽ gặp may. Những lầ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ì chúng chỉ đôi khi gây ra vấn đề.
Một cách để theo dõi các thao tác này là thông qua việc đo lường các thao tác có thời gian thực hiện dài (hoặc Khung hình động có thời gian thực hiện dài) và Tổng thời gian chặn.
9. 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. Tuy nhiên, còn yếu tố nào khác ảnh hưởng đến lần hiển thị tiếp theo?
Chà, cập nhật trang bằng các hiệu ứng đắt tiền!
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 hết công suất để hiển thị các trang đó!
Trên luồng chính:
- Các 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 chuyển đổi nhiều bộ chọn truy vấn CSS tốn kém có thể kích hoạt nhiều Style, Layout và Paint.
Ngoài luồng chính:
- Sử dụng CSS để hỗ trợ các hiệu ứng GPU
- Thêm hình ảnh có độ phân giải cao với 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 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 tạm dừng để cung cấp 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 chạy các trình nghe tốn kém để thực hiện việc này.
- Nút bật/tắt chế độ tối sẽ 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 toàn bộ mã: 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 độ trễ khi trình bày
Xem toàn bộ mã: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Tương tác vẫn diễn ra trong 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 kết xuất tiếp theo. Vì INP đo lường thời gian từ lượt tương tác đến nội dung hiển thị tiếp theo, nên blockFor(1000)
trong requestAnimationFrame
sẽ tiếp tục chặn nội dung 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 sử dụng trong "độ trễ trình bày" 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 trên luồng chính không còn là sự kiện nhấp mà là "Animation Frame Fired" (Khung hình động đã kích hoạt).
12. Chẩn đoán các lượt tương tác
Trên trang kiểm thử này, khả năng phản hồi rất trực quan, với các điểm số, bộ hẹn giờ và giao diện người dùng bộ đếm...nhưng khi kiểm thử trang trung bình, khả năng phản hồi sẽ tinh tế hơn.
Khi các hoạt động 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. Có phải là:
- Độ trễ khi nhập thông tin?
- Thời gian xử lý sự kiện?
- Độ trễ khi trình bày?
Trên trang bất kỳ mà 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 phản hồi. Để 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ố trực tiếp của bảng điều khiển Hiệu suất trong Công cụ cho nhà phát triển.
- Nếu bạn thấy một lượt tương tác hoạt động kém hiệu quả, 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 để nắm được thông tin chi tiết.
- Nếu 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 trường hợp chậm trễ
Hãy thử thêm một chút của tất cả những vấn đề này vào trang:
Xem toàn bộ mã: 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 hiệu suất để chẩn đoán các 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 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ộ hẹn giờ hoặc chỉ cần cập nhật trạng thái chung, đ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ần hiển thị tiếp theo sau một hoạt động tương tác được phép kết xuất, ngay cả khi trình duyệt quyết định rằng trình duyệt không thực sự cần một bản cập nhật kết xuất mới, thì hoạt động đo lường Tương tác sẽ dừng.
Để thử điều 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 thao tác chặn từ thời gian chờ.
Xem toàn bộ mã: 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 toàn bộ mã: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
Giờ đây, hoạt động tương tác diễn ra trong thời gian 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 dài vẫn chạy, chỉ là chạy vào thời điểm nào đó 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 trên giao diện người dùng.
Bài học: nếu không thể xoá, hãy di chuyển!
Phương thức
Chúng ta có thể làm tốt hơn 100 mili giây setTimeout
cố định không? Có lẽ chúng ta vẫn muốn mã chạy nhanh nhất có thể, nếu không thì chúng ta nên xoá mã đó!
Mục tiêu:
- Tương tác sẽ diễn ra trong
incrementAndUpdateUI()
. blockFor()
sẽ chạy sớm nhất có thể nhưng không chặn lần kết xuất 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()
requestAnimationFrame
requestIdleCallback
scheduler.postTask()
"requestPostAnimationFrame"
Không giống như requestAnimationFrame
(sẽ cố gắng chạy trước khi lần hiển thị tiếp theo và thường vẫn khiến hoạt động tương tác diễn ra chậm), requestAnimationFrame
+ setTimeout
tạo thành một polyfill đơn giản cho requestPostAnimationFrame
, chạy lệnh gọi lại sau khi 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);
});
});
Để có tính công thái học, bạn thậm chí có thể gói nó trong một promise:
Xem toàn bộ mã: 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 liên tục)
Việc di chuyển các tác vụ 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 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 chặn không đồng bộ của trang (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 hoãn công việc ở bước cuối cùng):
Xem toàn bộ mã: 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 một cách nhanh chóng?
Dấu vết hiệu suất
Đối với mỗi lượt nhấp, sẽ có một tác vụ kéo dài một giây được xếp 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ể.
Khi những tác vụ dài đó trùng lặp với các lượt nhấp mới, điều này sẽ dẫn đến các lượt tương tác diễn ra chậm, mặc dù bản thân trình nghe sự kiện trả về gần như ngay lập tức. Chúng ta đã tạo ra tình huống 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 đến từ setInterval
mà đến từ hoạt động do các trình nghe sự kiện trước đó kích hoạt.
Chiến lược
Lý tưởng nhất là chúng ta muốn loại bỏ hoàn toàn các tác vụ dài!
- 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ụ mất nhiều thời gian.
- Huỷ bỏ công việc cũ khi có lượt tương tác mới.
16. Chiến lược 1: loại bỏ hiện tượng dội lại
Một chiến lược cổ điển. Bất cứ khi nào các lượt tương tác diễn ra liên tục và việc xử lý hoặc hiệu ứng mạng tốn nhiều tài nguyên, hãy cố ý trì hoãn việc bắt đầu để bạn có thể huỷ và 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, bằng một bộ hẹn giờ, có thể là 500 đến 1000 mili giây. - Lưu mã nhận dạng bộ hẹn giờ khi bạn thực hiện việc nà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 cách sử dụng
clearTimeout
.
Xem toàn bộ mã: 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, đợi cho đến khi không có lượt nhấp nào trong một giây đầy đủ trước khi chạy. Đối với những lượt tương tác diễn ra theo đợt (chẳng hạn như nhập dữ liệu vào một thành phần 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ẽ đến ngay sau khi khoảng thời gian loại bỏ trùng lặp kết thúc, sẽ rơi vào giữa tác vụ dài đó 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 một hoạt động tương tác diễn ra trong quá trình chúng ta thực hiện tác vụ, chúng ta muốn tạm dừng công việc đang bận để mọi hoạt động tương tác mới đều được xử lý ngay lập tức. Chúng ta có thể làm việc đó bằng cách nào?
Có một số API như isInputPending
, nhưng thường thì tốt hơn là 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 một việc đơn giản.
Xem toàn bộ mã: 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 từng tác vụ riêng lẻ và đầ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 những tác vụ đó, không có lượt tương tác nào có độ trễ đầu vào quá 100 mili giây! Trình duyệt ưu tiên các trình nghe sự kiện đến hơn công việc setTimeout
và các hoạt động tương tác vẫn phản hồi.
Chiến lược này đặc biệt hiệu quả khi lên lịch các điểm truy cập riêng biệt, chẳng hạn như nếu 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, chỉ cần tải tập lệnh và chạy mọi thứ tại thời điểm đánh giá tập lệnh, bạn có thể chạy mọi thứ trong một tác vụ lớn và dài.
Tuy nhiên, chiến lược này không hiệu quả đối với việc tách 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ó giá 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 sẽ được tạo 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 đến, nhưng giờ đây, tất cả những gì cần thiết là một await schedulerDotYield()
thay vì các setTimeout
riêng biệt, giúp bạn có thể sử dụng ngay cả ở giữa vòng lặp for
.
Hiện có giá AbortContoller()
Cách này có hiệu quả, nhưng mỗi lượt tương tác sẽ lên lịch thêm công việc, 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 loại bỏ trùng lặp, 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 mọi công việc cần thiết trong khi định kỳ tạo ra luồng chính để trình duyệt vẫn 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 sẽ được gắn cờ là đã huỷ bằng AbortController
và một vòng lặp blockInPiecesYieldyAborty
mới sẽ 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 cần thực hiện thêm thao tác nào.
18. Kết luận
Việc chia nhỏ tất cả các tác vụ dài giúp trang web phản hồi các lượt tương tác mới. Nhờ đó, bạn có thể 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 tiến hành. Đôi khi, điều đó có nghĩa là bạn phải lên lịch các điểm truy cập dưới dạng các nhiệm vụ riêng biệt. Đôi khi, điều đó có nghĩa là bạn cần thêm các điểm "lợi nhuận" khi thuận tiện.
Lưu ý
- 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ừ đầu vào đến nội dung hiển thị tiếp theo – cách người dùng nhận thấy khả năng phản hồi.
- Độ trễ khi nhập, thời lượng xử lý sự kiện và độ trễ khi trình bày đề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à các lượt tương tác bằng Công cụ cho nhà phát triển!
Chiến lược
- Không có mã chạy trong thời gian dài (các tác vụ dài) trên 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 có hiệu quả đối với trình duyệt.