Split Critical CSS in Tailwind v3/v4 on Laravel + Vite
When a page feels slow, users stare at a blank screen while CSS blocks rendering. A pragmatic fix is to ship just enough styles for the above-the-fold view and defer the rest. This guide shows a clean two-entry setup in Laravel + Vite, with production-ready examples for Tailwind v3 and v4, why PageSpeed/Core Web Vitals care, and what to avoid.

Why PageSpeed cares (LCP/CLS)
Critical CSS is the minimal set of styles needed to render the first screen (above-the-fold). By delivering this slice early (inline or as a tiny file) and deferring the remaining CSS, you reduce render-blocking work and improve LCP. Careful font setup can also reduce unexpected layout shifts and help CLS.
Baseline strategy (Laravel + Vite)
- Two entry points:
critical.css
for the first screen;app.css
for everything else (plugins, extended utilities, long-form typography). - Load order: deliver critical in
<head>
(inline or a normal link). Defer the rest withrel="preload" as="style"
+onload
and a<noscript>
fallback. - Avoid duplicates: keep base/components in critical and move the bulk of utilities to the delayed bundle; ensure the source files scanned by each build don’t overlap.
Tailwind v3: two configs + one preset
1) Share your theme once
// tailwind.shared.js
module.exports = {
theme: {
extend: {
// colors, spacing, etc.
},
},
plugins: [],
};
2) Critical build (scan only above-the-fold templates)
// tailwind.critical.config.js
module.exports = {
presets: [require('./tailwind.shared')],
content: [
'./resources/views/layouts/app.blade.php',
'./resources/views/partials/header.blade.php',
'./resources/views/home/hero.blade.php',
],
};
/* resources/css/critical.css */
@config "tailwind.critical.config.js";
@tailwind base;
@tailwind components;
@tailwind utilities;
3) App bundle (exclude files used by critical)
// tailwind.app.config.js
module.exports = {
presets: [require('./tailwind.shared')],
content: [
'./resources/**/*.blade.php',
'./resources/**/*.vue',
'!./resources/views/partials/header.blade.php',
'!./resources/views/home/hero.blade.php',
],
};
/* resources/css/app.css */
/* Avoid duplicating base/components (already shipped in critical) */
@config "tailwind.app.config.js";
@tailwind utilities;
If you have dynamic class names, add them to safelist
in both configs.
4) Build and connect via Vite (Laravel)
// vite.config.ts
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/critical.css',
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
],
})
<!-- Blade: load critical normally (or inline it) -->
@vite('resources/css/critical.css')
<!-- Defer the rest: preload + onload + noscript -->
@php $appCss = Vite::asset('resources/css/app.css'); @endphp
<link rel="preload" as="style" href="{{ $appCss }}" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ $appCss }}"></noscript>
Tailwind v4: CSS-first sources with fine-grained control
In Tailwind v4, configuration is CSS-first. To avoid overlaps between critical and app bundles, disable auto-scanning and declare sources explicitly.
1) Shared tokens
/* resources/css/theme.css */
@theme {
/* brand tokens, breakpoints, etc. */
--color-brand-500: oklch(0.72 0.12 250);
--breakpoint-2xl: 96rem;
}
2) Critical CSS: scan only what’s needed
/* resources/css/critical.css */
@import "tailwindcss" source(none);
@import "./theme.css";
/* Enumerate just the above-the-fold templates */
@source "../views/layouts/app.blade.php";
@source "../views/partials/header.blade.php";
@source "../views/home/hero.blade.php";
/* Import required layers explicitly */
@layer theme, base, components, utilities;
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/base" layer(base);
@import "tailwindcss/components" layer(components);
@import "tailwindcss/utilities" layer(utilities);
3) App CSS: everything else (exclude what critical already covers)
/* resources/css/app.css */
@import "tailwindcss" source(none);
@import "./theme.css";
/* Scan the project... */
@source "../views/**/*.blade.php";
/* ...except templates you already used for critical */
@source not "../views/partials/header.blade.php";
@source not "../views/home/hero.blade.php";
/* App bundle: keep it lean for post-fold content */
@layer theme, utilities;
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
/* Inline safelist if you generate classes dynamically */
@source inline("md:grid lg:gap-6 bg-brand-500");
4) Vite setup and Blade loading
// vite.config.ts (same as v3)
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/critical.css',
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
],
})
<!-- Blade: same pattern as v3 -->
@vite('resources/css/critical.css')
@php $appCss = Vite::asset('resources/css/app.css'); @endphp
<link rel="preload" as="style" href="{{ $appCss }}" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ $appCss }}"></noscript>
What belongs in critical (and what doesn’t)
- Include: base resets, header/nav structure, hero section, essential buttons/links, minimal first-screen typography.
- Avoid: heavy components below the fold, long-form
.prose
typography, rare widgets.
Fonts & CLS mini-recipe
Layout shifts often happen when the fallback font is replaced by the web font. Use size-adjust
and related metric overrides to better match the fallback, and preload only truly critical faces.
@font-face {
font-family: "InterVar";
src: url("/fonts/InterVar.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
/* Tweak metrics to reduce shifts */
size-adjust: 100%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
Verify the result
- Run PageSpeed/Lighthouse and compare LCP; check “Reduce render-blocking resources” and “Reduce unused CSS”.
- Sanity check: make sure there’s no duplication between bundles; if you see extra weight, revisit the scanned sources.
- Remember: lab scores are indicative; watch the overall trend and field data.
Common pitfalls
- Overlapping sources → duplicates: in v3, fix your
content
globs; in v4, usesource(none)
with explicit@source
and@source not
. - Plugins/typography in critical: this easily bloats the file—keep it in
app.css
. - Inlining critical vs CSP: if your CSP is strict, ship critical as a tiny external file.
- v3 → v4 migration: move tokens from
theme.extend
into@theme
, switch fromcontent
to@source
, and prefer CSS-first imports.
Alternatives if two-entry isn’t your thing
- Auto-generated critical (e.g., critical, critters): fastest to start; harder to debug on complex templates.
- Single config + multiple sources: keep one config (v3) or CSS-first setup (v4) and manage inclusion/exclusion via globs/
@source
. - Route-level CSS: per-page bundles loaded only where needed; powerful but adds infra complexity.
- Component-scoped critical: inline tiny styles for key above-the-fold components; discipline required, works well for repeated blocks.
Summary
With a small critical bundle in <head>
and a deferred main bundle, you give the browser less to block on and users a faster first paint. Tailwind v3 favors two configs and careful content
paths; Tailwind v4 makes this even cleaner with CSS-first @source
control. On Laravel + Vite, it’s a straightforward, maintainable setup that measurably improves perceived speed.
Get in touch
Have a project
in mind?
Tell us the context and the outcome you want. We’ll reply within 1 business day with the simplest next step (timeline, rough budget, or quick audit).