Blog

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.

XCODX Team · 7 min read

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 by media.
  • Animate only transform/opacity; use will-change sparingly.
  • Skip offscreen work with content-visibility: auto (+ contain-intrinsic-size) and contain.
  • 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?
Fixing delivery: remove unused CSS, inline the critical above-the-fold styles, and defer or asynchronously load the rest so CSS stops blocking the first paint. After that, keep animations to GPU-composited 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?
Yes, on long pages. 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?
For above-the-fold styles, yes — inlining them removes a render-blocking round-trip so the page paints sooner. Keep the inlined block small (roughly under 14 KB), automate the extraction, and load the rest of the CSS asynchronously.

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.