Đang tải...

Tối ưu Core Web Vitals với INP & LoAF — Bộ Đôi Định Nghĩa UX Performance Hiện Đại

12/05/2026
6 phút đọc
Tối ưu Core Web Vitals với INP & LoAF — Bộ Đôi Định Nghĩa UX Performance Hiện Đại
Nhiều anh em làm FE chắc từng gặp cảnh này: Lighthouse chấm điểm Performance xanh nhưng khi đưa cho khách hàng dùng thì họ vẫn than "Em ơi sao bấm vào cái nút này n?...

Nhiều anh em làm FE chắc từng gặp cảnh này: Lighthouse chấm điểm Performance xanh nhưng khi đưa cho khách hàng dùng thì họ vẫn than "Em ơi sao bấm vào cái nút này nó cứ khựng?", "Nó cứ kiểu bị delay á em?". Đó là lúc chúng ta cần nói về INPLoAF.

1. INP (Interaction to Next Paint) là gì?

Trước đây ta hay nhìn vào FID (độ trễ lần tương tác đầu tiên). Nhưng FID quá hời hợt, nó chỉ tính cái click đầu lúc trang web mới load xong.

INP thì khác. Nó theo dõi tất cả các lần người dùng click, tap hay nhấn phím trên trang từ đầu đến cuối. Sau đó, nó chọn ra lần phản hồi chậm nhất để làm đại diện.

Dễ hiểu là: Nếu trang web của bạn có 10 cái nút, 9 cái bấm phát ăn ngay, nhưng cái nút thứ 10 bấm vào phải đợi nửa giây mới thấy phản hồi, thì INP của bạn sẽ bị tính theo cái nút thứ 10 đó.

Ví dụ: Bạn làm một cái menu mobile. Người dùng bấm vào icon "Hamburger". Nếu code xử lý logic quá nặng, trình duyệt mất 300ms mới mở được menu -> INP sẽ cao (xấu). Người dùng sẽ cảm thấy trang web bị "đơ".

image.png

2. LoAF (Long Animation Frames) - Công cụ "soi" lỗi tận gốc

Nếu INP nói cho bạn biết "Trang web đang lag đấy", thì LoAF sẽ chỉ tận tay "Thằng nào làm lag". Trước đây chúng ta có Long Tasks, nhưng nó chỉ báo chung chung là có một tác vụ chạy quá 50ms. Anh em dev lại phải đi mò xem đó là đoạn code nào. LoAF nâng cấp hơn. Nó cho bạn biết:

  • Đoạn script nào đang chạy (file nào, dòng nào).
  • Mất bao nhiêu thời gian để tính toán, bao nhiêu thời gian để vẽ (render) lên màn hình.
  • Thậm chí là do script của mình hay script của bên thứ ba (quảng cáo, tracker) gây ra.

3. Cách dùng LoAF để "fix" INP

Giả sử mình có một chức năng nặng như sau:

export function blockingHeavyCompute(durationMs: number = 300): void {
 const start = performance.now();

 // === VÙNG BLOCKING — đây là nơi LoAF sẽ trỏ đến ===
 let result = 0;
 while (performance.now() - start < durationMs) {
 for (let i = 0; i < 1_000_000; i++) {
 result += Math.sqrt(i) * Math.random();
 }
 }
 if (result < 0) console.log("never");
}

Trong App.tsx mình call khi click button:

function handleRunHeavyTask() {
 // → browser schedule trực tiếp runHeavyTask → LoAF thấy code của mình
 requestAnimationFrame(function runHeavyTask() {
 blockingHeavyCompute(400);
 });
 }

Thay vì đoán mò, mình dùng đoạn code này để soi:

