DNA🚀 PerformanceRendering Performance & CSS Optimization
ðŸĢHatchlingPerformanceCSSRendering

Rendering Performance & CSS Optimization

Reflows, repaints, compositor layers, CSS containment — the rendering pipeline has its own performance discipline. Architects who understand it build UIs that never jank.

Rendering Performance & CSS Optimization

JavaScript gets all the performance attention, but the rendering pipeline is often the real bottleneck. A CSS animation using top instead of transform can cause more jank than unoptimized JavaScript. Understanding the render pipeline is what separates engineers who build smooth UIs from those who debug choppy ones.

The Render Pipeline

JavaScript → Style → Layout → Paint → Composite
    10ms       2ms     2ms     1ms      <1ms

Every frame, the browser runs this pipeline. The goal: complete within 16ms.

The Three Tiers of CSS Changes

Tier 1: Layout Properties (Most Expensive)

Changes that affect geometry trigger Layout → Paint → Composite:

/* All trigger full reflow */
width, height, min-width, max-width
margin, padding, border-width
top, right, bottom, left
font-size, font-weight, line-height
display, position, float
flex-basis, grid-template-columns

Tier 2: Paint Properties (Moderate)

Changes that affect appearance but not geometry trigger Paint → Composite:

/* Skip layout, still repaint */
color, background-color, background-image
box-shadow, text-shadow
border-color, border-style
outline, visibility

Tier 3: Composite Properties (Cheapest)

These only trigger Composite — handled entirely by the GPU:

/* Zero main thread cost */
transform (translate, rotate, scale)
opacity
filter (blur, brightness, contrast)
will-change

This is why transform: translateX(100px) is dramatically smoother than left: 100px.

Animation Best Practices

✅ GPU-Accelerated Animations

/* Smooth — only compositor */
.slide-in {
  transform: translateX(-100%);
  transition: transform 300ms ease-out;
}
.slide-in.active {
  transform: translateX(0);
}
 
.fade {
  opacity: 0;
  transition: opacity 200ms;
}
.fade.visible {
  opacity: 1;
}

❌ Layout-Triggering Animations

/* Janky — triggers layout every frame */
.slide-in {
  left: -100%;
  transition: left 300ms ease-out;
}
.slide-in.active {
  left: 0;
}

FLIP Technique for Layout Animations

When you need to animate layout changes (like reordering a list), use FLIP:

// First: record initial position
const first = element.getBoundingClientRect();
 
// Last: apply the change
applyLayoutChange(); // DOM mutation
const last = element.getBoundingClientRect();
 
// Invert: calculate the difference and apply it as transform
const dx = first.left - last.left;
const dy = first.top - last.top;
element.style.transform = `translate(${dx}px, ${dy}px)`;
 
// Play: animate to identity
requestAnimationFrame(() => {
  element.style.transition = 'transform 300ms ease';
  element.style.transform = '';
});

CSS Containment

contain tells the browser what won't affect other elements, enabling optimization:

/* Layout containment — element's internals don't affect external layout */
.card {
  contain: layout;
}
 
/* Paint containment — element won't paint outside its bounds */
.widget {
  contain: paint;
}
 
/* Size containment — element's size doesn't depend on children */
.fixed-panel {
  contain: size;
  width: 300px;
  height: 400px;
}
 
/* Strict — all containment types */
.isolated-component {
  contain: strict;
}
 
/* Content — layout + paint (most common) */
.card {
  contain: content;
}

content-visibility — The Biggest Win

/* Skip rendering for off-screen sections entirely */
.section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Estimated height for scrollbar */
}

content-visibility: auto tells the browser: "Don't render this until it's near the viewport." For pages with many sections, this can reduce initial render time by 50-90%.

will-change — Compositor Layer Promotion

/* Promote to its own compositor layer before animation starts */
.animated-element {
  will-change: transform;
}
 
/* Better: add before animation, remove after */
.card:hover {
  will-change: transform;
}
.card.animating {
  transform: scale(1.05);
  transition: transform 200ms;
}

Warning: Every promoted layer consumes GPU memory. Don't apply will-change to everything. Only use it for elements that actually animate.

CSS Selector Performance

Selectors: Right to Left

Browsers evaluate selectors right to left:

/* Browser process: find ALL divs, then filter to those inside .container header */
.container header div { ... }
 
/* Browser process: find ALL .title elements, done */
.title { ... }

Practical Rules

/* ✅ Fast — single class */
.card-title { color: #333; }
 
/* ✅ Fast — simple descendant */
.card .title { color: #333; }
 
/* ❌ Slow — universal selector as key */
.card * { margin: 0; }
 
/* ❌ Slow — deeply nested */
body .app .main .content .section .card .title { color: #333; }
 
/* ✅ Use BEM or flat selectors */
.card__title { color: #333; }

In practice, selector performance rarely matters unless you have 10,000+ elements. Focus on layout thrashing and animation optimization first.

Font Performance

/* Optimal font loading */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* Latin subset */
}
<!-- Preload the font file -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
font-displayFlash of Invisible TextFlash of Unstyled TextBest For
swapNoYesBody text (show content ASAP)
optionalBrief (100ms)NoBranded headings
fallbackBrief (100ms)BriefBalance

Size-Adjust for CLS Prevention

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  size-adjust: 107%;    /* Match fallback metrics */
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

This minimizes the layout shift when the custom font loads by adjusting its metrics to match the fallback font.

Critical CSS

Inline the CSS needed for above-the-fold content, defer the rest:

<head>
  <!-- Critical CSS: inline for instant rendering -->
  <style>
    body { margin: 0; font-family: system-ui; }
    .hero { min-height: 100vh; display: flex; align-items: center; }
    .nav { position: sticky; top: 0; background: white; }
  </style>
 
  <!-- Non-critical CSS: load async -->
  <link rel="preload" href="/styles/full.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles/full.css"></noscript>
</head>

Measuring Render Performance

// Frame rate monitoring
let frames = 0;
let lastTime = performance.now();
 
function countFrames(now) {
  frames++;
  if (now - lastTime >= 1000) {
    console.log(`FPS: ${frames}`);
    frames = 0;
    lastTime = now;
  }
  requestAnimationFrame(countFrames);
}
requestAnimationFrame(countFrames);
 
// Layout shift monitoring
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.warn('Layout shift:', entry.value, entry.sources);
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

Interview Signal

Senior candidates demonstrate:

  1. Three-tier knowledge — Layout vs paint vs composite properties and their costs
  2. Animation discipline — Only animate transform/opacity, FLIP technique for layout
  3. Containment awareness — contain and content-visibility for rendering optimization
  4. Font strategy — Preload, font-display, size-adjust for CLS prevention
  5. Pipeline thinking — Understanding each step of the render pipeline and what triggers what