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 */
}
| Value | Effect |
|---|---|
layout | Isolates layout from siblings |
paint | Clips painting, creates stacking context |
style | Prevents counter/quote side effects |
size | Element 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 .eforces 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@importis a blocking request; use bundler imports instead - Minification:
cssnano,lightningcss - Modern syntax:
lightningcsscompiles modern CSS with smaller output than PostCSS autoprefixer