@import in CSS: Why It's Slow and What to Use Instead
The @import directive in CSS is a common cause of render delay. We break down how it works, why it hurts performance, and what alternatives exist.
The CSS directive @import url('...'); lets one CSS file load another. It's convenient for modular style organization: a main file pulls in themes, typography, media styles. Convenient for the developer, but almost always harmful for performance.
In this article — how @import works, why it slows down rendering, and what to use instead.
How the Browser Handles @import
The standard flow:
- The browser receives the HTML, parses it, and finds
<link rel="stylesheet" href="main.css"> - Downloads
main.css - Parses
main.css, finds@import url('theme.css')inside - Only now requests
theme.css - Downloads
theme.css - Parses it; if it also contains
@import— repeats the cycle - Finally builds the CSSOM, begins rendering
The core problem is that steps 2–5 are sequential. The browser doesn't know it needs theme.css until it has downloaded and parsed main.css. This is a blocking chain.
With <link> in HTML, the story is different — the browser sees all tags at once when parsing HTML and begins downloading all CSS files in parallel. That's a significant difference in final time-to-render.
What This Costs You
A concrete example. Say your main.css is 50 KB and theme.css is 30 KB. On a fast connection, each downloads in 200 ms.
Via <link>:
- The browser starts both requests simultaneously
- After 200 ms, both are ready
- Total time to first render: ~250 ms
Via @import in main.css:
- Browser downloads main.css — 200 ms
- Parses it, discovers @import
- Downloads theme.css — another 200 ms
- Total time to render: ~450 ms
That's nearly twice as long. On a slow connection (3G mobile) — the gap is 1–2 seconds. This is a direct hit to LCP and Core Web Vitals.
When It's Especially Painful
Deep chains
If main.css imports theme.css, theme.css imports typography.css, and typography.css imports variables.css — the browser processes them sequentially. At 4 levels deep, the delay can reach 1–2 seconds even on a fast connection.
Conditional imports
@import url('mobile.css') (max-width: 768px);
This looks logical — don't download desktop styles on mobile. But the browser downloads all CSS files anyway (it doesn't know the condition in advance), parses them, and only then decides whether to apply them. You save on execution, but not on loading.
Imports from CDN
@import url('https://cdn.jsdelivr.net/npm/normalize.css');
On top of the extra request, you add a DNS lookup and TCP handshake to a new domain. On a cold connection, that's +300–500 ms.
Alternatives
1. Multiple <link> tags in HTML
Old school, but it works:
<link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/theme.css">
The browser starts downloading all three in parallel. Parses each as it arrives. The combined CSSOM is built sooner.
2. Bundler merges CSS
Webpack, Vite, esbuild, Parcel — all of them can concatenate CSS files into a single file at build time. One HTTP request instead of several.
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
cssCodeSplit: false, // single CSS instead of splitting
},
});
Downside: changing a single line of CSS invalidates the entire file's cache. But styles rarely change, and not every change goes to production.
3. CSS Modules or CSS-in-JS
Modern React stacks often use CSS Modules (Next.js by default) or CSS-in-JS (styled-components, emotion). Styles are connected automatically at the component level, without @import. Each component gets its own scope; the bundler merges everything into the final bundle.
4. HTTP/2 push (deprecated, but was relevant)
In the HTTP/2 era, there was a push mechanism — the server could send a CSS file to the client before it was requested. This solved the @import chain problem. Since 2022, Chrome has dropped push (it caused more problems than it solved). Don't use it.
5. Inline critical CSS
The most radical and effective solution. Above-the-fold styles are inlined directly into HTML via <style>, and the rest are loaded asynchronously. More on this in the post about render-blocking resources.
When @import Is Actually Fine
A few cases where @import is justified:
- Fonts from Google Fonts — the Google Fonts API itself connects via
<link>, and internally uses@importto load specific weights. This is part of the documented API; you don't control it. - Conditional imports by media query — for print styles, for example
@import 'print.css' print;. Print styles are not needed during screen rendering, and the browser understands this (loads them at low priority). - Dynamic themes switched by JavaScript. In that case, the "extra" @import isn't critical — it fires only when the theme is switched, not on the initial page load.
In all other cases — avoid it.
How to Check Your Site
Open Chrome DevTools → Network → CSS filter. Load the page.
In the Initiator column, see who triggered the request:
- If a CSS file was initiated by "(index)" or HTML — it's a
<link>(good) - If it was initiated by another CSS file — it's
@import(bad)
If you see a chain of CSS files initiating each other — you have @import. It's worth fixing.
Summary
@import in CSS is a holdover from the era of modular CSS before bundlers existed. In 2026, with a modern build pipeline (Webpack/Vite/etc), there's always an alternative. Replacing @import with parallel <link> tags or a concatenated bundle typically shaves 200–1000 ms off the first paint. A simple fix with a predictable result.
Want to check your CSS for chains and render-blocking issues? Contact us. Performance audit in 2 business days.