How Browsers Work · Part 4 — CSS & the CSSOM
How the browser parses CSS into the CSSOM, resolves origins, layers, specificity and scope, computes values, and builds the layout tree — including when stylesheets block rendering.
Phần 3 dựng DOM từ byte HTML. Nhưng DOM một mình chỉ nói trình duyệt có gì, không phải trông thế nào. CSS cung cấp quy tắc hiển thị, và browser parse chúng thành các stylesheet/rule object thuộc CSS Object Model (CSSOM). CSSOM là API và mô hình stylesheet có cấu trúc; nó không phải một cây song song 1:1 với DOM.
Từ DOM và tập rule đang áp dụng, browser tính style cho phần tử, tạo layout box cần thiết, rồi tiếp tục layout và paint (Phần 5). Phần này đi từ byte stylesheet đến ngưỡng layout tree; “render tree” chỉ là tên mental model, không phải Web API chuẩn hay một cấu trúc giống hệt trong mọi engine.
1. Parse CSS thành CSSOM
CSS đến từ nhiều nguồn — <link rel="stylesheet">, khối <style>, @import, thuộc tính style, user stylesheet và mặc định user-agent. Mỗi stylesheet được parse thành danh sách rule có thể lồng qua at-rule; DOM API như document.styleSheets và CSSStyleSheet.cssRules lộ một phần mô hình đó.
styles.css bytes
│
▼
┌──────────────┐
│ CSS parser │ → tokenize CSS (selectors, properties, values)
└──────┬───────┘
▼
┌──────────────┐
│ stylesheet │ → ordered CSS rules / nested grouping rules
└──────────────┘
Example stylesheet model (conceptual):
StyleSheet
├─ Rule: h1 { font-size: 2rem; color: white; }
├─ Rule: .card { padding: 1rem; border: 1px solid #333; }
└─ Rule: .card > p { margin: 0; }
Khác HTML, CSS có cơ chế phục hồi lỗi theo rule/declaration: property không biết hoặc value sai cú pháp thường bị bỏ mà phần còn lại của sheet vẫn dùng được. Một số lỗi chỉ thành invalid at computed-value time sau khi custom property được thay thế, nên DevTools mới là nơi xác nhận declaration thực sự áp dụng.
Các rule liên quan từ mọi origin cùng tham gia cascade. Thứ tự vẫn quan trọng, nhưng chỉ sau origin, importance, cascade layer, specificity và scoping proximity — ta sẽ thấy ở Mục 3.
2. Vì sao CSS chặn render
Trình duyệt trì hoãn lần paint liên quan khi còn stylesheet render-blocking chưa sẵn sàng. Lý do là tránh paint với style tạm rồi đổi mạnh khi CSS đến, gây FOUC và layout shift.
Without render-blocking CSS:
Paint #1: plain Times New Roman, no layout
Paint #2: styled — jarring flash and layout shift
With render-blocking CSS:
(wait for CSSOM…)
Paint #1: styled correctly from the start
Cái gì được coi là chặn render:
<link rel="stylesheet">do parser chèn và cómediađang khớp thường là render-blocking.<style>do parser chèn được parse đồng bộ và được coi là potentially render-blocking trong thuật toán HTML.@importhợp lệ phải được resolve trước khi sheet phụ thuộc hoàn tất; chuỗi import cũng làm discovery muộn hơn<link>trực tiếp.
Cái không chặn render:
mediaquery không khớp —<link rel="stylesheet" media="print">trên màn hình thường vẫn có thể tải nhưng không chặn lần paint đầu.rel="preload"cho CSS — fetch sớm nhưng không áp dụng cho đến khi promote thành stylesheet.
Đặt CSS cần cho viewport đầu trong
<head>và tách CSS theo route khi đo đạc cho thấy có lợi. Pattern đổimediatrongonloadcó semantics/error handling riêng; đừng dùng như mẹo mặc định chỉ để “né” render-blocking. Phần 12 đưa các quyết định này vào trace thực tế.
3. Cascade: quy tắc xung đột được giải quyết thế nào
Hầu hết phần tử khớp nhiều quy tắc — <h1 class="title"> có thể dính selector phần tử, class, property kế thừa từ body, và mặc định user-agent. Cascade chọn một giá trị thắng cho mỗi property.
Cascade không chỉ là “specificity rồi source order”. Các bước chính là:
1. Relevance media/support/query có áp dụng không?
2. Origin + importance UA normal < user normal < author normal
< animation < author !important
< user !important < UA !important < transition
3. Cascade layer thứ tự @layer; thứ tự đảo với !important
4. Specificity chỉ so trong origin/layer đang thắng
5. Scoping proximity @scope gần hơn thắng nếu specificity hòa
6. Order of appearance declaration sau thắng khi mọi thứ trên hòa
Thứ tự đầy đủ và các ngoại lệ animation/transition nằm trong CSS cascade guide. Hai điểm thường bị bỏ sót: user !important thắng author !important để bảo toàn accessibility, và layer precedence thắng specificity — selector cực mạnh trong layer thấp vẫn thua selector nhẹ ở layer cao hơn.
Specificity là hệ thống chấm điểm bạn đã biết, tính (inline, ID, class/attr/pseudo-class, type/pseudo-element):
| Selector | Specificity | Ghi chú |
|---|---|---|
* | (0,0,0,0) | Phổ quát |
p | (0,0,0,1) | Selector kiểu |
.card | (0,0,1,0) | Class |
#hero | (0,1,0,0) | ID |
style="…" | (1,0,0,0) | Style inline |
:where(#hero .card) | (0,0,0,0) | :where() luôn có trọng số 0 |
@layer reset, components, overrides;
@layer components {
#hero p { color: red; } /* specificity cao nhưng layer thấp */
}
@layer overrides {
.intro { color: blue; } /* layer cao hơn → thắng */
}
!important không phải specificity; nó đổi bucket importance trước khi specificity được so. :is(), :not() và :has() lấy specificity từ selector argument mạnh nhất, còn :where() luôn bằng 0.
Kế thừa tách khỏi cascade — vài property (color, font-family) chảy từ cha xuống con trừ khi set rõ; vài cái khác (margin, border) thì không.
4. Tính style: khớp selector với phần tử
Với phần tử cần style, engine phải xác định giá trị computed của property liên quan. Thuật toán (đơn giản hóa):
- Thu thập mọi quy tắc có selector khớp phần tử.
- Sắp theo cascade (origin/importance → layer → specificity → scope → thứ tự nguồn).
- Áp dụng declaration thắng; lấp chỗ trống qua kế thừa và giá trị ban đầu.
- Lưu kết quả trong cấu trúc computed style gắn với phần tử.
Blink và nhiều engine khớp selector từ compound selector ngoài cùng bên phải rồi kiểm tra ancestor/sibling cần thiết. Với .card > p, <p> là điểm bắt đầu rồi engine kiểm tra cha .card. Đây là chi tiết implementation hữu ích để hình dung, không phải complexity guarantee của CSS.
Selector: .sidebar .nav li a
Match: find all <a> → check parent <li> → check ancestor .nav → check ancestor .sidebar
Modern engines index rules and track invalidation sets; they don't blindly
try every rule against every element.
Đổi class trên ancestor rộng như <body> có thể invalidate nhiều hậu duệ nếu nhiều selector phụ thuộc nó, nhưng không có selector nào mặc định “chậm” trong mọi DOM. Khi Recalculate Style dài, bật CSS selector stats trong Performance để xem elapsed time, match attempts và slow-path non-match thay vì tối ưu theo folklore.
5. Giá trị computed, used, và resolved
Property CSS qua vài giai đoạn giá trị — biết chúng giúp debug “vì sao width không đúng tôi set?”:
| Stage | Nghĩa | Ví dụ |
|---|---|---|
| Specified | Bạn viết gì | width: 50% |
| Computed | Sau cascade, defaulting và phép tính được định nghĩa cho property | width: 50% (percentage vẫn còn) |
| Used | Sau layout resolve containing block và constraint | width: 300px vì max-width chặn |
| Actual | Used value sau rounding/giới hạn môi trường render | Có thể lệch subpixel nhỏ |
.box {
width: 50%; /* specified/computed: 50%; 400px used trước constraint */
max-width: 300px; /* computed: 300px; used width cuối: 300px */
}
Custom property (--token) đặc biệt — cascade và kế thừa như giá trị specified, được thay thế lúc computed-value. em/rem resolve khi compute; vh/vw resolve theo kích thước viewport lúc compute.
getComputedStyle() có tên lịch sử hơi gây hiểu nhầm: API trả resolved value. Với phần lớn property đó là computed value; với property phụ thuộc layout như width/height, nó thường là used value theo pixel. Xem định nghĩa resolved values của CSSOM.
Trong series này, computed style là đầu vào của layout. Browser tính/invalidate tăng dần; nó không cần tạo một map đầy đủ cho toàn bộ document trước mọi lượt layout.
6. Từ DOM đã style tới layout tree
Tài liệu nhập môn thường gọi cấu trúc kế tiếp là render tree. Trong Blink hiện đại, layout tree/box tree là tên sát hơn: các layout object/box mang style và hình học; paint sau đó tạo paint chunk/display item. Mapping DOM → box không phải 1:1.
DOM Layout boxes (conceptual)
─── ─────────────────────────
html root box
├─ head ✗ excluded (not visible content)
├─ body body (computed styles)
│ ├─ h1 h1
│ ├─ p p
│ ├─ div.hidden ✗ display: none → excluded entirely
│ │ └─ span ✗ (child of display:none — pruned)
│ └─ p.note p.note
│ └─ ::before (pseudo) ::before (generated box, gets its own entry)
└─ script ✗ excluded
Quy tắc đưa vào chính:
| DOM node / style | Có layout box? |
|---|---|
display: none | Không — node và mọi con bị cắt |
visibility: hidden | Có — chiếm chỗ, không hiển thị, con có thể override bằng visibility: visible |
opacity: 0 | Có — vẫn layout và thường vẫn hit-test; browser có thể tối ưu paint/raster vì không có pixel nhìn thấy |
display: contents | Element không có principal box, nhưng box của con vẫn tham gia layout |
<head>, <script>, <meta> | Không — metadata không hiển thị |
::before, ::after | Có thể sinh box nếu content/display khiến nó được tạo |
| Text nodes | Có — bọc trong anonymous box nếu cần |
Layout tree là đầu vào để tính box, line fragment và vị trí. Node không tạo principal box vẫn có thể ảnh hưởng hệ thống theo cách khác (accessibility tree, metadata, script); “không có layout box” không đồng nghĩa “không tồn tại”.
7. Invalidate style và chi phí tính lại
Đổi class/attribute/DOM hay state như :hover có thể kích hoạt style invalidation. Engine đánh dấu phần có khả năng bị ảnh hưởng rồi chỉ rematch/recompute khi cần. Chi phí phụ thuộc số element bị invalidate, rule candidate và đường slow-path — không thể xếp hạng chính xác chỉ nhìn selector.
Potentially narrow: toggle class chỉ ảnh hưởng chính element đó
Potentially broad: toggle ancestor class xuất hiện trong selector descendant
Always new work: chèn subtree lớn; node mới cần style và layout tương ứng
Gạt theme trên <html>/<body> thường cố ý đổi style rộng, nên một lượt recalc rộng có thể là chi phí hợp lý. Custom property kế thừa cũng có thể invalidate hậu duệ dùng nó; đổi kỹ thuật không tự động làm rẻ hơn.
Quy trình đúng: record thao tác thật, tìm event Recalculate Style, bật CSS selector stats, rồi giảm số node bị ảnh hưởng hoặc selector có match-attempt đắt. content-visibility/containment có thể giúp subtree lớn trong đúng use case, nhưng phải đo lại vì chúng đổi layout/paint và có thể ảnh hưởng scroll, focus hay find-in-page behavior.
8. DOM + stylesheet → style + layout tree
Pipeline có thể chạy tăng dần khi HTML còn parse. Mental model chính xác hơn là dữ liệu DOM hiện có cùng rule áp dụng tạo computed style và layout box cần thiết:
┌─────────────┐ ┌─────────────┐
│ DOM hiện có │ │ stylesheets │
│ (Part 3) │ │ (Part 4) │
└──────┬──────┘ └──────┬──────┘
│ │
└─────────┬─────────┘
▼
┌───────────────────┐
│ Style computation │ cascade + inheritance → computed styles
└─────────┬─────────┘
▼
┌───────────────────┐
│ Layout tree │ boxes/fragments cần cho layout
└─────────┬─────────┘
▼
Layout (Part 5)
→ Paint
→ Composite
Điểm kiểm này giải thích hai quan sát hàng ngày:
- HTML có thể tiếp tục parse khi external stylesheet tải, nhưng stylesheet render-blocking có thể giữ lần paint đầu cho đến khi sẵn sàng.
display: nonelàm subtree không sinh layout box; bật lại thường cần style + layout + paint, không chỉ repaint.
Giờ bạn có cấu trúc dữ liệu pipeline render cần; Phần 5 biến box thành tọa độ và pixel.
Tóm tắt
- CSS được parse thành stylesheet/rule object của CSSOM; nó không phải cây song song 1:1 với DOM.
- Stylesheet render-blocking trì hoãn paint;
mediakhông khớp thường không chặn lần paint đầu. - Cascade xét relevance, origin/importance, layer, specificity, scoping proximity rồi order; inheritance/defaulting mới lấp giá trị còn thiếu.
width: 50%có thể giữ percentage ở computed value và chỉ thành pixel ở used value;getComputedStyle()trả resolved value.- Layout tree là mental model box, không phải tập “mọi DOM node nhìn thấy”;
display: nonecắt box subtree,visibility: hiddenvẫn giữ geometry. - Style invalidation là tăng dần; profile Recalculate Style + selector stats trước khi tối ưu selector.
Phần tiếp theo: Layout, paint & compositing — layout box thành fragment, paint record, pixel, và compositor frame thế nào.