Split Critical CSS dans Tailwind v3/v4 sur Laravel + Vite
Lorsqu'une page semble lente, les utilisateurs fixent un écran vide tandis que le CSS bloque le rendu. Solution pragmatique : extraire le critical CSS pour la vue au-dessus du pli et différer le reste. Ce guide montre une configuration propre à deux entrées dans Laravel + Vite, avec des exemples prêts à la production pour Tailwind v3 et v4, pourquoi PageSpeed/Core Web Vitals est important, et ce qu'il faut éviter.
Pourquoi PageSpeed est important (LCP/CLS)
Le CSS critique est l'ensemble minimal de styles nécessaires au rendu du premier écran (au-dessus du pli). En livrant cette partie tôt (en ligne ou sous forme de fichier minuscule) et en reportant le reste du CSS, vous réduisez le travail de blocage du rendu et améliorez le LCP. Une configuration soignée des polices de caractères peut également réduire les changements inattendus de mise en page et favoriser le CLS. Pour une perspective plus large sur l'équilibre entre vitesse et stabilité, consultez La vitesse sans les retombées.
Stratégie de base (Laravel + Vite)
- Deux points d'entrée :
critical.csspour le premier écran ;app.csspour tout le reste (plugins, utilitaires étendus, typographie longue). - Ordre de chargement : livrez les éléments critiques sur
<head>(en ligne ou lien normal). Différez le reste avecrel="preload" as="style"+onloadet un fallback<noscript>. - Évitez les doublons : gardez la base/les composants dans critical et déplacez le gros des utilitaires dans le bundle différé ; assurez-vous que les fichiers sources analysés par chaque build ne se chevauchent pas.
Tailwind v3 : deux configurations + un preset
1) Partagez votre thème une fois
// tailwind.shared.js
module.exports = {
theme: {
extend: {
// colors, spacing, etc.
},
},
plugins: [],
};
2) Construction critique (analyse uniquement les modèles au-dessus du pli)
// 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 (exclure les fichiers utilisés par 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;
Si vous avez des noms de classes dynamiques, ajoutez-les à safelist dans les deux configurations.
4) Construire et se connecter 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 : sources CSS-first avec contrôle fin
Dans Tailwind v4, la configuration est CSS-first. Au lieu d'importer tout Tailwind d'un coup, importez uniquement les modules dont chaque bundle a besoin — cela évite de dupliquer le reset (preflight) dans les deux fichiers. Déclarez les sources explicitement avec @source, utilisez des noms de layers séparés pour verrouiller l'ordre de cascade entre les bundles, et enregistrez les plugins avec @plugin.
1) Jetons partagés
/* resources/css/theme.css */
@theme {
/* brand tokens, breakpoints, etc. */
--color-brand-500: oklch(0.72 0.12 250);
--breakpoint-2xl: 96rem;
}
2) CSS critique : ne scannez que ce qui est nécessaire
/* resources/css/critical.css */
@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";
/* Declare ALL layers upfront — including "deferred" from the app bundle.
This locks cascade order regardless of async load timing. */
@layer theme, base, deferred, utilities;
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
3) App CSS : tout le reste (exclure ce que critical couvre déjà)
/* resources/css/app.css */
@import "./theme.css";
/* Scan Blade templates and JS (Alpine components, dynamic classes) */
@source "../views/**/*.blade.php";
@source "../js/**/*.js";
/* ...except templates already handled by critical */
@source not "../views/layouts/app.blade.php";
@source not "../views/partials/header.blade.php";
@source not "../views/home/hero.blade.php";
/* Use a dedicated layer so deferred utilities never override critical ones */
@layer deferred;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(deferred);
/* v4 plugin syntax (replaces the plugins array in config) */
@plugin "@tailwindcss/typography";
/* Inline safelist if you generate classes dynamically */
@source inline("md:grid lg:gap-6 bg-brand-500");
4) Configuration du site et chargement de Blade
// 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>
Ce qui a sa place dans critical (et ce qui ne l'a pas)
- Inclure : les réinitialisations de base, la structure de l'en-tête/nav, la section hero, les boutons/liens essentiels, la typographie minimale du premier écran.
- À éviter : composants lourds sous le pli, typographie longue
.prose, widgets rares.
Polices de caractères et mini-recette CLS
Les changements de mise en page se produisent souvent lorsque la police de secours est remplacée par la police web. Utilisez size-adjust et les métriques connexes pour mieux correspondre à la police de repli, et ne préchargez que les polices vraiment essentielles.
@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%;
}
Vérifiez le résultat
- Exécutez PageSpeed/Lighthouse et comparez LCP ; cochez les cases « Réduire les ressources bloquant le rendu » et « Réduire les CSS inutilisées ».
- Vérifiez qu'il n'y a pas de doublons entre les bundles ; si vous constatez un surpoids, revoyez les sources scannées.
- N'oubliez pas : les résultats de lab sont indicatifs ; surveillez la tendance générale et les données de terrain. Pour en savoir plus sur l'accélération d'un site Laravel au-delà du CSS, ce guide couvre le caching, la base de données et les pipelines d'assets.
Pièges courants
- Chevauchement des sources → doublons : dans la version 3, corrigez vos globs
content; dans la version 4, utilisez des@sourceet@source notexplicites. - Même nom de layer dans les deux bundles : si critical et app déclarent tous deux
@layer utilities, l'ordre de cascade dépend du fichier chargé en dernier. Donnez au bundle app son propre layer (ex.deferred) et pré-déclarez-le dans la liste@layerde critical pour verrouiller l'ordre. - Plugins/typographie dans critical : cela gonfle facilement le fichier — gardez-les dans
app.css. En v4, utilisez@pluginpour les enregistrer. - Inlining critical vs CSP : si votre CSP est strict, envoyez critical comme un minuscule fichier externe.
- migration v3 → v4 : déplacez les tokens de
theme.extendvers@theme, passez decontentà@source, remplacez@tailwind basepar@import "tailwindcss/preflight.css", et préférez les importations CSS-first.
Alternatives si la double entrée n'est pas votre truc
- Critical auto-généré (par exemple, critical, critters) : plus rapide à démarrer ; plus difficile à déboguer sur des modèles complexes.
- Une seule configuration + plusieurs sources : conservez une configuration (v3) ou une configuration CSS-first (v4) et gérez l'inclusion/exclusion via globs/
@source. - CSS au niveau de la route : les bundles par page ne sont chargés que là où c'est nécessaire ; puissant mais ajoute de la complexité à l'infrastructure.
- Critical par composant : styles minuscules en ligne pour les composants clés au-dessus du pli ; discipline requise, fonctionne bien pour les blocs répétés.
Résumé
Avec un petit bundle critique dans <head> et un bundle principal différé, vous donnez au navigateur moins de choses à bloquer et aux utilisateurs une première peinture plus rapide. Tailwind v3 favorise deux configurations et des chemins d'accès prudents à content ; Tailwind v4 rend cela encore plus propre avec le contrôle CSS-first de @source. Sur Laravel + Vite, il s'agit d'une configuration simple et facile à maintenir qui améliore sensiblement la vitesse perçue.
Quelle taille doit avoir le bundle CSS critique ?
Moins de 14 Ko compressé est un bon objectif — cela tient dans le premier aller-retour TCP (environ 14 Ko après TLS). Sur ce site, le CSS critique en ligne pèse environ 6 Ko gzippé.
Cette approche fonctionne-t-elle avec Tailwind JIT ?
Oui. Le JIT ne génère que les classes réellement utilisées dans les fichiers scannés, ce qui s'accorde naturellement avec l'analyse de templates spécifiques par bundle. Dans Tailwind v4, le JIT est le seul mode — il n'y a plus de moteur classique.
Faut-il inliner le CSS critique ou utiliser une balise link ?
L'inlining est le plus rapide — zéro requête supplémentaire. Mais cela entre en conflit avec les politiques CSP strictes qui bloquent les styles en ligne. Un petit fichier externe avec rel="preload" est le compromis pratique quand le CSP pose problème.
Comment vérifier qu'il n'y a pas de duplication entre les bundles ?
Ouvrez DevTools → onglet Coverage, chargez la page et vérifiez combien de CSS dans chaque fichier reste inutilisé. Si les deux bundles contiennent les mêmes règles, vous verrez des pourcentages d'inutilisation élevés dans l'un d'eux. Comparez aussi les tailles brutes avant et après le découpage.
Contactez-nous
Un projeten tête?
Indiquez le contexte et l'objectif visé. Nous répondons sous 1 jour ouvrable avec la prochaine étape la plus simple (planning, budget indicatif ou audit rapide).