Khi một trang trông như đã hiện đầy đủ nhưng bấm vào nút lại đứng hình vài trăm mili-giây, thủ phạm gần như luôn là luồng chính (main thread — luồng xử lý chính của trình duyệt) đang kẹt vì JavaScript. TBT chính là con số đo độ kẹt đó trong môi trường thử nghiệm, và nó là chỉ số nặng ký nhất trong điểm Lighthouse.
TBT đo cái gì
Lighthouse mô phỏng quá trình tải trang rồi cộng dồn phần “chặn” của mọi long task (tác vụ dài — tác vụ chạy quá 50ms trên luồng chính) nằm giữa hai mốc FCP (First Contentful Paint — thời điểm nội dung đầu tiên hiện ra) và TTI (Time to Interactive — thời điểm trang sẵn sàng phản hồi).
Quy tắc tính rất gọn: với mỗi tác vụ dài hơn 50ms, phần vượt quá 50ms được tính là thời gian chặn. Một tác vụ chạy 250ms đóng góp 200ms vào TBT (250 trừ 50). Một tác vụ 90ms chỉ đóng góp 40ms. Tác vụ dưới 50ms hoàn toàn không tính.
| Độ dài tác vụ | Thời gian chặn cộng vào TBT |
|---|---|
| 45ms | 0ms (dưới ngưỡng) |
| 90ms | 40ms |
| 250ms | 200ms |
| 3 tác vụ × 80ms | 90ms (30 × 3) |
Ngưỡng “Good” (tốt) theo Google là TBT dưới 200ms khi đo trên cấu hình di động trung bình. Trong điểm hiệu năng Lighthouse, TBT là chỉ số có trọng số cao nhất (30%) — nên cải thiện TBT thường là đòn bẩy kéo điểm rõ nhất.
TBT là chỉ số lab, INP mới là chỉ số field
Đây là điểm hay bị nhầm. TBT là chỉ số lab (phòng thí nghiệm — đo trong môi trường mô phỏng, không có người dùng thật). Lighthouse không đo được INP (Interaction to Next Paint — thời gian từ lúc người dùng tương tác đến khung hình kế tiếp), nên dùng TBT làm proxy (đại diện — chỉ số thay thế để dự báo).
Khác biệt cốt lõi: TBT chỉ đo độ kẹt luồng chính trong lúc tải trang, còn INP đo độ phản hồi với mọi tương tác suốt cả phiên của người dùng thật. Vì thế TBT có thể báo động giả: nếu không ai bấm gì vào đúng lúc luồng chính kẹt, trải nghiệm thực tế chẳng hề sao. Google khuyến nghị: dùng TBT để gỡ lỗi trong lab, nhưng quyết định cuối dựa trên INP đo từ người dùng thật (chi tiết về cặp lab/field xem bài INP thay thế FID trong Core Web Vitals).
INP đã chính thức thay FID làm Core Web Vital từ 12/3/2024. Ngưỡng “Good” hiện tại của ba chỉ số (đo ở phân vị 75 — p75 của người dùng thật): LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1. Bạn sẽ thấy ngưỡng TBT 200ms và ngưỡng INP 200ms trùng con số nhưng là hai phép đo khác hẳn nhau.

