Skip to main content

Performance

Critical CSS

Inline above-the-fold CSS to eliminate render-blocking:

<head>
<style>
/* Critical — inlined */
.hero { min-height: 100dvh; display: grid; place-items: center; }
.nav { position: sticky; top: 0; }
</style>
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>

Tools: critical, critters (webpack plugin), @astrojs/critters.

Layout Thrashing

Reading layout properties forces the browser to recalculate — avoid interleaving reads and writes:

// Bad — forces layout on every iteration
items.forEach(item => {
const height = item.offsetHeight; // read (forces layout)
item.style.height = height + 10 + 'px'; // write (invalidates layout)
});

// Good — batch reads, then batch writes
const heights = items.map(item => item.offsetHeight);
items.forEach((item, i) => {
item.style.height = heights[i] + 10 + 'px';
});

CSS contain

Limit the browser's recalculation scope:

.card {
contain: layout style paint;
/* or shorthand: */
contain: strict; /* layout + style + paint + size */
content-visibility: auto; /* skip rendering off-screen elements */
}
ValueEffect
layoutIsolates layout from siblings
paintClips painting, creates stacking context
stylePrevents counter/quote side effects
sizeElement doesn't depend on children for sizing

content-visibility: auto

Skips rendering of off-screen elements entirely — massive performance win for long lists:

.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px; /* estimated height to prevent scrollbar jump */
}

Selector Performance

Selectors are matched right to left. Tips:

  • Avoid universal key selectors: div * is slow
  • Avoid deeply nested selectors: .a .b .c .d .e forces many checks
  • ID and class selectors are fast
  • :has() is expensive — use on specific containers, not *

In practice, selector performance rarely matters unless you have 10,000+ elements. Focus on reducing layout/paint triggers instead.

Reducing CSS Bundle Size

  • PurgeCSS / Tailwind purge: Remove unused classes at build time
  • Code splitting: Load CSS per route, not one global bundle
  • Avoid @import: Each @import is a blocking request; use bundler imports instead
  • Minification: cssnano, lightningcss
  • Modern syntax: lightningcss compiles modern CSS with smaller output than PostCSS autoprefixer