jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

How Browsers Work · Part 5 — Layout, Paint & Compositing

The pixel pipeline: how styled DOM becomes layout geometry, paint records, raster tiles and compositor frames — why invalidation and animation cost depend on context.

Phần 1–4 đã dựng đầu vào: DOM, stylesheet, computed style và layout tree. Phần này là lúc chúng trở thành pixel trên màn hình. Không phải mọi update đi qua mọi stage: engine giữ kết quả cũ và chỉ chạy lại phần đã bị invalidate.

Nếu bạn nhớ một điều từ Phần 1: main thread làm style/layout và ghi paint record, còn compositor/raster thread có thể chuẩn bị frame từ nội dung đã ghi. Sự tách đó cho phép một số animation đủ điều kiện tiếp tục khi main thread bận — nhưng không bảo đảm frame sẽ luôn rẻ hay mượt.

DOM + stylesheets (from Parts 3–4)


   ┌─────────┐
   │  Style  │  recalc affected computed values
   └────┬────┘

   ┌─────────┐
   │ Layout  │  compute geometry: positions, sizes (a.k.a. reflow)
   └────┬────┘

   ┌─────────┐
   │Pre-paint│  property trees, clipping, invalidation
   └────┬────┘

   ┌─────────┐
   │  Paint  │  record draw ops / paint chunks
   └────┬────┘

   ┌─────────┐
   │ Raster  │  paint records → tiles/pixels
   └────┬────┘

   ┌─────────────┐
   │ Compositing │  assemble compositor frame → display
   └─────────────┘

Đây là bản rút gọn của RenderingNG pipeline; Phần 13 sẽ tách sâu raster, tile, Viz và GPU process.


1. Tính lại style

Trước khi tính hình học, browser resolve computed style cho node bị ảnh hưởng: áp cascade, inheritance/defaulting và cập nhật layout object nếu display hay cấu trúc box thay đổi.

Style recalc được kích hoạt khi:

  • Rule/style input thay đổi (toggle class, đổi attribute/style, nạp stylesheet).
  • Cấu trúc DOM thay đổi theo cách ảnh hưởng selector (node mới, đổi attribute khớp selector).
  • Thuộc tính kế thừa thay đổi trên tổ tiên.

Style recalc một mình không luôn kéo theo layout — đổi color hay background-color tính lại style nhưng có thể bỏ qua layout hoàn toàn. Trình duyệt theo dõi cờ invalidation trên từng node để bỏ qua giai đoạn mà thay đổi không chạm tới.


2. Layout (reflow): tính hình học

Layout — còn gọi reflow trong nhiều tài liệu — biến box đã style thành fragment/hình học cụ thể: width, height, vị trí, ngắt dòng, scroll extent. Với block có width: 50%, layout resolve phần trăm theo containing block; với chữ, nó shape text và tạo line box.

Layout hiện đại là incremental, nhưng dependency hình học có thể làm thay đổi lan sang ancestor, sibling hay descendant. Một lượt layout nhỏ không nhất thiết đắt; vấn đề là phạm vi lớn hoặc lặp đồng bộ nhiều lần trong cùng task. Containment có thể thu hẹp dependency trong use case phù hợp.

Các thuộc tính thường kích hoạt layout gồm:

  • Box model: width, height, padding, margin, border, box-sizing.
  • Định vị: top, left, right, bottom, position.
  • Luồng: display, float, clear, thuộc tính con flex/grid ảnh hưởng kích thước.
  • Typography: font-size, font-family, line-height, text-align (ảnh hưởng line box).

Layout đọc layout tree và ghi fragment/hình học được cache cho pre-paint/paint. Nếu bạn đọc geometry từ JS (offsetWidth, getBoundingClientRect) sau khi ghi làm layout invalid, browser có thể phải flush style/layout đồng bộ để trả kết quả mới nhất.


3. Paint: ghi lệnh vẽ

Khi hình học đã rõ, paint (đôi khi gọi repaint) ghi cách mỗi vùng trông ra sao — chưa phải pixel, mà là chuỗi thao tác vẽ: tô hình chữ nhật, vẽ chữ, đổ bóng, clip theo border-radius.

Các thay đổi thường không đổi geometry nhưng cần paint record/raster mới gồm:

  • color, background, background-image, background-position.
  • box-shadow, outline, border-color (khi border width không đổi).
  • visibility (nhiều trường hợp chỉ paint; display: none bỏ qua cả layout lẫn paint).

Không có luật “paint luôn rẻ hơn layout”: shadow/blur lớn, text hoặc vùng invalidation rộng có thể tốn đáng kể. Đầu ra là display item/paint chunk cùng property tree — hãy coi như băng lệnh mà raster sau này biến thành tile/pixel.


4. Compositing: layer và GPU

Compositing lắp các quad/surface từ nội dung đã raster thành compositor frame, áp transform, clip, opacity/effect cần thiết rồi gửi qua Viz/display compositor để hiển thị. GPU thường tham gia, nhưng raster/composite có cả đường phần mềm tùy thiết bị và fallback.

Animation transform/opacity có thể cập nhật compositor-only khi nội dung nằm trong composited layer và effect đủ điều kiện. Lần chuẩn bị đầu vẫn có style, paint/raster và layerization; scale lớn, filter, thay nội dung hay thiếu texture có thể gây raster/GPU work. Vì vậy “dùng transform” là điều kiện tốt, không phải giấy bảo hành 60 fps.

Khi trình duyệt promote layer