const observer = new PerformanceObserver((list) => {
 for (const entry of list.getEntries()) {
 console.group(`🚀 Khung hình chậm phát hiện! (${entry.duration.toFixed(2)}ms)`);
 
 entry.scripts.forEach((s, i) => {
 // Dùng groupCollapsed để mặc định nó đóng lại, khi nào cần mới click vào xem
 console.groupCollapsed(`Script #${i + 1}: ${s.invokerType} - ${s.duration.toFixed(2)}ms`);
 
 console.log("File:", s.sourceLocation || "N/A"); // sourceLocation bao gồm cả URL, dòng và cột
 console.log("Function:", s.functionName || "(anonymous)");
 console.log("Invoker Type:", s.invokerType); // Ví dụ: 'user-callback', 'event-listener', v.v.
 console.log("Char Position:", s.charPosition);
 
 console.groupEnd();
 });

 console.groupEnd();
 }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

Mình có show ra UI cho dễ nhìn. Ban đầu mới vào: image.png

Khi click button thì ta sẽ có kết quả như sau: image.png

Fix khá đơn giản, chỉ cần thêm settimeout bọc ngoài

setTimeout(() => {
 blockingHeavyCompute(400);
 }, 0);

image.png Nhưng tốt nhất là nên tối ưu lại logic bên trong hàm, tránh xử lý tác vụ nặng

Cách xử lý hay dùng:

  • Chia nhỏ Task: Đừng bắt trình duyệt xử lý 1000 kết quả tìm kiếm cùng lúc. Hãy chia nhỏ ra bằng setTimeout hoặc requestIdleCallback.
  • Ưu tiên hiển thị trước: Khi người dùng bấm nút, hãy hiển thị cái loading ngay lập tức (phản hồi thị giác), rồi mới xử lý logic nặng phía sau.
  • Dùng Web Worker: Nếu có tính toán gì quá khủng khiếp, hãy đẩy nó sang một luồng khác (Worker), đừng để nó chiếm dụng Main Thread của trình duyệt.

4. Tổng kết

  • INP là cái thước đo trải nghiệm thực tế: Đừng để người dùng phải chờ sau khi click.
  • LoAF là cái kính hiển vi: Dùng nó để tìm ra chính xác dòng code nào đang chiếm dụng tài nguyên.

Tối ưu Web giờ không chỉ là lo load nhanh (LCP) nữa, mà phải là tương tác mượt. Hy vọng bài chia sẻ ngắn này giúp anh em có cái nhìn thực tế hơn để tối ưu dự án của mình.

📚 Nguồn: Viblo

Bình luận

0 bình luận

Email không hiển thị công khai.

Chưa có bình luận nào. Hãy là người đầu tiên bình luận.

Chia sẻ bài viết

Cần tư vấn?

Liên hệ với chúng tôi để được hỗ trợ

Liên hệ ngay

Bài viết liên quan

Giải Mã Từ Khóa super Trong JavaScript: "Chiếc Cầu Nối" Đến Lớp Cha Và Cú Vấp Bắt Buộc Trước Khi Chạm Vào this
28/06/2026

Giải Mã Từ Khóa super Trong JavaScript: "Chiếc Cầu Nối" Đến Lớp Cha Và Cú Vấp Bắt Buộc Trước Khi Chạm Vào this

Chào anh em Viblo! 👋 Khi thế giới JavaScript chuyển mình lên chuẩn ES6, cú pháp Class (Lớp) ra đời đã thay đổi hoàn toàn cách chúng ta viết code theo tư duy Lập trì...

Đọc thêm
Vén Màn Bí Mật var require: NodeRequire;: Hàm require Của Node.js Khủng Khiếp Hơn Bạn Nghĩ!
28/06/2026

Vén Màn Bí Mật var require: NodeRequire;: Hàm require Của Node.js Khủng Khiếp Hơn Bạn Nghĩ!

Chào anh em Viblo! 👋 Nếu anh em đã từng làm việc với Node.js ở kỷ nguyên CommonJS, hoặc đang cấu hình các file script, file webpack, vite trong các dự án hiện đại, ...

Đọc thêm
Giải Mã var __dirname: string;: Tấm Bản Đồ Định Vị File Và Cú Vấp Ngã Xuyên Quốc Gia Giữa Windows và Linux
28/06/2026

Giải Mã var __dirname: string;: Tấm Bản Đồ Định Vị File Và Cú Vấp Ngã Xuyên Quốc Gia Giữa Windows và Linux

Chào anh em Viblo! 👋 Tiếp nối chuỗi bài viết mổ xẻ các tham số "quyền lực" được tiêm (inject) ngầm vào bên trong Module Wrapper Function của Node.js (sau khi chúng ...

Đọc thêm

Bắt đầu dự án của bạn

Hãy để Flash Dev đồng hành cùng bạn

Liên hệ ngay