Trình duyệt không vẽ trực tiếp HTML lên màn hình. Nó nhận byte, dựng các mô hình nội bộ, chạy JavaScript giữa những cơ hội kết xuất, rồi mới hiển thị pixel đã tổng hợp. Một trang chậm có thể do stylesheet chặn hiển thị, geometry thiếu ổn định, paint quá lớn, quá nhiều GPU layer hoặc JavaScript giữ main thread.
Hiểu pipeline giúp biến tối ưu performance từ phỏng đoán thành chẩn đoán. Câu hỏi hữu ích không chỉ là “Trang có chậm không?”, mà là “Công đoạn nào làm trễ khung hình hữu ích tiếp theo, và điều gì đã kích hoạt công đoạn đó?”
Phân tích HTML và CSS thành các mô hình làm việc
HTML parser giải mã response thành ký tự, tách token và dựng Document Object Model (DOM). Nó có thể tạo node, phát hiện subresource trước khi nhận hết document và sửa markup sai theo quy tắc parsing; vì thế DOM trong DevTools đôi khi khác source.
Preload scanner phát hiện sớm stylesheet, script, font và image, nhưng phát hiện không đồng nghĩa với thực thi. Classic script không có defer dừng parser trong lúc tải và chạy vì nó có thể gọi document.write() hoặc kiểm tra node đã parse.
CSS đi theo một nhánh song song. Trình duyệt tách token và phân tích stylesheet thành CSS Object Model (CSSOM), gồm selector, declaration, cascade layer, media query và quy tắc tính giá trị. DOM cho biết element nào tồn tại; CSSOM cung cấp các rule có thể áp dụng. Không cây nào tự nó quyết định pixel cuối cùng.
<head>
<link rel="stylesheet" href="/app.css" />
<script type="module" src="/app.js"></script>
</head>
<body>
<main class="article">
<h2>Kết xuất có các dependency</h2>
<img src="/diagram.webp" width="960" height="540" alt="Pipeline kết xuất" />
</main>
</body>
Module script mặc định được defer: nó tải song song và chạy sau khi parse HTML. Kích thước image tường minh giữ sẵn không gian trước khi byte ảnh đến. Hai chi tiết này xử lý hai nút thắt khác nhau: một cái tránh ngắt parser, cái còn lại tránh layout shift về sau.
Note
DOM không phải render tree. Các element như head, node nằm dưới display: none và comment không tạo box. Ngược lại, pseudo-element và generated content có thể tạo box dù không phải DOM child thông thường.
Từ tính style đến pixel đã composite
Khi có đủ DOM và CSSOM, trình duyệt tính style rồi tạo cấu trúc box. Engine hiện đại có thể tối ưu và chạy chồng lấp công việc; pipeline dưới đây mô tả dependency, không phải một lượt tuần tự bắt buộc.
Mỗi công đoạn chính tạo ra kết quả và loại chi phí riêng:
| Công đoạn | Câu hỏi chính | Tác nhân thường gặp | Kết quả |
|---|---|---|---|
| Style | Declaration nào được áp dụng? | Đổi class, rule, state hoặc media query | Computed style |
| Layout | Mỗi box ở đâu và lớn bao nhiêu? | Đổi geometry, content, font hoặc viewport | Vị trí và kích thước box |
| Paint | Từng vùng hiển thị trông thế nào? | Đổi màu, shadow, text, border hoặc image | Display list hay paint record |
| Raster | Bitmap tile nào biểu diễn lệnh vẽ? | Vùng paint mới lộ ra hoặc bị invalid | Texture tile |
| Composite | Layer được biến đổi và trộn ra sao? | Transform, opacity, scroll, cập nhật layer | Khung hình cuối |
Style calculation giải quyết inheritance, specificity, custom property và selector. Layout xác định geometry, gồm xuống dòng và intrinsic size. Thay đổi gần đầu document có thể ảnh hưởng nhiều node phía sau, dù invalidation tracking giúp tránh tính lại mọi thứ.
Paint ghi lệnh vẽ background, text, border, image và effect. Rasterization chuyển chúng thành bitmap tile. Compositor ghép layer, áp dụng transform, opacity và vị trí scroll. Compositor-only animation tránh được layout và paint nhưng vẫn tốn memory cùng chi phí blending.
Resource chặn kết xuất và lần hiển thị đầu tiên
HTML có thể stream, nhưng lần hiển thị đầu vẫn phụ thuộc resource quan trọng. Stylesheet mặc định chặn render để tránh vẽ nội dung chưa style; CSS cũng trì hoãn script cần đọc computed style. Bundle lớn, redirect và @import kéo dài critical rendering path.
Classic script đồng bộ chặn parser. defer tải classic script song song, giữ thứ tự document và chạy sau parsing nhưng trước DOMContentLoaded. async cũng tải song song nhưng chạy ngay khi sẵn sàng, không đảm bảo thứ tự. Module mặc định đã defer; có thể thêm async cho module thật sự độc lập.
| Cách khai báo resource | Hành vi parsing | Lúc chạy hoặc áp dụng | Trường hợp phù hợp |
|---|---|---|---|
<script src="app.js"> |
Dừng parser | Ngay sau khi tải | Dependency legacy hiếm gặp |
<script defer src="app.js"> |
Tiếp tục | Có thứ tự, sau parsing | Classic script cần DOM |
<script async src="analytics.js"> |
Tiếp tục | Ngay khi sẵn sàng | Analytics hoặc quảng cáo độc lập |
<script type="module"> |
Tiếp tục | Defer, hiểu dependency | Entry ứng dụng hiện đại |
<link rel="stylesheet"> |
Vẫn parse, nhưng chặn render | Khi CSS sẵn sàng | Style cần thiết của trang |
Font đến muộn có thể làm text dịch chuyển. Hãy chọn font-display, preload đúng font quan trọng, subset ký tự và dùng fallback có metric tương thích. Đặt width cùng height hoặc aspect-ratio cho image; tải sớm ảnh LCP nhưng lazy-load media dưới fold.
Warning
Preload mọi thứ không làm mọi thứ trở thành critical. Quá nhiều preload tranh bandwidth với CSS, font và ảnh LCP, thậm chí làm lần hiển thị đầu chậm hơn. Priority phải phản ánh dependency mà người dùng thật sự nhìn thấy.
Event loop và ngân sách khung hình
JavaScript, DOM event, style, layout và chuẩn bị paint phần lớn dùng chung main thread. Event loop chạy một task, rút cạn microtask queue, rồi mới có thể render. Vì thế chuỗi promise dài cũng có thể chiếm thread như một callback dài.
Ở tần số 60 Hz, màn hình trình bày một frame sau khoảng ms. Với 120 Hz, khoảng này chỉ còn xấp xỉ ms. Ứng dụng không sở hữu toàn bộ ngân sách: trình duyệt còn xử lý input, style, layout, paint, điều phối raster và composite. Khi lỡ deadline, frame cũ tiếp tục hiện và chuyển động bị giật.
requestAnimationFrame (rAF) chạy trước lần paint tiếp theo, nhưng callback 30 ms vẫn làm rớt frame. Hãy chia computation nặng, chuyển phần phù hợp sang Web Worker và yield giữa các chunk. Chỉ dùng requestIdleCallback cho việc tùy chọn; trang bận có thể không có idle time.
const pendingWidths = new Map<HTMLElement, number>();
let frameRequested = false;
export function scheduleWidth(element: HTMLElement, width: number): void {
pendingWidths.set(element, width);
if (frameRequested) return;
frameRequested = true;
requestAnimationFrame(() => {
frameRequested = false;
for (const [target, nextWidth] of pendingWidths) {
target.style.width = `${nextWidth}px`;
}
pendingWidths.clear();
});
}
Đoạn code gộp nhiều lần ghi thành một visual update. Với input handler, chỉ làm phần đồng bộ tối thiểu, tránh chuỗi microtask không giới hạn và hủy async work đã lỗi thời. Responsiveness đến từ việc thường xuyên trả quyền cho trình duyệt đi tới checkpoint input và rendering, không phải chỉ vì code mang nhãn async.
Reflow, paint invalidation, layer và GPU
“Reflow” là tên thường dùng cho layout chạy sau invalidation. Thay đổi text, dimension, font metric, grid track hoặc viewport có thể làm geometry mất hiệu lực. Đọc thuộc tính phụ thuộc layout như offsetWidth sau khi ghi style có thể buộc trình duyệt flush style và layout đang chờ theo cách đồng bộ để trả về số chính xác.
// Không tốt: mỗi lần ghi có thể khiến lần đọc kế tiếp ép layout.
for (const card of cards) {
card.style.width = `${container.clientWidth / 3}px`;
}
// Tốt hơn: đọc một lần rồi gom các lần ghi.
const cardWidth = container.clientWidth / 3;
requestAnimationFrame(() => {
for (const card of cards) card.style.width = `${cardWidth}px`;
});
Xen kẽ đọc và ghi là layout thrashing. Ưu tiên CSS Grid hoặc Flexbox; nếu phải đo bằng JavaScript, gom read trước write. contain: layout paint thu nhỏ vùng ảnh hưởng, còn content-visibility: auto có thể bỏ rendering của section ngoài viewport trong khi giữ intrinsic size ước lượng.
Layer là trade-off, không phải nút tăng tốc. Compositor có thể animate transform hoặc opacity của layer đã promote mà không repaint, nhưng mỗi layer tốn texture memory, raster, upload và blending. will-change chỉ là gợi ý tạm thời.
GPU rất mạnh với raster và composition, nhưng không sửa được DOM construction, selector matching, JavaScript hay layout đắt đỏ. Animate transform: translate(...) thường rẻ hơn animate left, vì left thay đổi geometry. Filter lớn, mask, transparency và layer khổng lồ bị scale vẫn có thể tốn kém dù không chạy layout.
.drawer {
transform: translateX(-100%);
transition: transform 180ms ease-out;
}
.drawer[data-open='true'] {
transform: translateX(0);
}
Tip
Chỉ promote sau khi profiling. Thêm will-change ngay trước interaction đã biết và gỡ nó sau đó để trình duyệt giải phóng tài nguyên layer không còn cần thiết.
Quan sát metric, profile và sửa đúng công đoạn
Polling layout bằng timer vừa lãng phí vừa ép measurement sai thời điểm. Browser observer cung cấp thay đổi tại checkpoint rõ ràng hơn:
| API | Dữ liệu báo về | Cách dùng thường gặp | Điều cần thận trọng |
|---|---|---|---|
ResizeObserver |
Kích thước content hoặc border của element | Component responsive, resize chart | Tránh feedback loop liên tục resize target đang observe |
IntersectionObserver |
Mức nhìn thấy so với root | Lazy work, view tracking, virtual list | Bất đồng bộ, không phải phép đo che khuất chính xác từng pixel |
PerformanceObserver |
Entry trên performance timeline | Web Vitals, long task, resource timing | Mức hỗ trợ entry khác nhau; cần feature detection |
MutationObserver |
Mutation của DOM tree và attribute | Ranh giới tích hợp, editor tooling | Observe subtree quá rộng có thể tạo batch rất lớn |
const supported = PerformanceObserver.supportedEntryTypes;
if (supported.includes('longtask')) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task', Math.round(entry.duration), 'ms');
}
});
observer.observe({ type: 'longtask', buffered: true });
}
Metric cần phản ánh điều người dùng trải nghiệm. Largest Contentful Paint (LCP) đo thời điểm phần tử nội dung đủ điều kiện lớn nhất trong viewport được vẽ; hãy tối ưu server response, resource discovery, priority và render delay của nó. Interaction to Next Paint (INP) tóm tắt độ phản hồi của interaction trong cả lượt truy cập; giảm long task, công việc trong event handler và rendering do interaction kích hoạt. Cumulative Layout Shift (CLS) đo bất ổn hình ảnh ngoài dự kiến; giữ sẵn chỗ cho media và quảng cáo, ổn định font, không chèn nội dung phía trên vùng người dùng đang đọc.
TTFB, FCP, long task, thời gian JavaScript, style/layout, paint area và resource waterfall giúp giải thích nguyên nhân. Lab tool dễ tái lập; real-user monitoring phản ánh device, network, cache và interaction thật. Cần cả hai.
Bắt đầu bằng flow lặp lại được và triệu chứng cụ thể: nội dung chậm, input trễ, scroll giật hay layout dịch chuyển. Ghi DevTools Performance trace với throttling phù hợp. Xem network waterfall để tìm blocking chain, rồi main-thread flame chart để tìm long task và lượt style, layout hay paint lặp lại.
Trong trace, hãy đặt câu hỏi về quan hệ nhân quả. JavaScript có mutate hàng nghìn node không? Geometry read nào ép layout? Một element lớn có repaint khi scroll không? Animation có tạo nhiều layer quá khổ không? So sánh trước và sau trong cùng điều kiện, đồng thời kiểm tra field metric thay vì chỉ ăn mừng synthetic score.
Biện pháp thực tế phải đi theo công đoạn sở hữu chi phí:
- Gửi HTML hữu ích sớm, nén response và bỏ redirect khỏi critical resource.
- Giữ critical CSS nhỏ; tách style theo route và loại rule không dùng.
- Defer JavaScript không quan trọng, giảm chi phí thực thi bundle và chia long task.
- Giữ trước geometry cho image, embed, quảng cáo; tối ưu và ưu tiên resource LCP.
- Để CSS lo layout; gom DOM read trước write khi thật sự cần JavaScript.
- Virtualize list thật sự lớn và dùng containment cho vùng độc lập.
- Animate
transformvàopacitykhi hình ảnh tương đương, nhưng không tạo layer tràn lan. - Đo LCP, INP, CLS trong production và gắn thông tin chẩn đoán như route, nhóm device, release.
Điểm cần nhớ
Rendering là đồ thị dependency đi từ byte tới mô hình, geometry, lệnh vẽ, raster tile và pixel composite. Thay đổi DOM hoặc CSS làm mất hiệu lực những phần khác nhau của đồ thị đó. Scheduling trên main thread quyết định trình duyệt có hoàn tất công việc trước frame deadline hay không. Observer và metric hướng người dùng cho thấy triệu chứng mà không cần polling liên tục; performance trace xác định công đoạn chịu trách nhiệm. Hãy tối ưu bottleneck đã đo được, giữ visual stability và kiểm tra lại trên hardware cùng network gần với người dùng thật.