Không phải mọi element đều có compositor layer riêng. Chromium layerize paint chunk/surface theo compositing reason và cost model có thể đổi giữa phiên bản, ví dụ:

  • will-change: transform hoặc will-change: opacity (gợi ý; dùng tiết kiệm).
  • Animation transform/opacity đang chạy và đủ điều kiện.
  • <video>, canvas tăng tốc, iframe/surface nhúng và một số UI overlay.
  • Phần tử position: fixed hoặc position: sticky trong một số trường hợp.
  • Effect/clip/scrolling cần surface độc lập theo quyết định engine.

Bùng nổ layer

Layer/surface và tile backing tốn bộ nhớ, upload/raster và chi phí aggregate. Promote hàng loạt phần tử “phòng hờ” có thể làm hiệu năng tổng thể tệ hơn. Dùng will-change gần thời điểm animation, bỏ khi xong, và xác nhận bằng DevTools thay vì dùng như hack cố định.


5. Cái gì kích hoạt cái gì: heuristic, không phải luật

Không có bảng thuộc tính CSS nào chính xác cho mọi engine, element và frame. Bảng dưới là đường thường gặp khi thay đổi giá trị; engine vẫn có thể mở rộng/thu hẹp invalidation hoặc cần layer preparation:

Thay đổiStyleLayoutPaintComposite
color, background-color
width, height, padding, margin
transformcó thể lúc chuẩn bị/raster lại
opacitycó thể lúc chuẩn bị layer
box-shadow
display: none → block

Quy tắc ra quyết định hữu ích hơn:

  1. Thay đổi geometry có thể kéo style → layout → pre-paint → paint/raster.
  2. Thay đổi visual không đổi geometry thường bỏ qua layout nhưng vẫn có paint/raster.
  3. Update compositor-only tránh main-thread layout/paint, nhưng vẫn trả chi phí composite/GPU và chỉ xảy ra khi đủ điều kiện.

Dùng Performance trace, Paint flashing và Layers/Compositing Reasons để kiểm chứng đường thực tế. Containment (contain) và content-visibility có thể thu hẹp công việc, nhưng cũng đổi semantics layout/paint nên cần test.


6. Layout thrashing: xen kẽ đọc/ghi

Layout thrashing xảy ra khi JavaScript xen kẽ ghi làm layout invalid với đọc cần geometry mới nhất. Khi relevant style/layout đang pending, lần đọc có thể buộc browser flush đồng bộ; lặp pattern này tạo nhiều layout nhỏ thay vì một batch.

// ❌ BAD — repeated read/write cycles can force repeated layouts
for (const el of elements) {
  el.style.width = `${container.offsetWidth / elements.length}px`; // read then write, every loop
}

// ✅ GOOD — 1 read, then N writes
const width = container.offsetWidth; // batch all reads first
for (const el of elements) {
  el.style.width = `${width / elements.length}px`; // then batch writes
}

Mẫu luôn giống nhau: gom đọc, rồi gom ghi. Bạn có thể mã hóa nó bằng queue read/write riêng, nhưng thường chỉ cần tổ chức lại vòng lặp nóng như ví dụ.

Các API geometry như offsetWidth, offsetHeight, clientWidthgetBoundingClientRect() có thể force style/layout nếu kết quả đang stale; getComputedStyle() chỉ force layout trong một số điều kiện/property. Bản thân một read không xấu — chuỗi write → read → write → read trong đường nóng mới là thứ cần tìm trong trace.


7. requestAnimationFrame và nhịp khung hình

requestAnimationFrame lên lịch callback cho rendering opportunity tiếp theo và dùng timestamp cùng nhịp refresh. Trên màn 60 Hz, khoảng giữa hai refresh là ~16,7 ms; màn 120 Hz chỉ còn ~8,3 ms, và browser còn phải chia ngân sách cho input, style/layout, raster và composite.

Một khung điển hình trên luồng chính trông như:

JS (including rAF callbacks)
  → Style
  → Layout
  → Paint (record)
  → (Compositor thread: raster + composite — often parallel)

Dùng requestAnimationFrame cho cập nhật visual chạm DOM — không phải setTimeout(fn, 16). setTimeout không khớp vsync và có thể bắn giữa các khung, gây việc thừa hoặc bỏ lỡ paint. Với hiệu ứng gắn scroll, ưu tiên CSS (position: sticky, scroll-driven animations) hoặc đọc scroll position trong rAF để bám lịch render.

Panel Performance của Chrome DevTools hiện Layout, Recalculate Style, Paint và raster/compositor work; bật Rendering → Paint flashing để xem vùng repaint. Panel Layers và thông tin compositing reason giúp kiểm tra layer, nhưng tên/placement UI có thể đổi giữa phiên bản DevTools.


Tóm tắt

  • Pipeline rút gọn là Style → Layout → Pre-paint → Paint → Raster → Compositing; stage không bị invalidate có thể được bỏ qua.
  • Layout tính geometry theo dependency; nó không luôn toàn cục hay luôn đắt nhất, nhưng repeated forced layout là pattern nguy hiểm.
  • Paint ghi lệnh, raster biến chúng thành tile/pixel, compositing lắp frame; GPU không biến mọi công việc thành “miễn phí”.
  • transformopacity có thể compositor-only khi đủ điều kiện; layer quá nhiều lại tốn memory/raster/composite.
  • Tránh layout thrashing bằng cách gom đọc DOM trước khi ghi; dùng requestAnimationFrame cho cập nhật visual bằng JS.

Phần tiếp theo: JavaScript engine (V8) — parsing, bytecode, JIT tối ưu, hidden class, và vì sao JS theo idiom lại nhanh.