jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ínhKích hoạt
Rất tốnwidth, height, top, left, margin, padding, font-sizeLayout + Paint + Composite
Trung bìnhcolor, background-color, box-shadow, border-colorPaint + Composite
Rẻtransform, opacity, filterComposite only

Quy tắc vàng: animate CHỈ transformopacity. 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:

  1. Element ngoài viewport → browser bỏ qua Style, Layout, Paint hoàn toàn
  2. Element tiến gần viewport → browser pre-render
  3. Element hiển thị → render đầy đủ
  4. 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ậnKích thước bundleChi phí runtimeBảo trì
Utility-first (Tailwind)Tăng logarithmZero (static classes)Dễ xoá
CSS ModulesTheo componentZero (scoped)Medium
CSS-in-JS (runtime)Small initialThực thi JS mỗi renderCoupled to JS
CSS-in-JS (zero-runtime)Per-componentZero (extracted at build)Medium
Global stylesheetTăng tuyến tínhZeroKhó 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áchTạ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 levelsMatching speed + maintainability
Font total≤ 100KBTránh layout shift
Animationscompositor-only60Đả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: strict trê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 timeWebPageTest / Lighthouse≤ 100ms
Layout shifts from CSSCLS field data (CrUX)≤ 0.1
Animation frame dropsDevTools Performance0 drops at 60fps
CSS bundle growthCI size check≤ 50KB gzipped
Time to First PaintReal User Monitoringp75 ≤ 1.2s

Tham khảo nhanh: Tối ưu gì ở mỗi cấp

CấpTập trungTác động
JuniorClean selectors, avoid layout thrashing, animate transform/opacity onlyNgăn regression
SeniorCritical CSS, containment, content-visibility, font optimization, unused CSS removalCải thiện CWV đo được
PrincipalCSS architecture, design tokens, performance budgets, layer strategy, loading patterns, continuous monitoringMở 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 transformopacity. Mọi thứ khác đều tốn kém.
  • Dùng content-visibility: auto cho 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.