« Éliminer les ressources qui bloquent le rendu » : l’alerte figure parmi les plus anciennes de PageSpeed Insights, et parmi les plus mal traitées. Les extensions de cache promettent de la régler d’un clic, en générant un « CSS critique » ou en supprimant le « CSS inutilisé ». L’alerte disparaît souvent ; la page, elle, ne s’affiche pas toujours mieux, et il arrive qu’elle se mette à sauter au moment où le reste du CSS arrive.
Le sujet mérite mieux qu’une case à cocher. Le CSS bloque l’affichage pour une raison précise, le CSS critique répond à une contrainte de réseau mesurable, et un générateur peut produire un CSS critique qui contient toutes les bonnes règles tout en peignant une autre page que la feuille complète. Ce guide reprend la question dans l’ordre : pourquoi le CSS bloque, quand un CSS critique se justifie, comment le générer et surtout comment le contrôler.
Il s’appuie sur trois interventions récentes de l’agence, présentées avec leurs conditions de mesure : une feuille si petite que la mettre en ligne n’apportait rien, des feuilles appliquées une à une qui faisaient grimper le CLS, et un générateur dont trois défauts n’apparaissaient qu’à certaines largeurs. Une question pour commencer : votre CSS critique peint-il la même page que votre feuille complète ?
Pourquoi le CSS bloque-t-il l’affichage d’une page ?
Parce que le navigateur refuse de peindre une page sans ses styles. Tant qu’une feuille applicable à l’écran n’est pas téléchargée et analysée, Chrome, Firefox et Safari ne peignent rien, pour éviter un affichage brut suivi d’un saut. Chaque feuille <link rel="stylesheet"> du <head> retarde donc le premier rendu, donc le FCP et souvent le LCP.
Ce qui bloque le rendu
Une feuille liée par <link rel="stylesheet"> dont l’attribut media correspond à l’écran bloque le premier rendu ; c’est le cas par défaut, sans attribut media. Une règle @import dans une feuille bloque aussi, avec un défaut de plus : le navigateur ne découvre la feuille importée qu’après avoir reçu et analysé la feuille parente, d’où un aller-retour réseau de plus sur le chemin critique, comme l’explique Harry Roberts dans son article de 2018 sur le CSS et la performance réseau.
Le CSS agit aussi sur les scripts. Un script synchrone placé après une feuille attend que cette feuille soit chargée avant de s’exécuter, puisqu’il peut lire des styles calculés : la feuille retarde alors tout ce qui suit le script dans le document. Enfin, l’attribut blocking="render", documenté par MDN sur <link>, <style> et <script>, permet de rendre bloquante une ressource qui ne l’est pas par défaut, ce qui sert rarement en dehors de cas très particuliers.
Ce qui ne bloque pas
Une feuille dont la media query ne correspond pas à l’écran (media="print", media="(min-width: 1600px)" sur un écran plus étroit) est téléchargée sans bloquer le rendu. C’est ce comportement qu’exploite la technique de chargement différé décrite plus loin. La contrepartie est une priorité de téléchargement très basse : une feuille hors écran ne passe pas avant les ressources utiles, ce qui est voulu tant qu’elle ne sert qu’à du contenu situé plus bas.
Le lire dans le navigateur et dans Lighthouse
Chromium expose le statut de chaque ressource dans l’API Resource Timing : PerformanceResourceTiming.renderBlockingStatus vaut blocking ou non-blocking. MDN classe la propriété en disponibilité limitée, absente de Firefox et de Safari au moment où nous écrivons, mais elle suffit pour un diagnostic depuis Chrome. Les extraits de console de la dernière partie s’en servent.
Côté Lighthouse, la version 13, publiée le 10 octobre 2025, a remplacé l’audit historique render-blocking-resources par l’insight render-blocking-insight, le même que celui du panneau Performance de Chrome. Sa documentation conseille de différer ce qui n’est pas nécessaire au premier affichage et de mettre en ligne les petites ressources critiques, en prévenant que le CSS critique est une technique avancée, capable d’améliorer les performances comme d’introduire des bugs. La suite de cet article montre lesquels.
Qu’est-ce que le CSS critique ?
Le CSS critique est le sous-ensemble de règles nécessaire au premier écran, mis en ligne dans un <style> du <head>. Le navigateur peint alors le haut de page dès la réception du HTML, sans attendre aucune feuille externe ; la feuille complète se charge ensuite sans bloquer. web.dev la décrit dans son article sur l’extraction du CSS critique.
Le budget des 14 Ko
Le CSS critique répond à une contrainte de réseau. Sur une connexion TCP neuve, le serveur ne peut pas envoyer tout le document d’un coup : il commence par une fenêtre de congestion initiale, puis attend les accusés de réception avant d’envoyer davantage. La RFC 6928, publiée en avril 2013, fixe cette fenêtre à min(10 × MSS, max(2 × MSS, 14 600)) octets, soit dix segments et au plus 14 600 octets au premier aller-retour avec la taille de segment courante d’Ethernet.
Ces 14 Ko se comptent en octets transmis, donc après compression gzip ou Brotli, et ils incluent tout ce qui part au premier aller-retour : le haut du HTML, le CSS en ligne, les en-têtes de réponse.
Le budget reste un ordre de grandeur : TLS ajoute ses propres échanges avant le premier octet de HTML, HTTP/3 sur QUIC applique une règle de démarrage comparable, et un serveur peut être configuré avec une autre fenêtre initiale. L’ordre de grandeur, lui, reste juste : ce qui tient dans 14 Ko transmis arrive en un aller-retour, chaque tranche suivante en coûte au moins un.
La fenêtre double ensuite à chaque aller-retour réussi : environ 14 Ko, puis 29 Ko, soit 44 Ko cumulés au terme du deuxième aller-retour. Sur un réseau mobile où un aller-retour coûte 100 à 150 ms, la différence entre un premier écran livré en un aller-retour et un premier écran qui en demande trois se mesure en centaines de millisecondes de FCP. Notre article sur HTTP/2, HTTP/3 et QUIC détaille ce que chaque version du protocole change à ces allers-retours.
Le revers : un CSS sans cache
Un CSS en ligne voyage avec chaque page HTML. Il n’est jamais mis en cache séparément, donc il se retélécharge à chaque navigation, là où une feuille externe servie avec un en-tête Cache-Control de longue durée ne coûte rien à partir de la deuxième page. Un CSS critique de 6 Ko transmis coûte 6 Ko par page vue, ce qui reste raisonnable ; un CSS « critique » de 60 Ko, souvent la feuille complète recopiée, annule le bénéfice et reproduit le coût du blocage sans l’avantage du cache, comme l’explique notre guide du cache web.
Faut-il toujours générer un CSS critique ?
Le CSS critique devient utile quand le CSS bloquant dépasse le premier aller-retour, environ 14 Ko transmis. En deçà, une feuille unique, compacte et servie tôt depuis le domaine du site arrive avec le HTML ; au-delà de 50 Ko transmis, il devient indispensable, selon la convention de l’agence exposée ci-dessous.
Des points de contrôle, et une convention
Le tableau ci-dessous résume les seuils que l’agence applique en audit. Le budget de 14 Ko découle de la RFC 6928 ; les seuils de 50 Ko et de 10 Ko sont des conventions de l’agence. Le premier correspond au point où le seul CSS bloquant déborde des deux premiers allers-retours d’une connexion neuve (44 Ko cumulés) : il signale qu’aucun tri n’a été fait. Le second correspond à environ deux tiers du premier aller-retour, taille à partir de laquelle une feuille découverte tard pèse sur le premier rendu.
| Situation | Attendu | À améliorer | Critique |
|---|---|---|---|
CSS critique en ligne dans le <head> | 14 Ko transmis au plus, HTML du haut de page compris | Au-delà de 14 Ko transmis | Plusieurs dizaines de Ko, feuille complète recopiée |
| Feuilles bloquantes, total transmis | 14 Ko au plus, ou aucune | Au-delà de 14 Ko | Au-delà de 50 Ko (convention agence) |
| Feuilles bloquantes sans aucun CSS en ligne | Acceptable si elles tiennent ensemble dans 14 Ko, depuis le domaine du site | Tenant dans 14 Ko mais découvertes tard | Au-delà de 14 Ko |
<style> dans le <body> pour le premier écran | Aucun | Chaque occurrence | Non concerné |
| Feuille bloquante de plus de 10 Ko découverte tard | Annoncée par preload as="style" | Non annoncée (convention agence : 10 Ko) | Non concerné |
Règle @import | Aucune | Non concerné | Chaque occurrence |
Le cas où le CSS critique ne sert à rien
Le 8 octobre 2026, l’agence a mis tout le CSS en ligne sur l’une de ses propres applications web. La feuille bloquante unique pesait 44,5 Ko décodés et 8,6 Ko transmis en Brotli, servie depuis le même domaine en HTTP/2 derrière Cloudflare : elle tenait donc déjà dans le premier aller-retour. PageSpeed Insights et GTmetrix estimaient pourtant à 110 à 133 ms le gain d’une mise en ligne, et l’alerte de ressource bloquante restait affichée.
Résultat : GTmetrix en profil mobile (serveur de Paris, 4G à 9 Mb/s descendants et 125 ms de latence) a mesuré un LCP de 1 050 ms sur un passage, contre 1 122 et 1 050 ms avec la feuille externe sur deux passages ; Lighthouse en local donnait un LCP mobile de 1,5 à 1,7 s, contre 1,5 à 1,6 s.
Aucun gain lisible, donc, et deux pertes : une feuille qui n’était plus en cache d’une page à l’autre, et un validateur W3C qui signalait 22 messages sur des propriétés récentes du CSS en ligne. La feuille externe a été rétablie.
La leçon tient en une phrase : l’estimation de gain d’un outil de laboratoire compte la feuille comme une requête de plus, sans savoir qu’elle arrive dans le même aller-retour que le HTML. Le budget transmis décide, l’alerte informe. L’ordre dans lequel traiter les chantiers de performance, CSS compris, est discuté dans notre article sur les priorités de la webperf.
Comment charger le reste du CSS sans bloquer ?
Deux motifs font référence : la feuille déclarée en media="print" puis basculée à son chargement, ou la feuille préchargée par rel="preload" as="style" puis transformée en feuille de style. Les deux demandent un repli <noscript> pour les navigateurs sans JavaScript, et un script de bascule compatible avec la politique de sécurité du site.
Le motif media="print", basculé par un script externe
Le principe : déclarer la feuille complète avec un media qui ne correspond pas à l’écran, ce qui la télécharge sans bloquer, puis lui rendre son media d’origine une fois chargée. La version la plus répandue écrit la bascule dans un attribut onload="this.media='all'". Or un gestionnaire d’événement en ligne est soumis à la directive script-src-attr (ou, à défaut, script-src) de la politique de sécurité de contenu : sous une CSP stricte, il est bloqué, et la feuille reste en print pour toujours.
Le même article de web.dev sur le report du CSS non critique recommande d’ailleurs, en production, une fonction de chargement externe compatible avec une CSP. La variante ci-dessous porte le media d’origine dans un attribut data-media et confie la bascule à un petit script externe ; elle a été vérifiée sous script-src 'self' dans Chromium 141, feuille appliquée et statut non-blocking.
<head>
<!-- 1. CSS critique : uniquement ce que peint le premier écran -->
<style>/* … règles du haut de page … */</style>
<!-- 2. Feuille complète téléchargée sans bloquer : media="print" ne correspond pas à l'écran -->
<link rel="stylesheet" href="/css/site.3f9a1c.css" media="print" data-media="all">
<!-- 3. Repli sans JavaScript : la feuille dans son media d'origine -->
<noscript><link rel="stylesheet" href="/css/site.3f9a1c.css"></noscript>
<!-- 4. Bascule par un script externe, autorisé par script-src 'self' -->
<script src="/js/css-differe.js" defer></script>
</head>
// /js/css-differe.js : rend à chaque feuille différée son media d'origine une fois chargée
document.querySelectorAll('link[rel="stylesheet"][data-media]').forEach((lien) => {
const appliquer = () => { lien.media = lien.dataset.media; };
if (lien.sheet) appliquer(); // déjà chargée avant l'exécution du script
else lien.addEventListener('load', appliquer, { once: true });
});
Une feuille dont le media ne correspond pas à l’écran part avec la priorité la plus basse et n’est pas repérée par l’analyseur de préchargement ; elle ne se télécharge qu’une fois atteinte par l’analyseur principal. Mesuré par l’agence le 2 octobre 2026 dans Chrome 154, sur un banc WordPress avec un script bloquant de 2 s placé avant elle : feuille demandée à 2,1 s en priorité minimale, contre 0,1 s en priorité maximale sans la réécriture. La technique est donc réservée au CSS du reste de la page ; celui du premier écran appartient au CSS critique.
La feuille liée en fin de <body>, une fausse bonne idée
Déplacer simplement le <link> en fin de <body> semble plus simple. Chrome classe bien cette feuille en non-blocking. Mais quand le HTML arrive vite et en entier avant le premier rendu, Chrome attend souvent la feuille pour peindre : le parseur l’a déjà atteinte, et Jake Archibald décrivait dès 2016, dans son billet sur les <link> dans le <body>, des navigateurs qui suspendent le rendu à la découverte d’une feuille. Le statut annoncé et le comportement observé divergent.
Mesure de l’agence du 2 octobre 2026 (Chromium 153, ordinateur 1 440 × 900, réseau bridé à 40 ms et 10 Mb/s, 20 pages, 620 chargements par version) : le premier rendu précédait l’arrivée de la feuille dans seulement 224 chargements sur 620 avec un HTML servi en clair, et 117 sur 620 une fois le HTML compressé, donc arrivé plus vite. Plus le HTML arrive vite, plus la feuille « non bloquante » bloque. Le contrôle à faire est celui de la dernière partie : comparer l’heure d’arrivée de la feuille et le FCP, plutôt que lire son statut.
Une feuille par page plutôt qu’une cascade en morceaux
Le deuxième piège vient du découpage. Un site qui lie séparément la bibliothèque de composants, la feuille du site et la feuille de la page, toutes chargées sans bloquer après un CSS critique, ne passe pas directement à l’état final : chaque feuille qui arrive relance la cascade, et la page traverse autant d’états intermédiaires que de feuilles. Une bibliothèque de base qui réinitialise des marges peut défaire, pendant quelques centaines de millisecondes, ce que le CSS critique avait fixé.
Mesure de l’agence du 2 octobre 2026, sur un site client de 31 pages (Chromium 153, 20 chargements par page et par profil) : CLS de 0,19 à 1,05 en profil ordinateur dans PageSpeed Insights.
En laboratoire, on comptait 422 chargements sur 620 au-dessus du seuil de 0,1 en ordinateur, avec deux ou trois états successifs du conteneur principal sur les pages les plus touchées. Après regroupement en une feuille par page, il restait 7 chargements au-dessus du seuil, tous dus au changement de police, puis zéro après calibrage de la police de repli ; en mobile, de 138 à zéro.
La feuille regroupée se construit à la compilation : feuilles concaténées dans l’ordre de la cascade, minifiées, nom de fichier à empreinte (site.3f9a1c.css), version précompressée, et en-tête Cache-Control: max-age=315360000, public, immutable puisque toute modification change le nom. Le coût accepté est la bibliothèque retéléchargée avec chaque regroupement ; le calibrage de la police de repli est détaillé dans notre article sur l’impact des polices d’écriture, et le CLS en général dans notre guide du CLS.
Les url() relatives dans un CSS en ligne
Dernier piège, silencieux : une URL relative dans une feuille se résout depuis l’URL de la feuille, et celle d’un <style> en ligne depuis l’URL de la page.
Une règle url('../fonts/police.woff2'), juste dans le fichier CSS, pointe ailleurs une fois recopiée dans le HTML : police ou image en 404, sans autre signal qu’un affichage en police de repli. Dans toute feuille susceptible d’être mise en ligne, écrire les chemins depuis la racine du site (/wp-content/themes/…). WordPress réécrit lui-même les chemins des feuilles qu’il met en ligne ; le risque porte sur les copies manuelles et les autres outils.
Votre site est-il aussi rapide que vos visiteurs l’espèrent ?
Que valent les générateurs de CSS critique ?
Les générateurs trouvent les bonnes règles, mais peuvent les restituer dans un ordre ou une forme qui change le rendu. Sur un site client contrôlé le 2 octobre 2026, le module critical 9.0.0 produisait 13 à 40 écarts de géométrie par largeur sur les deux premières pages testées : chaque génération se contrôle, quel que soit l’outil.
Le module critical et ses trois défauts
Le module npm critical, d’Addy Osmani, est la référence des générateurs : il charge la page dans Chromium à chaque largeur demandée, garde les règles dont un élément touche le premier écran, puis assemble le résultat. Sur le site contrôlé, à 1 440 px, le conteneur principal mesurait 1 232 px avec le seul CSS critique et 1 248 px avec la feuille complète, et le titre de l’accueil 438 px de large au lieu de 672 px. Trois causes distinctes expliquaient ces écarts.
Premier défaut, l’union des largeurs casse la cascade. Le moteur de rendu de la version 9 calcule le CSS critique de chaque largeur, de la plus petite à la plus grande, puis met les résultats bout à bout avant de dédoublonner. La suite obtenue n’est plus dans l’ordre de la feuille d’origine : la règle de base d’une largeur revient après la requête média d’une largeur précédente, et la bat à spécificité égale.
/* Sortie de la largeur 1 280 px */
@media (min-width: 1280px) { .container { padding-inline: 2rem } }
/* Sortie de la largeur 1 440 px, ajoutée à la suite :
la règle de base revient APRÈS la requête média et l'emporte à 1 280 px et au-delà */
.container { padding-inline: 1rem; max-width: 1248px; margin-inline: auto }
/* Ordre attendu, celui de la feuille d'origine :
.container { padding-inline: 1rem; … }
@media (min-width: 1280px) { .container { padding-inline: 2rem } } */
Reproduit par l’agence le 15 octobre 2026 sur un banc minimal (Chromium 141) : avec ce CSS critique, le contrôle de géométrie décrit plus bas relève 4 écarts à 1 280 et 1 440 px, aucun à 375 px. Le défaut n’apparaît qu’aux largeurs où la requête média s’applique, ce qui explique qu’un coup d’œil sur mobile ne le voie pas.
Deuxième défaut, les règles qui masquent sont oubliées. Une règle est gardée si un élément qu’elle désigne a une boîte qui coupe le premier écran. Or un élément masqué par la feuille complète (display: none) n’a pas de boîte : la règle qui le masque n’est jamais retenue, et le CSS critique seul l’affiche. Sur le site contrôlé, un message d’erreur vide sous le champ de recherche, avec 4 px de marge, décalait tout le bas du premier écran.
Troisième défaut, les raccourcis à var() se perdent. critical recopie chaque règle par sa sérialisation rule.style.cssText. Quand un raccourci contient var(), ses propriétés détaillées attendent la substitution de la variable et se sérialisent en chaîne vide si une déclaration suivante de la même règle en remplace une ; la spécification des variables CSS prévoit ce comportement, donc tout outil qui sérialise le CSSOM le subit.
/* Règle d'origine */
.carte { border: 1px solid var(--bord); border-left: 4px solid green }
/* Ce que rend rule.style.cssText (vérifié dans Chromium 141 le 15 octobre 2026) */
.carte { border-top-color: ; border-top-style: ; border-top-width: ; … border-left: 4px solid green; }
/* Recopiées dans un <style>, les déclarations vides sont invalides :
la bordure disparaît du CSS critique, et chaque carte perd 2 px de hauteur */
L’outil d’extraction écrit pour l’intervention corrige les trois : union faite dans l’ordre d’origine des règles, racine d’un élément masqué retenue quand son parent touche le premier écran, déclarations reprises du texte source quand la sérialisation en perd. Résultat sur ce site, le 2 octobre 2026 : 58 pages régénérées, zéro écart aux quatre largeurs, 57 CSS critiques sous 7 Ko compressés et un à 7,3 Ko.
Les extensions WordPress
Sur WordPress, la génération passe le plus souvent par une extension de cache ou d’optimisation. Trois approches dominent, documentées par leurs éditeurs. WP Rocket propose « Load CSS Asynchronously » (CSS critique et feuilles différées) et « Remove Unused CSS », qui fait générer le CSS utilisé de chaque URL par un service distant, l’insère dans un <style id="wpr-usedcss"> et retire les feuilles d’origine du HTML.
FlyingPress fait aussi générer le CSS utilisé par un service distant, l’insère dans un <style id="flying-press-css"> et charge les feuilles d’origine quand le navigateur est inactif ; sa documentation prévient que les styles des éléments injectés ou masqués au chargement (fenêtres modales, carrousels, menus au survol) peuvent manquer. Perfmatters génère le sien sur le serveur, un seul pour tous les articles et un seul pour toutes les catégories, et diffère par défaut les feuilles d’origine jusqu’à la première interaction.
Les pièges observés en intervention se ressemblent d’une extension à l’autre. Les classes ajoutées par JavaScript (menu mobile ouvert, modale, carrousel) n’existent pas au moment de la génération, et leurs règles manquent. Les blocs présents dans quelques articles seulement (tableau, citation, colonnes) manquent au CSS partagé si l’article visité en premier ne les contenait pas : bloc nu au premier rendu, décalage à la première interaction. Les exclusions de sélecteurs, enfin, valent souvent au nom exact : dans Perfmatters 2.6.8, .gp-modal ne garde pas .gp-modal--open.
Ces extensions ne se classent pas entre elles sur un critère unique, et nous n’en recommandons aucune par défaut : le bon choix dépend du thème, du constructeur de pages et de l’hébergement. Le choix du thème en amont, traité dans notre article sur le choix d’un thème WordPress, pèse souvent plus que celui de l’extension. Dans tous les cas, une extension activée sans contrôle de rendu revient à publier sans relire.
Le contrôle de géométrie
Le contrôle qui attrape ces défauts compare deux rendus de la même page, à chaque largeur de rupture : l’un avec le seul CSS en ligne, feuilles neutralisées ; l’autre avec la feuille complète. Les scripts sont retirés des deux, puisque le premier écran se peint avant leur exécution. Pour chaque élément qui touche le premier écran, position et taille doivent coïncider au pixel près ; tout écart sera un décalage de mise en page le jour où la feuille complète s’appliquera.
Trois conditions évitent les faux écarts, chacune rencontrée en intervention. La page est servie depuis le domaine du site, pour que les URL relatives et les polices se résolvent comme en production. Toutes les polices déclarées sont chargées dans les deux rendus, sans quoi le relevé compare une fois la police finale, une fois le repli. Les largeurs couvrent chaque palier du framework : 375, 768, 1 280 et 1 440 px sur le site contrôlé, la dernière franchissant le palier de 1 400 px de Bootstrap 5.
Le contrôle a besoin d’un témoin positif : le même contrôle, lancé sur un CSS critique volontairement amputé, doit échouer. Sans ce témoin, un contrôle qui ne compare rien (éléments mal appariés, page vide, mauvaise URL) passe aussi, et donne une fausse assurance. Sur le site contrôlé, le témoin amputé des règles du conteneur échouait sur 8 pages sur 8, de 3 à 95 écarts par largeur.
// controle-geometrie.mjs : compare le rendu « CSS en ligne seul » et le rendu « feuille complète »
// Usage : node controle-geometrie.mjs <url> [largeurs=375,768,1280,1440] (Playwright 1.56, Chromium)
import { chromium } from 'playwright';
const [url, larg = '375,768,1280,1440'] = process.argv.slice(2);
const largeurs = larg.split(',').map(Number);
const HAUTEUR = 900, TOLERANCE = 1; // premier écran contrôlé, tolérance en px
const html = await (await fetch(url)).text();
const complet = html.replace(/<script\b[\s\S]*?<\/script>/gi, ''); // sans scripts
// Même DOM dans les deux rendus : les <link> restent, seul leur rel est neutralisé
const critique = complet.replace(/<link\b[^>]*rel=["'](?:stylesheet|preload)["'][^>]*>/gi,
(l) => (/rel=["']stylesheet["']|as=["']style["']/i.test(l) ? l.replace(/rel=["']/i, (m) => m + 'x-neutralise-') : l));
const releve = () => [...document.body.querySelectorAll('*')].map((el, i) => {
const r = el.getBoundingClientRect();
return { i, nom: el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(' ')[0] : ''),
x: r.x, y: r.y, l: r.width, h: r.height };
});
const navigateur = await chromium.launch();
let ecarts = 0;
for (const largeur of largeurs) {
const rendus = [];
for (const variante of [critique, complet]) {
const page = await navigateur.newPage({ viewport: { width: largeur, height: HAUTEUR } });
// Page servie depuis l'URL du site (route interceptée) : URL relatives et polices comme en production
await page.route(url, (route) => route.fulfill({ contentType: 'text/html', body: variante }));
await page.goto(url, { waitUntil: 'load' });
await page.evaluate(async () => { // toutes les polices déclarées, puis les images
await Promise.all([...document.fonts].map((f) => f.load().catch(() => {})));
await document.fonts.ready;
await Promise.all([...document.images].map((img) => img.decode().catch(() => {})));
});
rendus.push(await page.evaluate(releve));
await page.close();
}
const [a, b] = rendus;
const touche = (e) => e.y < HAUTEUR && e.y + e.h > 0; // coupe le premier écran
const dedans = (e) => e.y >= 0 && e.y + e.h <= HAUTEUR; // tient entier dans le premier écran
for (const ea of a) {
const eb = b[ea.i];
if (!touche(ea) && !touche(eb)) continue;
const position = Math.abs(ea.x - eb.x) > TOLERANCE || Math.abs(ea.y - eb.y) > TOLERANCE;
const taille = dedans(ea) && dedans(eb) && (Math.abs(ea.l - eb.l) > TOLERANCE || Math.abs(ea.h - eb.h) > TOLERANCE);
if (position || taille) {
ecarts++;
console.log(`${largeur}px ${ea.nom} : critique ${[ea.x, ea.y, ea.l, ea.h].map(Math.round)} / complet ${[eb.x, eb.y, eb.l, eb.h].map(Math.round)}`);
}
}
}
await navigateur.close();
console.log(ecarts ? `${ecarts} écart(s)` : 'Aucun écart');
process.exit(ecarts ? 1 : 0); // témoin positif : relancer sur une page au CSS critique amputé, le code doit valoir 1
Deux précautions complètent le contrôle sur un site réel. Un gabarit alimenté par la base (article, fiche produit) partage un seul CSS critique entre toutes ses instances : contrôler au moins deux instances de plus, choisies aux extrêmes (titre le plus long et le plus court, avec et sans image). Et un écart isolé se rejoue trois fois avant d’être corrigé : sur un portrait contrôlé le 3 octobre 2026, 20 écarts relevés au premier passage ont disparu aux quatre passages suivants, simple variation du rendu de la police au moment du relevé.
Faut-il supprimer le CSS inutilisé ?
Seulement le CSS mort, celui qu’aucune page n’utilise dans aucun état. Le CSS non critique, utile plus bas dans la page, sur un autre gabarit ou après une interaction, se charge sans bloquer ; le CSS mort se supprime. Confondre les deux casse des pages que personne n’a testées, souvent des mois plus tard.
Lire la couverture, et ses limites
Le panneau Coverage des outils de développement de Chrome indique, pour chaque feuille, la part des octets utilisés pendant l’enregistrement. Le chiffre est trompeur par construction : il porte sur une page, dans un état, à une largeur. Une feuille utilisée à 30 % sur l’accueil en ordinateur peut l’être à 60 % sur une fiche produit en mobile avec le menu ouvert. La couverture désigne des pistes, elle ne prouve pas qu’une règle est morte.
Les risques d’une suppression
Trois familles de règles paraissent inutilisées sans l’être. Les classes ajoutées par JavaScript : menu mobile, modale, onglet actif, message d’erreur d’un formulaire. Les états d’interaction : :hover, :focus-visible, :checked, dont la suppression dégrade aussi l’accessibilité au clavier. Les gabarits rares : page 404, résultats de recherche vides, page de paiement. Une règle se juge sur l’ensemble des gabarits et des états, en testant ses sélecteurs sur chacun, jamais sur une capture de couverture.
Ce qu’en dit Lighthouse 13
L’audit « Reduce unused CSS » (unused-css-rules) ne figure ni parmi les audits remplacés par des insights, ni parmi les audits retirés de la version 13. Vérifié dans la configuration par défaut de Lighthouse 13.5.0, il existe toujours, rangé dans le groupe des diagnostics avec un poids nul dans le score. Il signale donc une piste sans peser sur la note, et mérite le même traitement que la couverture : un point de départ, à confirmer gabarit par gabarit.
Comment WordPress met-il lui-même du CSS en ligne ?
Depuis WordPress 5.8, la fonction wp_maybe_inline_styles() met en ligne les petites feuilles mises en file dont le cœur connaît le fichier, de la plus petite à la plus grande, jusqu’à la limite du filtre styles_inline_size_limit. Cette limite vaut 40 000 octets depuis WordPress 6.9, contre 20 000 auparavant.
Fonctionnement
Selon la documentation du cœur, seules les feuilles qui déclarent leur fichier par la donnée path (posée par wp_style_add_data()) sont candidates : les feuilles des blocs du cœur l’ont, celles des extensions et des polices rarement. Les candidates sont triées de la plus petite à la plus grande, puis mises en ligne tant que le total reste sous la limite. La fonction passe deux fois, sur wp_head puis sur wp_footer pour les feuilles tardives, et chaque passage dispose du budget entier.
Ce double passage change le calcul : une limite de 14 000 octets a donné, sur un article mesuré par l’agence, 20 Ko de CSS en ligne. Ces limites comptent des octets non compressés, et sur notre banc du 15 octobre 2026, 43,8 Ko de CSS en ligne pesaient 7,8 Ko une fois compressés. Abaisser la limite réduit ce volume, mais fait ressortir des feuilles que le cœur mettait en ligne.
Testé le 15 octobre 2026 sur WordPress 7.1.3 et GeneratePress 3.6.1, sur une page riche en blocs du cœur : sans réglage, 5 feuilles bloquantes ; avec une limite à 7 000 octets et la déclaration de path, 8 ; avec la déclaration seule, 3. La limite basse rejetait en <link> bloquant cinq feuilles de blocs (bouton, colonnes, tableau, recherche, derniers articles) de 2,3 à 4 Ko chacune.
Convention de l’agence, révisée après ce banc : garder la limite du cœur et déclarer le path des petites feuilles de polices et d’extensions qui ne le portent pas. Abaisser la limite se justifie seulement quand le HTML compressé dépasse le premier aller-retour, environ 14 Ko ; la ligne reste en commentaire dans l’extrait.
<?php
/**
* Petites feuilles mises en ligne par le cœur (wp_maybe_inline_styles).
* Testé le 15 octobre 2026 sur WordPress 7.1.3 et GeneratePress 3.6.1.
* Limite du cœur conservée (40 000 octets par passage depuis WordPress 6.9).
*/
// Option : abaisser la limite seulement si le HTML compressé dépasse le premier aller-retour.
// Deux passages (wp_head puis wp_footer), chacun avec le budget entier.
// add_filter( 'styles_inline_size_limit', fn() => 7000, 20 ); // priorité 20 : après une extension qui forcerait 0
// Déclarer le fichier des petites feuilles qui ne le font pas (polices, extensions).
add_action( 'wp_enqueue_scripts', 'awp_declarer_petites_feuilles', PHP_INT_MAX );
add_action( 'wp_footer', 'awp_declarer_petites_feuilles', 0 ); // avant le second passage (priorité 1)
function awp_declarer_petites_feuilles() {
$poignees = array( 'font-titres', 'mon-extension-public' ); // à adapter au site
$styles = wp_styles();
foreach ( $styles->queue as $poignee ) {
$style = $styles->registered[ $poignee ] ?? null;
if ( ! in_array( $poignee, $poignees, true ) || ! $style || ! is_string( $style->src ) || $styles->get_data( $poignee, 'path' ) ) {
continue;
}
$src = strtok( $style->src, '?' );
if ( 0 !== strpos( $src, content_url( '/' ) ) ) {
continue; // feuille servie hors de wp-content : pas de fichier local
}
$fichier = WP_CONTENT_DIR . substr( $src, strlen( content_url() ) );
if ( is_readable( $fichier ) ) {
wp_style_add_data( $poignee, 'path', $fichier );
}
}
}
Sur le même banc, la déclaration met bien en ligne les deux feuilles visées. Une feuille enfilée pendant wp_footer et munie d’un path passe aussi en ligne au second passage, et WordPress 7.1.3 imprime alors son <style> dans le <head> : le cœur remonte les styles tardifs.
Ce que mesure l’agence
Mesure de septembre 2026 sur WordPress 7.1.2, avec GeneratePress et GeneratePerf : limite à 7 000 octets et chemins déclarés, un article passe de 12 à 6 feuilles bloquantes, avec une performance Lighthouse mobile de 94 à 95 et un LCP de 2,9 à 2,8 s. Sur cette pile, la plupart des feuilles n’avaient pas de path : le gain venait de leur déclaration. Le gain reste modeste parce que les six feuilles restantes bloquent encore. La mise en ligne par le cœur réduit le nombre de requêtes, sans remplacer un vrai CSS critique.
Mesure d’octobre 2026 sur la même pile (GeneratePress 3.6.1, GeneratePerf 3.1.4), avec le CSS utilisé de Perfmatters 2.6.8 mis en ligne, les feuilles d’origine différées et les petites feuilles exclues mises en ligne : 7 à 10 Ko compressés de CSS dans le HTML, plus aucune feuille bloquante.
Le FCP simulé mobile passe de 1,65 à 1,01 s sur une page de texte, et la performance Lighthouse de 98 à 100. Sur un article dont l’élément LCP est une photo, de 1,7 à 1,1 s de FCP, pour un document passé de 26 à 39 Ko transférés. Notre article sur les Core Web Vitals sur WordPress replace ce réglage parmi les autres chantiers du CMS.
Comment vérifier que votre CSS ne bloque plus le rendu ?
Avec trois contrôles en console et un dans le panneau Performance de Chrome : lister les feuilles bloquantes et leur poids transmis, comparer l’arrivée de chaque feuille au FCP, estimer le poids compressé du CSS en ligne, et repérer les <style> du <body>. Les extraits ci-dessous ont été exécutés le 15 octobre 2026 dans Chromium 141 sur un banc local.
// 1. Feuilles bloquantes et poids transmis (transferSize vaut 0 pour une feuille en cache
// ou d'une autre origine sans Timing-Allow-Origin : la peser alors avec curl --compressed)
performance.getEntriesByType('resource')
.filter((r) => r.initiatorType === 'link' && r.renderBlockingStatus === 'blocking')
.map((r) => ({ url: r.name, transmis: r.transferSize, decode: r.decodedBodySize }));
// 2. Le premier rendu a-t-il attendu chaque feuille ? (peintAvant: false = la feuille est arrivée avant le FCP)
const fcp = performance.getEntriesByName('first-contentful-paint')[0]?.startTime;
performance.getEntriesByType('resource')
.filter((r) => r.initiatorType === 'link' && /\.css$/.test(new URL(r.name).pathname))
.map((r) => ({ feuille: new URL(r.name).pathname.split('/').pop(), arrivee: Math.round(r.responseEnd),
fcp: Math.round(fcp), peintAvant: fcp < r.responseEnd, statut: r.renderBlockingStatus }));
// 3. Poids compressé estimé du CSS en ligne du <head> (gzip ; Brotli compresse un peu mieux)
const css = [...document.querySelectorAll('head style')].map((s) => s.textContent).join('');
const gz = await new Response(new Blob([css]).stream().pipeThrough(new CompressionStream('gzip'))).arrayBuffer();
console.log(css.length, 'caractères en ligne,', gz.byteLength, 'octets compressés (gzip)');
// 4. Blocs <style> dans le <body> (hors styles internes d'un SVG)
[...document.querySelectorAll('body style')].filter((s) => !s.closest('svg') && s.textContent.trim());
Le deuxième extrait est le plus instructif : une feuille marquée non-blocking dont l’arrivée précède le FCP a pu retenir le premier rendu, comme la feuille en fin de <body> décrite plus haut. Il se lance sur plusieurs chargements et dans deux conditions de réseau, rapide et lent, puisque la proportion change avec la vitesse d’arrivée du HTML. Dans le panneau Performance, aucune requête CSS ne doit apparaître entre la réception du document et le premier rendu.
Pour un contrôle plus complet, le snippet « Critical CSS Detection » des webperf-snippets de Joan León mesure en une fois le CSS en ligne face au budget de 14 Ko, les feuilles bloquantes, les feuilles préchargées et celles ciblées par media. Les deux approches se complètent : le snippet pour l’état des lieux, le contrôle de géométrie pour la justesse du rendu.
Par où commencer ?
Par la mesure du CSS bloquant transmis : sous 14 Ko et découvert tôt, une feuille unique suffit ; au-delà, générer un CSS critique, charger le reste sans bloquer dans une feuille par page, puis contrôler la géométrie avec un témoin positif. L’ordre compte davantage que l’outil.
Les erreurs que nous rencontrons en audit suivent presque toujours l’ordre inverse : une extension activée pour faire taire une alerte, un CSS critique jamais comparé au rendu final, des feuilles différées qui arrivent une à une, et un CLS qui grimpe dans les données de terrain pendant que le score de laboratoire s’améliore. Le guide de l’optimisation du LCP montre comment le CSS bloquant s’inscrit dans les sous-parties du LCP, et notre guide du responsive rappelle pourquoi chaque palier de largeur doit être contrôlé.
Mesurer le CSS bloquant de chaque gabarit, décider où un CSS critique se justifie et le contrôler à chaque génération, c’est le travail d’une optimisation de performance ; repérer d’abord où se situent vos feuilles par rapport au budget est l’objet d’un audit de performance. Dans les deux cas, la question du début reste le critère : votre CSS critique peint-il la même page que votre feuille complète ?