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.

Split Critical CSS in Tailwind v3/v4 on Laravel + Vite

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)

  1. Two entry points:critical.css for the first screen; app.css for everything else (plugins, extended utilities, long-form typography).
  2. Load order: deliver critical in <head> (inline or a normal link). Defer the rest with rel="preload" as="style" + onload and a <noscript> fallback.
  3. 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, use source(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 from content 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).

By submitting, you agree that we’ll process your data to respond to your enquiry and, if applicable, to take pre-contract steps at your request (GDPR Art. 6(1)(b)) or for our legitimate interests (Art. 6(1)(f)). Please avoid sharing special-category data. See our Privacy Policy.
We reply within 1 business day.