Blog · Performance & Optimization
How to Optimize CSS in 2026: 14 Techniques for Fast Styles
Practical CSS performance techniques — remove unused CSS, inline critical styles, GPU-composited animation, content-visibility, containment and font optimization — each with why it matters and a code example.
CSS is render-blocking: the browser won’t paint until it has processed the stylesheets in the critical path. That makes CSS a high-leverage place to optimize for a fast first paint and better Core Web Vitals. Here are 14 CSS performance techniques for 2026, each with the reasoning and a code example.
1. Remove unused CSS
Shipping selectors that never match wastes bytes and parse time — utility frameworks especially can emit a large surface. Tree-shaking CSS against your actual markup can cut a stylesheet from tens of KB to a few. Note that tools like PurgeCSS match class *strings* and do not run your JavaScript, so dynamically-built class names get purged — use a safelist or write complete literal class strings.
// tailwind.config.js — scan the files that actually use classes
export default {
content: ["./src/**/*.{html,js,ts,jsx,tsx,vue}"],
};
2. Inline critical CSS and defer the rest
Because CSS blocks rendering, inlining just the styles needed to paint the first viewport removes a network round-trip from the critical path, so the page renders before the full stylesheet arrives. Keep the inlined CSS small (roughly under ~14 KB) and automate extraction with a tool like Critters/Beasties rather than hand-maintaining it.
<head>
<style>/* critical above-the-fold rules only */</style>
<link rel="preload" href="/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>
3. Load non-critical CSS asynchronously
A plain stylesheet link blocks rendering. Loading it with a non-matching media type lets the browser fetch it at low priority without blocking, then you swap it to all once loaded. Always include a <noscript> fallback.
<!-- non-blocking load, then apply -->
<link rel="stylesheet" href="/below-fold.css" media="print" onload="this.media='all'">
4. Split CSS by media so it doesn't block unnecessarily
A stylesheet whose media query doesn’t match the current device is fetched at low priority and doesn’t block the initial render, so print or large-screen styles never delay first paint. They still download — this is about priority and ordering, not eliminating the request.
<link rel="stylesheet" href="base.css"> <!-- blocks -->
<link rel="stylesheet" href="print.css" media="print"> <!-- non-blocking -->
<link rel="stylesheet" href="wide.css" media="(min-width:1024px)">
5. Avoid @import in delivered CSS
An @import inside a stylesheet is only discovered after the parent CSS downloads and parses, creating a serial request chain that delays rendering; <link> tags are discovered up front and fetched in parallel by the preload scanner. (Build-time @import in Sass/PostCSS that gets inlined is fine — it never ships as a runtime @import.)
/* ❌ serial, blocks — can't be preloaded */
@import url("typography.css");
<!-- ✅ parallel, preload-scanner-friendly -->
<link rel="stylesheet" href="typography.css">
6. Animate transform and opacity, not layout properties
transform and opacity can be handled by the compositor thread without triggering layout or paint, so they stay smooth at 60fps. Animating top/left/width/height forces layout on every frame, and animating colors or shadows forces paint. Note transform doesn’t reflow surrounding content — design around that.
/* ❌ animates layout every frame */
.box { transition: left .3s, height .3s; }
/* ✅ composited, cheap */
.box { transition: transform .3s, opacity .3s; }
.box:hover { transform: translateX(20px) scale(1.05); }
7. Use will-change sparingly
will-change hints the browser to promote an element to its own compositor layer ahead of a change, but applied to many elements — or left on permanently in a stylesheet — it balloons GPU memory and can make the page slower. Only reach for it after you measure jank, and toggle it around the interaction.
/* ❌ don't leave it on everything */
* { will-change: transform; }
/* ✅ scope it, toggle around the interaction */
.menu.is-opening { will-change: transform; }
8. Skip offscreen rendering with content-visibility: auto
It lets the browser skip layout and paint for offscreen subtrees until they scroll near the viewport — web.dev measured up to a ~7x initial-render improvement on long pages, and it became Baseline (across all engines) in late 2025. Always pair it with contain-intrinsic-size so skipped elements reserve height and don’t collapse to zero, which would wreck the scrollbar and cause layout shift.
.section {
content-visibility: auto;
contain-intrinsic-size: auto 600px; /* reserve estimated height */
}
9. Apply CSS containment (contain)
contain tells the browser a subtree is independent, so a change inside it can’t invalidate layout or paint for the rest of the page — a big win in dynamic UIs like lists, cards and widgets. contain: content is the safe default; strict/size makes the element lay out as if empty, so you must give it a size or it collapses.
.card { contain: content; } /* layout + paint + style */
.widget { contain: strict; } /* adds size containment (needs a known size) */
10. Reduce selector complexity
Deeply nested, universal or over-qualified selectors increase style-recalculation cost, which matters on large DOMs and during frequent DOM mutations. Flat, class-based selectors are cheapest to match. This matters most for very large pages and heavy runtime DOM churn — and watch that :has() and broad * selectors can be costly at scale.
/* ❌ expensive, matched right-to-left across the whole tree */
body div.container ul li a span { color: red; }
/* ✅ single class */
.nav-label { color: red; }
11. Avoid expensive paint during scroll and animation
Large box-shadow, filter: blur() and big border-radius are paint-heavy; recomputing them every frame while scrolling or animating drops frames. A common fix is to pre-render the shadow on a pseudo-element and animate only its opacity, which is composited.
/* ❌ repaints a large shadow every hover frame */
.card:hover { box-shadow: 0 40px 80px rgba(0,0,0,.4); transition: box-shadow .3s; }
/* ✅ animate a pre-rendered shadow's opacity (composited) */
.card::after { box-shadow: 0 40px 80px rgba(0,0,0,.4); opacity: 0; transition: opacity .3s; }
.card:hover::after { opacity: 1; }
12. Optimize web fonts
Web fonts are a top cause of invisible text (FOIT) and layout shift. Ship WOFF2 (≈97% support), subset to the characters you use (which can cut 80–95% of the weight), preload the one hero font to remove it from the discovery chain, and use font-display: swap to show fallback text immediately. Preload only 1–2 above-the-fold fonts so you don’t contend with the LCP image.
<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin.woff2") format("woff2");
font-display: swap;
unicode-range: U+0000-00FF; /* subset to Latin */
}
13. Use system fonts where a brand font isn't required
The fastest font is the one already on the device: zero bytes, zero requests, zero FOIT. A system font stack is ideal for body and UI text on performance-sensitive pages. Rendering varies per OS, so verify metrics across platforms if precise layout matters.
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
14. Cache with long-lived, content-hashed files
Hashed filenames let you serve CSS with immutable, year-long caching, so repeat visits pay no CSS cost and a content change produces a new URL — no stale-cache risk. The catch: the HTML referencing the hashed file must not be immutable-cached, or clients keep pointing at the old hash. Cache HTML short, assets forever.
# built asset: main.9f2a1c.css
Cache-Control: public, max-age=31536000, immutable
CSS optimization checklist
- Remove unused CSS; inline critical styles and defer/async-load the rest.
- Avoid runtime
@import; split non-essential CSS bymedia. - Animate only
transform/opacity; usewill-changesparingly. - Skip offscreen work with
content-visibility: auto(+contain-intrinsic-size) andcontain. - Keep selectors flat; avoid expensive paint (shadows/blur) during scroll.
- Ship WOFF2, subset + preload fonts,
font-display: swap; use system fonts where possible. - Serve content-hashed CSS with immutable caching.
Frequently asked questions
What is the biggest CSS performance win?
transform/opacity. Together these usually move Core Web Vitals the most.Which CSS properties are cheap to animate?
transform and opacity — the compositor can animate them without layout or paint, so they stay at 60fps. Animating width, height, top/left, or colors and shadows triggers layout or paint every frame and causes jank.Does content-visibility improve performance?
content-visibility: auto lets the browser skip layout and paint for offscreen sections until they near the viewport — web.dev measured large initial-render gains. Always pair it with contain-intrinsic-size so skipped sections reserve height and don’t cause layout shift.Should I inline critical CSS?
Test CSS changes with an instant preview in XCODX Studio — tweak animations, containment and critical styles and see the effect live. See also our CSS best practices and how CSS affects Core Web Vitals.