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.

How This Post Works {Cách bài viết này hoạt động}

CSS optimization isn’t one skill — it’s a spectrum {Tối ưu CSS không phải một kỹ năng — mà là phổ rộng}. What you should focus on depends on where you are in your career {Điều bạn nên tập trung phụ thuộc vào vị trí trong sự nghiệp}.

This post is structured in three levels {Bài viết được cấu trúc theo ba cấp}:

  • Junior — fundamentals that prevent common mistakes {nền tảng ngăn lỗi phổ biến}
  • Senior — techniques that meaningfully improve real user metrics {kỹ thuật cải thiện thực sự các chỉ số người dùng}
  • Principal — architecture decisions that scale across large codebases {quyết định kiến trúc mở rộng cho codebase lớn}

Each level builds on the previous {Mỗi cấp xây dựng trên cấp trước}. Don’t skip ahead {Đừng nhảy bước} — a principal who can’t write a clean selector is a liability {một principal không viết được selector sạch là gánh nặng}.


Level 1: Junior — Write CSS That Doesn’t Hurt {Cấp 1: Junior — Viết CSS không gây hại}

At this level {Ở cấp này}, the goal is to stop making things worse {mục tiêu là ngừng làm mọi thứ tệ hơn}. Most CSS performance issues in production come from juniors (and some seniors) writing CSS that’s structurally problematic {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}.

1.1 Selector Specificity {Specificity của Selector}

Browsers match selectors right to left {Trình duyệt khớp selector từ phải sang trái}. This means the rightmost part (key selector) matters most for performance {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 { }

Rules {Quy tắc}:

  • Keep selectors flat — max 2-3 levels deep {Giữ selector phẳng — tối đa 2-3 cấp}
  • Avoid universal key selectors (*, div, span) {Tránh key selector phổ quát}
  • Prefer class selectors over tag selectors {Ưu tiên class selector hơn tag selector}
  • Never use ID selectors for styling {Không bao giờ dùng ID selector cho styling} (specificity too high)

1.2 Avoid Layout Thrashing {Tránh Layout Thrashing}

Layout thrashing happens when you read and write geometry in alternating fashion {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
});

1.3 Use Shorthand Properties Wisely {Dùng thuộc tính rút gọn khôn ngoan}

Shorthand properties reset ALL sub-properties {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;
}

1.4 Reduce Repaints {Giảm Repaint}

Some CSS properties are expensive because they trigger layout or paint {Một số thuộc tính CSS tốn kém vì chúng kích hoạt layout hoặc paint}:

Cost {Chi phí}Properties {Thuộc tính}Triggers {Kích hoạt}
🔴 Very expensive {Rất tốn}width, height, top, left, margin, padding, font-sizeLayout + Paint + Composite
🟡 Moderate {Trung bình}color, background-color, box-shadow, border-colorPaint + Composite
🟢 Cheap {Rẻ}transform, opacity, filterComposite only

The golden rule {Quy tắc vàng}: animate ONLY transform and opacity {animate CHỈ transformopacity}. Everything else forces the browser to recalculate layout or repaint {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;
}

1.5 Don’t Overuse !important {Đừng lạm dụng !important}

!important isn’t a performance issue directly {!important không phải vấn đề performance trực tiếp} — it’s a maintenance bomb {nó là quả bom bảo trì}. Once you start, every override needs another !important {Khi bạn bắt đầu, mọi override cần thêm !important}, making the cascade unpredictable and refactoring impossible {khiến cascade không dự đoán được và refactor không thể}.

Fix {Sửa}: understand specificity, use BEM or utility classes, structure your selectors so the cascade works FOR you {hiểu specificity, dùng BEM hoặc utility class, cấu trúc selector để cascade hoạt động CHO bạn}.


Level 2: Senior — Measurably Improve User Experience {Cấp 2: Senior — Cải thiện UX đo lường được}

At this level {Ở cấp này}, you’re not just avoiding problems — you’re actively making things faster {bạn không chỉ tránh vấn đề — mà chủ động làm mọi thứ nhanh hơn}. You think in terms of Core Web Vitals {Bạn suy nghĩ theo Core Web Vitals}: LCP, INP, CLS.

2.1 Critical CSS {CSS quan trọng}