Vì sao TBT cao
Gần như mọi nguyên nhân đều quy về JavaScript làm việc quá nhiều, quá sớm, trên luồng chính:
- Bundle JS lớn phải parse và execute (phân tích và thực thi) ngay khi tải — riêng việc đọc và biên dịch JS đã chiếm luồng chính.
- Script bên thứ ba (third-party — quảng cáo, chat widget, analytics, A/B testing) chạy ngay khi vào trang.
- Một hàm nặng chạy liền một mạch tạo ra long task vài trăm mili-giây.
- Hydration (gắn JS vào HTML đã render) của các framework SPA dồn việc vào lúc tải đầu.
Cách kéo TBT xuống
1. Bỏ bớt JavaScript thừa
Đòn bẩy gốc rễ: ít JS phải chạy thì luồng chính ít kẹt. Tách code theo route, loại bỏ thư viện không dùng, và soi tab Coverage trong DevTools để thấy phần JS tải về nhưng không chạy.
2. Defer / async đúng script
Script chặn parser sẽ giữ luồng chính. Gắn defer hoặc async để trình duyệt không phải dừng dựng trang chờ tải và chạy script. Hai thuộc tính này hành xử khác nhau ở thời điểm thực thi — chọn sai dễ vỡ thứ tự phụ thuộc; phân biệt rõ ở bài defer và async khác nhau thế nào.
<!-- Tải song song, chạy sau khi parse xong HTML, giữ đúng thứ tự -->
<script src="/js/app.js" defer></script>
<!-- Tải song song, chạy ngay khi tải xong, không đảm bảo thứ tự -->
<script src="/js/analytics.js" async></script>3. Chia nhỏ long task
Một hàm chạy 250ms tạo 200ms blocking; cắt nó thành nhiều mẩu, mỗi mẩu dưới 50ms, thì gần như xoá sạch phần TBT. Cách hiện đại là dùng scheduler.yield() để nhường luồng chính giữa chừng, hoặc tách bằng setTimeout nếu cần hỗ trợ rộng:
async function xuLyDanhSach(items) {
for (const item of items) {
xuLyMot(item);
// Nhường luồng chính cho tương tác / render
if (navigator.scheduling?.isInputPending?.()) {
await scheduler.yield();
}
}
}4. Đẩy việc nặng sang Web Worker
Tính toán nặng không động đến DOM (xử lý dữ liệu lớn, parse JSON khổng lồ, mã hoá) nên đưa vào Web Worker — chạy ở luồng riêng, không chiếm luồng chính, nên không cộng vào TBT.
5. Hoãn script bên thứ ba
Chat widget, pixel quảng cáo, công cụ khảo sát hiếm khi cần chạy ngay giây đầu. Tải chúng sau khi trang đã tương tác được, hoặc khi người dùng cuộn/di chuột — đây thường là phần TBT dễ cắt nhất mà ít rủi ro.
Đo và đối chiếu
Xem TBT trong báo cáo Lighthouse (DevTools hoặc PageSpeed Insights). Lưu ý chế độ lab của PageSpeed thường khắt khe hơn máy bạn vì giả lập di động chậm. Khi tối ưu xong, đừng chỉ nhìn TBT — đối chiếu với INP thực tế từ trường dữ liệu CrUX (Chrome User Experience Report) để chắc chắn cải thiện chạm tới người dùng thật, không phải chỉ làm đẹp con số lab.
Web22 vận hành web22.dev trên LiteSpeed + Cloudflare và đo lặp lại bằng Lighthouse cho từng thay đổi JS; kinh nghiệm thực chiến cho thấy cắt long task của script bên thứ ba thường cho hiệu quả/công sức cao hơn việc minify vài kilobyte. Nếu cần soi cả luồng chính lẫn INP cho một site đang chậm, tham khảo audit hiệu năng web hoặc gói tối ưu hiệu năng web.

Câu hỏi thường gặp
TBT 0ms có nghĩa là trang chắc chắn phản hồi nhanh?
Không hẳn. TBT 0ms nghĩa là luồng chính không kẹt trong lúc tải, nhưng INP đo cả những tương tác về sau. Vẫn nên kiểm tra INP từ field để chắc.
TBT và TTI khác nhau ra sao?
TTI là một mốc thời điểm (lúc trang sẵn sàng tương tác). TBT là tổng lượng thời gian chặn tích luỹ giữa FCP và TTI — đo “mức độ kẹt”, không phải “thời điểm”.
Vì sao không đo TBT từ người dùng thật?
Vì tương tác của người dùng làm TBT dao động rất lớn, khó tin cậy. Ở field, Google khuyên dùng INP hoặc Long Animation Frames API thay cho TBT.
