CSS Optimization by Level — From Junior to Principal
A progressive guide to CSS performance optimization structured by developer seniority. Each level builds on the previous — from writing clean selectors to architecting compositor-only animation pipelines.
Cách bài viết này hoạt động
Tối ưu CSS không phải một kỹ năng — mà là phổ rộng. Điều bạn nên tập trung phụ thuộc vào vị trí trong sự nghiệp.
Bài viết được cấu trúc theo ba cấp:
- nền tảng ngăn lỗi phổ biến
- kỹ thuật cải thiện thực sự các chỉ số người dùng
- quyết định kiến trúc mở rộng cho codebase lớn
Mỗi cấp xây dựng trên cấp trước. Đừng nhảy bước — một principal không viết được selector sạch là gánh nặng.
Cấp 1: Junior — Viết CSS không gây hại
Ở cấp này, mục tiêu là ngừng làm mọi thứ tệ hơn. Hầu hết vấn đề performance CSS production đến từ junior (và vài senior) viết CSS có vấn đề về cấu trúc.
Specificity của Selector
Trình duyệt khớp selector từ phải sang trái. Phần ngoài cùng bên phải (key selector) ảnh hưởng nhiều nhất đến performance.
/* ❌ Slow — browser checks EVERY div, then walks up to find .sidebar */
.sidebar > .nav > ul > li > div { }
/* ✅ Fast — browser only checks elements with this class */
.nav-item { }
Quy tắc:
- Giữ selector phẳng — tối đa 2-3 cấp
- Tránh key selector phổ quát
- Ưu tiên class selector hơn tag selector
- Không bao giờ dùng ID selector cho styling
Tránh Layout Thrashing
Layout thrashing xảy ra khi bạn đọc và ghi hình học xen kẽ:
// ❌ Layout thrashing — forces reflow on every iteration
for (const el of elements) {
const height = el.offsetHeight; // READ (forces layout)
el.style.height = `${height * 2}px`; // WRITE (invalidates layout)
}
// ✅ Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // ALL READS
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`; // ALL WRITES
});
Dùng thuộc tính rút gọn khôn ngoan
Thuộc tính rút gọn reset TẤT CẢ thuộc tính con:
/* ❌ This resets animation-delay, animation-fill-mode, etc. */
.box {
animation-duration: 2s;
animation: slide-in; /* oops — overwrites duration! */
}
/* ❌ This resets background-position, background-size, etc. */
.hero {
background-size: cover;
background: url('bg.jpg'); /* resets size to 'auto'! */
}
/* ✅ Use longhand when you need to preserve other sub-properties */
.hero {
background-image: url('bg.jpg');
background-size: cover;
background-position: center;
}
Giảm Repaint
Một số thuộc tính CSS tốn kém vì chúng kích hoạt layout hoặc paint:
| Chi phí | Thuộc tính | Kích hoạt |
|---|---|---|
| Rất tốn | width, height, top, left, margin, padding, font-size | Layout + Paint + Composite |
| Trung bình | color, background-color, box-shadow, border-color | Paint + Composite |
| Rẻ | transform, opacity, filter | Composite only |
Quy tắc vàng: animate CHỈ transform và opacity. Mọi thứ khác buộc browser tính lại layout hoặc repaint.
/* ❌ Animating top/left — triggers layout every frame */
.modal {
transition: top 0.3s, left 0.3s;
}
/* ✅ Animating transform — compositor-only, smooth 60fps */
.modal {
transition: transform 0.3s;
}
/* ❌ Animating height for accordion */
.accordion-body {
transition: height 0.3s;
}
/* ✅ Use grid/transform tricks or clip-path */
.accordion-body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s;
}
.accordion-body.open {
grid-template-rows: 1fr;
}
Đừng lạm dụng !important
!important không phải vấn đề performance trực tiếp — nó là quả bom bảo trì. Khi bạn bắt đầu, mọi override cần thêm !important, khiến cascade không dự đoán được và refactor không thể.
Sửa: hiểu specificity, dùng BEM hoặc utility class, cấu trúc selector để cascade hoạt động CHO bạn.
Cấp 2: Senior — Cải thiện UX đo lường được
Ở cấp này, bạn không chỉ tránh vấn đề — mà chủ động làm mọi thứ nhanh hơn. Bạn suy nghĩ theo Core Web Vitals:
CSS quan trọng
CSS là render-blocking — trình duyệt không paint cho đến khi đã tải và phân tích TẤT CẢ CSS. Critical CSS giải quyết bằng cách inline style trên fold:
<head>
<!-- Critical CSS inlined — renders immediately -->
<style>
.hero { display: grid; min-height: 100vh; }
.nav { position: sticky; top: 0; }
/* Only what's needed for first viewport paint */
</style>
<!-- Rest loaded asynchronously -->
<link rel="preload" href="/styles/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
</head>
Mục tiêu: critical CSS nên dưới 14KB vừa trong cửa sổ tắc nghẽn TCP đầu tiên.
Công cụ: critical hoặc trích xuất thủ công.
CSS Containment
contain cho browser biết subtree của element là độc lập — thay đổi bên trong không ảnh hưởng phần còn lại:
/* Layout containment — size changes inside .card
don't trigger layout on siblings */
.card {
contain: layout;
}
/* Content containment (layout + paint + style) */
.widget {
contain: content;
}
/* Strict containment (layout + paint + style + size) */
.modal {
contain: strict;
width: 500px;
height: 400px;
}
Khi nào dùng:
- Card trong grid
- Modal/dialog overlays
- Independent widgets (chat, notifications)
- Bất kỳ component nào không ảnh hưởng anh em
Chiến thắng lớn nhất
content-visibility: auto là thuộc tính CSS ảnh hưởng lớn nhất cho trang dài. Nó bảo browser bỏ qua việc render phần tử ngoài viewport hoàn toàn:
/* Each section below the fold skips layout+paint until visible */
.blog-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
Cách hoạt động:
- Element ngoài viewport → browser bỏ qua Style, Layout, Paint hoàn toàn
- Element tiến gần viewport → browser pre-render
- Element hiển thị → render đầy đủ
- Element rời viewport → nội dung có thể bị huỷ
Tác động thực: 7-10nhanh hơn 7-10 lần render ban đầu trên trang nhiều nội dung.
cung cấp kích thước placeholder để scrollbar không nhảy. Dùng từ khoá auto để browser nhớ kích thước thật sau lần render đầu.
Tối ưu Font
Font là một trong những thủ phạm CLS lớn nhất:
/* ❌ Font swap causes visible layout shift */
@font-face {
font-family: 'Custom';
src: url('/fonts/custom.woff2');
font-display: swap;
}
/* ✅ Use font metrics override to match fallback dimensions */
@font-face {
font-family: 'Custom';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
size-adjust: 105%;
}
Danh sách kiểm tra:
- Dùng định dạng WOFF2 (nhỏ hơn WOFF 30%)
- Subset font chỉ ký tự bạn cần
- Dùng variable font thay vì nhiều weight
- Preload font quan trọng:
<link rel="preload" href="font.woff2" as="font" crossorigin> - Ngân sách font tổng: dưới 100KB
Dùng tiết kiệm
will-change đẩy element lên layer compositor riêng. Điều này làm animation tương lai mượt nhưng tốn bộ nhớ GPU:
/* ❌ Never put will-change on everything */
* { will-change: transform; }
/* ❌ Don't leave it permanently on idle elements */
.card { will-change: transform; }
/* ✅ Apply when animation is about to start */
.card:hover {
will-change: transform;
}
.card.animating {
will-change: transform;
transform: scale(1.05);
}
Thực hành tốt nhất: áp dụng qua JavaScript ngay trước khi animation bắt đầu, gỡ khi animation kết thúc.
Giảm CSS không dùng
Website trung bình gửi 80% CSS không dùng. Công cụ để sửa:
- hiển thị chính xác rule CSS nào được dùng
- loại bỏ utility không dùng lúc build
- tự nhiên tree-shake theo component
- chỉ gửi CSS cho component được render
# Check your CSS coverage
# Open DevTools → Sources → Coverage → click reload
# Red = unused, Blue = used
Cấp 3: Principal — Kiến trúc mở rộng được
Ở cấp này, bạn đưa ra quyết định ảnh hưởng toàn bộ tổ chức engineering. Tối ưu từng cái ít quan trọng hơn tối ưu hệ thống.
Quyết định kiến trúc CSS
Chiến thắng performance CSS lớn nhất là không viết CSS cần phải tối ưu:
| Cách tiếp cận | Kích thước bundle | Chi phí runtime | Bảo trì |
|---|---|---|---|
| Utility-first (Tailwind) | Tăng logarithm | Zero (static classes) | Dễ xoá |
| CSS Modules | Theo component | Zero (scoped) | Medium |
| CSS-in-JS (runtime) | Small initial | Thực thi JS mỗi render | Coupled to JS |
| CSS-in-JS (zero-runtime) | Per-component | Zero (extracted at build) | Medium |
| Global stylesheet | Tăng tuyến tính | Zero | Khó xoá |
Quyết định cấp principal: chọn chiến lược CSS khiến CSS tệ không thể được gửi đi, không phải chiến lược đòi hỏi kỷ luật để dùng đúng.
Kiến trúc Design Token
Token là nguồn sự thật duy nhất cho quyết định hình ảnh:
/* Design tokens via CSS custom properties — zero runtime cost */
@theme {
--color-bg: #0a0a0a;
--color-surface: #111111;
--color-accent: #c8ff00;
--spacing-sm: 4px;
--spacing-md: 8px;
--spacing-lg: 16px;
--radius-sm: 4px;
--font-mono: 'JetBrains Mono', monospace;
}
Tại sao token quan trọng cho performance:
- Thay đổi theme = một update custom property, browser xử lý invalidation
- Không cần JavaScript runtime cho theming
- Browser tối ưu kế thừa custom property native
Mô hình tư duy Render Pipeline
Principal cần hiểu điều gì xảy ra giữa CSS và pixel:
CSS → Style → Layout → Paint → Composite → Pixels
↑
GPU renders here
Expensive changes:
{Thay đổi tốn kém:}
├── Layout properties → recalculate ALL subsequent steps
│ (width, height, margin, padding, top, left, font-size)
│
├── Paint properties → skip layout, still expensive
│ (color, background, box-shadow, border-radius)
│
└── Composite properties → GPU only, cheapest possible
(transform, opacity, filter, will-change)
Nhận thức cấp principal: hầu hết vấn đề performance không phải lựa chọn từng thuộc tính — mà là quyết định kiến trúc buộc dùng thuộc tính tốn kém.
Ví dụ: nếu design system yêu cầu animation chiều cao cho accordion, mọi team sẽ animate height (tốn kém). Principal sẽ thay thế bằng cung cấp component Accordion dùng grid-template-rows hoặc clip-path bên trong.
Ngân sách Performance cho CSS
Đặt và thực thi ngân sách:
| Chỉ số | Ngân sách | Tại sao |
|---|---|---|
| Critical CSS | ≤ 14KB (gzipped) | Cửa sổ tắc nghẽn TCP đầu tiên |
| Total CSS | ≤ 50KB (gzipped) | Hiệu quả giảm dần vượt mức này |
| Unused CSS | ≤ 20% | Coverage tab target |
| Selector depth | ≤ 3 levels | Matching speed + maintainability |
| Font total | ≤ 100KB | Tránh layout shift |
| Animations | compositor-only | 60Đảm bảo 60fps |
Thực thi trong CI:
# Example: fail build if CSS exceeds budget
TOTAL_CSS=$(find dist -name "*.css" -exec wc -c {} + | tail -1 | awk '{print $1}')
if [ "$TOTAL_CSS" -gt 51200 ]; then
echo "CSS budget exceeded: ${TOTAL_CSS} bytes (max 50KB)"
exit 1
fi
Animation do Compositor điều khiển
Thread compositor chạy độc lập với main thread. Nếu tất cả animation dùng thuộc tính compositor-only, chúng vẫn mượt ngay cả khi JavaScript đang bận:
/* ✅ Scroll-driven animation (compositor thread) */
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.section {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* ✅ View Transitions API (compositor-managed) */
::view-transition-old(page) {
animation: fade-out 0.2s ease;
}
::view-transition-new(page) {
animation: fade-in 0.3s ease;
}
API hiện đại giữ animation khỏi main thread:
- liên kết scroll không cần JS
- dựa trên intersection
- chuyển trang trên compositor
@starting-style— entry animations without JS
Chiến lược quản lý Layer
Mỗi layer compositor tiêu thụ bộ nhớ GPU. Việc của principal là ngăn bùng nổ layer:
Common causes of excessive layers:
{Nguyên nhân phổ biến tạo quá nhiều layer:}
1. will-change on idle elements (50+ layers just sitting there)
2. z-index stacking forcing overlap promotion
3. position: fixed on many elements (mobile)
4. Animated elements overlapping non-animated elements
Kiểm tra layer:
Chiến lược:
- tối đa 30-50 layer cho app thông thường
- Animate trong cô lập — không chồng nội dung animated và static
- Dùng
contain: stricttrên widget độc lập để giới hạn cascading overlap
Chiến lược tải CSS
Cho ứng dụng lớn, cách CSS được tải quan trọng hơn nội dung của nó:
Strategy 1: Component-Level Code Splitting
{Chiến lược 1: Tách code cấp component}
┌── critical.css (inline, ≤14KB)
Global Shell ─────┤
└── shell.css (preloaded)
┌── dashboard.css (loaded with route)
Route Level ──────┤
└── settings.css (loaded with route)
┌── chart.css (loaded when Chart mounts)
Component Level ──┤
└── modal.css (loaded when Modal opens)
Strategy 2: Priority-Based Loading
{Chiến lược 2: Tải theo ưu tiên}
1. Inline critical CSS (above-fold styles, ≤14KB)
2. Preload route CSS (non-blocking, high priority)
3. Lazy-load below-fold component CSS (low priority)
4. Prefetch next-page CSS on idle (speculative)
Đo những gì quan trọng
Principal thiết lập giám sát liên tục, không phải kiểm tra một lần:
| Đo gì | Công cụ | Mục tiêu |
|---|---|---|
| Unused CSS % | Chrome Coverage | ≤ 20% |
| Render-blocking time | WebPageTest / Lighthouse | ≤ 100ms |
| Layout shifts from CSS | CLS field data (CrUX) | ≤ 0.1 |
| Animation frame drops | DevTools Performance | 0 drops at 60fps |
| CSS bundle growth | CI size check | ≤ 50KB gzipped |
| Time to First Paint | Real User Monitoring | p75 ≤ 1.2s |
Tham khảo nhanh: Tối ưu gì ở mỗi cấp
| Cấp | Tập trung | Tác động |
|---|---|---|
| Junior | Clean selectors, avoid layout thrashing, animate transform/opacity only | Ngăn regression |
| Senior | Critical CSS, containment, content-visibility, font optimization, unused CSS removal | Cải thiện CWV đo được |
| Principal | CSS architecture, design tokens, performance budgets, layer strategy, loading patterns, continuous monitoring | Mở rộng performance toàn tổ chức |
Một điều ở mỗi cấp
Nếu bạn chỉ nhớ được một điều từ mỗi cấp:
- Chỉ animate
transformvàopacity. Mọi thứ khác đều tốn kém. - Dùng
content-visibility: autocho nội dung dưới fold. Đó là thuộc tính CSS có ROI cao nhất từng được tạo. - Chọn kiến trúc CSS khiến pattern tệ không thể xảy ra, không phải kiến trúc đòi hỏi cảnh giác liên tục.