CSS is render-blocking {CSS là render-blocking} — the browser won’t paint until it has downloaded and parsed ALL CSS {trình duyệt không paint cho đến khi đã tải và phân tích TẤT CẢ CSS}. Critical CSS solves this by inlining above-the-fold styles {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>

Target {Mục tiêu}: critical CSS should be under 14KB {critical CSS nên dưới 14KB} (fits in first TCP congestion window) {vừa trong cửa sổ tắc nghẽn TCP đầu tiên}.

Tools {Công cụ}: critical npm package, Astro’s built-in CSS scoping, or manual extraction {hoặc trích xuất thủ công}.

2.2 CSS Containment {CSS Containment}

contain tells the browser that an element’s subtree is independent {contain cho browser biết subtree của element là độc lập} — changes inside won’t affect the rest of the page {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;
}

When to use {Khi nào dùng}:

  • Cards in a grid {Card trong grid}
  • Modal/dialog overlays
  • Independent widgets (chat, notifications)
  • Any component that doesn’t affect siblings {Bất kỳ component nào không ảnh hưởng anh em}

2.3 content-visibility — The Biggest Win {Chiến thắng lớn nhất}

content-visibility: auto is the single most impactful CSS property for long pages {content-visibility: auto là thuộc tính CSS ảnh hưởng lớn nhất cho trang dài}. It tells the browser to skip rendering offscreen elements entirely {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;
}

How it works {Cách hoạt động}:

  1. Element is offscreen → browser skips Style, Layout, Paint entirely {Element ngoài viewport → browser bỏ qua Style, Layout, Paint hoàn toàn}
  2. Element approaches viewport (~1 viewport away) → browser pre-renders {Element tiến gần viewport → browser pre-render}
  3. Element is visible → fully rendered {Element hiển thị → render đầy đủ}
  4. Element leaves viewport → content can be discarded {Element rời viewport → nội dung có thể bị huỷ}

Real impact {Tác động thực}: 7-10x faster initial render on content-heavy pages {nhanh hơn 7-10 lần render ban đầu trên trang nhiều nội dung}.

contain-intrinsic-size provides a placeholder size so scrollbar doesn’t jump {cung cấp kích thước placeholder để scrollbar không nhảy}. Use auto keyword to let the browser remember the real size after first render {Dùng từ khoá auto để browser nhớ kích thước thật sau lần render đầu}.

2.4 Font Optimization {Tối ưu Font}

Fonts are one of the biggest CLS culprits {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%;
}

Checklist {Danh sách kiểm tra}:

  • Use WOFF2 format (30% smaller than WOFF) {Dùng định dạng WOFF2 (nhỏ hơn WOFF 30%)}
  • Subset fonts to only characters you need {Subset font chỉ ký tự bạn cần}
  • Use variable fonts instead of multiple weights {Dùng variable font thay vì nhiều weight}
  • Preload critical fonts {Preload font quan trọng}: <link rel="preload" href="font.woff2" as="font" crossorigin>
  • Total font budget: under 100KB {Ngân sách font tổng: dưới 100KB}

2.5 will-change — Use Sparingly {Dùng tiết kiệm}

will-change promotes an element to its own compositor layer {will-change đẩy element lên layer compositor riêng}. This makes future animations smooth but costs GPU memory {Đ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);
}

Best practice {Thực hành tốt nhất}: apply via JavaScript just before animation starts, remove when animation ends {áp dụng qua JavaScript ngay trước khi animation bắt đầu, gỡ khi animation kết thúc}.

2.6 Reduce Unused CSS {Giảm CSS không dùng}

Average website ships 80% unused CSS {Website trung bình gửi 80% CSS không dùng}. Tools to fix {Công cụ để sửa}:

  • Chrome DevTools Coverage tab — shows exactly which CSS rules are used {hiển thị chính xác rule CSS nào được dùng}
  • PurgeCSS / Tailwind’s JIT — removes unused utilities at build time {loại bỏ utility không dùng lúc build}
  • CSS Modules / Scoped Styles — naturally tree-shake by component {tự nhiên tree-shake theo component}
  • Astro’s built-in scoping — only ships CSS for rendered components {chỉ gửi CSS cho component được render}
# Check your CSS coverage
# Open DevTools → Sources → Coverage → click reload
# Red = unused, Blue = used

Level 3: Principal — Architecture That Scales {Cấp 3: Principal — Kiến trúc mở rộng được}

At this level {Ở cấp này}, you’re making decisions that affect the entire engineering organization {bạn đưa ra quyết định ảnh hưởng toàn bộ tổ chức engineering}. Individual optimizations matter less than systemic ones {Tối ưu từng cái ít quan trọng hơn tối ưu hệ thống}.

3.1 CSS Architecture Decisions {Quyết định kiến trúc CSS}

The biggest CSS performance win is not writing CSS that needs to be optimized {Chiến thắng performance CSS lớn nhất là không viết CSS cần phải tối ưu}:

Approach {Cách tiếp cận}Bundle size {Kích thước bundle}Runtime cost {Chi phí runtime}Maintenance {Bảo trì}
Utility-first (Tailwind)Grows logarithmically {Tăng logarithm}Zero (static classes)Easy to delete {Dễ xoá}
CSS ModulesPer-component {Theo component}Zero (scoped)Medium
CSS-in-JS (runtime)Small initialJS execution per render {Thực thi JS mỗi render}Coupled to JS
CSS-in-JS (zero-runtime)Per-componentZero (extracted at build)Medium
Global stylesheetGrows linearly {Tăng tuyến tính}ZeroHard to delete {Khó xoá}

Principal-level decision {Quyết định cấp principal}: choose a CSS strategy that makes bad CSS impossible to ship {chọn chiến lược CSS khiến CSS tệ không thể được gửi đi}, not one that requires discipline to use correctly {không phải chiến lược đòi hỏi kỷ luật để dùng đúng}.

3.2 Design Token Architecture {Kiến trúc Design Token}

Tokens are the single source of truth for visual decisions {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;
}

Why tokens matter for performance {Tại sao token quan trọng cho performance}:

  • Theme changes = one custom property update, browser handles invalidation {Thay đổi theme = một update custom property, browser xử lý invalidation}
  • No runtime JavaScript for theming {Không cần JavaScript runtime cho theming}
  • Browser optimizes custom property inheritance natively {Browser tối ưu kế thừa custom property native}

3.3 The Render Pipeline Mental Model {Mô hình tư duy Render Pipeline}

A principal needs to understand what happens between CSS and pixels {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)

Principal-level insight {Nhận thức cấp principal}: most performance problems aren’t individual property choices {hầu hết vấn đề performance không phải lựa chọn từng thuộc tính} — they’re architectural decisions that force expensive properties to be used {mà là quyết định kiến trúc buộc dùng thuộc tính tốn kém}.

Example {Ví dụ}: if your design system requires height animations for accordions {nếu design system yêu cầu animation chiều cao cho accordion}, every team will animate height (expensive) {mọi team sẽ animate height (tốn kém)}. A principal would instead provide a Accordion component that uses grid-template-rows or clip-path internally {Principal sẽ thay thế bằng cung cấp component Accordion dùng grid-template-rows hoặc clip-path bên trong}.

3.4 Performance Budgets for CSS {Ngân sách Performance cho CSS}

Set and enforce budgets {Đặt và thực thi ngân sách}:

Metric {Chỉ số}Budget {Ngân sách}Why {Tại sao}
Critical CSS≤ 14KB (gzipped)First TCP congestion window {Cửa sổ tắc nghẽn TCP đầu tiên}
Total CSS≤ 50KB (gzipped)Diminishing returns beyond this {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≤ 100KBAvoid layout shift {Tránh layout shift}
Animationscompositor-only60fps guarantee {Đảm bảo 60fps}

Enforce in CI {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

3.5 Compositor-Driven Animations {Animation do Compositor điều khiển}

The compositor thread runs independently from the main thread {Thread compositor chạy độc lập với main thread}. If all your animations use compositor-only properties {Nếu tất cả animation dùng thuộc tính compositor-only}, they stay smooth even when JavaScript is busy {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;
}

Modern APIs that keep animations off the main thread {API hiện đại giữ animation khỏi main thread}:

  • animation-timeline: scroll() — scroll-linked without JS {liên kết scroll không cần JS}
  • animation-timeline: view() — intersection-based {dựa trên intersection}
  • View Transitions API — page transitions on compositor {chuyển trang trên compositor}
  • @starting-style — entry animations without JS

3.6 Layer Management Strategy {Chiến lược quản lý Layer}

Every compositor layer consumes GPU memory {Mỗi layer compositor tiêu thụ bộ nhớ GPU}. A principal’s job is to prevent layer explosion {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

Audit layers {Kiểm tra layer}: Chrome DevTools → Layers panel → sort by memory.

Strategy {Chiến lược}:

  • Budget: max 30-50 layers for typical app {tối đa 30-50 layer cho app thông thường}
  • Animate in isolation — don’t overlap animated and static content {Animate trong cô lập — không chồng nội dung animated và static}
  • Use contain: strict on independent widgets to cap overlap cascading {Dùng contain: strict trên widget độc lập để giới hạn cascading overlap}

3.7 CSS Loading Strategy {Chiến lược tải CSS}

For large applications {Cho ứng dụng lớn}, how CSS is loaded matters more than what it contains {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)

3.8 Measuring What Matters {Đo những gì quan trọng}

A principal sets up continuous monitoring {Principal thiết lập giám sát liên tục}, not one-time audits {không phải kiểm tra một lần}:

What to measure {Đo gì}Tool {Công cụ}Target {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

Quick Reference: What to Optimize at Each Level {Tham khảo nhanh: Tối ưu gì ở mỗi cấp}

Level {Cấp}Focus {Tập trung}Impact {Tác động}
JuniorClean selectors, avoid layout thrashing, animate transform/opacity onlyPrevent regressions {Ngăn regression}
SeniorCritical CSS, containment, content-visibility, font optimization, unused CSS removalMeasurably improve Core Web Vitals {Cải thiện CWV đo được}
PrincipalCSS architecture, design tokens, performance budgets, layer strategy, loading patterns, continuous monitoringScale performance across org {Mở rộng performance toàn tổ chức}

The One Thing at Each Level {Một điều ở mỗi cấp}

If you can only remember one thing from each level {Nếu bạn chỉ nhớ được một điều từ mỗi cấp}:

  • Junior: Only animate transform and opacity {Chỉ animate transformopacity}. Everything else is expensive. {Mọi thứ khác đều tốn kém.}
  • Senior: Use content-visibility: auto on below-fold content {Dùng content-visibility: auto cho nội dung dưới fold}. It’s the highest ROI CSS property ever created. {Đó là thuộc tính CSS có ROI cao nhất từng được tạo.}
  • Principal: Choose a CSS architecture that makes bad patterns impossible {Chọn kiến trúc CSS khiến pattern tệ không thể xảy ra}, not one that requires constant vigilance. {không phải kiến trúc đòi hỏi cảnh giác liên tục.}