Le FOUC (Flash of Unstyled Content) n'est pas un "petit bug visuel". C'est la preuve qu'une architecture frontend ne maîtrise pas le Critical Rendering Path (CRP) du navigateur.
Quand un utilisateur voit une page blanche clignoter, puis du texte noir en Times New Roman, puis soudainement le design final, deux choses se produisent :
- Confiance brisée : Le cerveau reptilien perçoit l'instabilité comme un danger ("Ce site est cassé/lent").
- Pénalité Google : Si le layout bouge pendant ce processus, c'est du CLS (Cumulative Layout Shift). Google dégrade le ranking.
Ce guide détaille comment éradiquer le FOUC, en descendant au niveau du moteur de rendu.
1. La mécanique du désastre
Pour comprendre le FOUC, il faut visualiser ce que fait le navigateur (Chrome/Safari) à la milliseconde près.
- Request : GET
/. - Parsing : Le navigateur reçoit le HTML et construit le DOM.
- Blocking : Il tombe sur
<link rel="stylesheet">. Il arrête le rendu. Il attend. - Paint : Une fois le CSS reçu, il construit le CSSOM (CSS Object Model), le fusionne avec le DOM (Render Tree), et peint les pixels.
Le dilemme du "dark mode"
Le pire cas de FOUC moderne est le thème sombre. Si le CSS par défaut est clair, et que l'utilisateur a une préférence système "Dark", le navigateur va :
- Peindre la page en Blanc.
- Exécuter le JS qui détecte
prefers-color-scheme: dark. - Ajouter la classe
.dark. - Repeindre la page en Noir.
Résultat : Un flash blanc aveuglant. C'est inacceptable.
2. L'injection de script synchrone
La seule façon de battre la latence réseau, c'est d'exécuter la logique de thème de manière synchrone, avant même que le <body> n'existe.
Voici le code réel du fichier templates/partials/_theme_init.html.twig, injecté directement dans le <head>.
<script nonce="{{ csp_nonce('script') }}">
(() => {
// 1. Single source of truth : Le cookie (pour le SSR/PHP)
const getThemeCookie = () => {
const value = `; ${document.cookie}`;
const parts = value.split(`; theme=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
};
const savedTheme = getThemeCookie();
// 2. Fallback système immédiat
const shouldBeDark = () => {
if (savedTheme === "dark") return true;
if (savedTheme === "light") return false;
return window.matchMedia?.("(prefers-color-scheme: dark)").matches || false;
};
const isDark = shouldBeDark();
// 3. Application INSTANTANÉE (avant le body paint)
// Le navigateur est bloqué ici (c'est volontaire).
if (isDark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
})();
</script>
Pourquoi ça marche ?
Ce <script> est placé dans le <head>.
Le navigateur bloque le parsing HTML tant que ce script n'est pas exécuté.
Comme le script est inline (pas de requête HTTP), cela prend ~1ms.
Quand le navigateur reprend le parsing et arrive au <body>, la classe .dark est DÉJÀ présente sur <html>.
Le premier affichage est donc correct : zéro flash.
3. Stabilisation du layout (.loaded)
Pour les éléments complexes qui dépendent de JavaScript (comme les Stimulus, ou SyntaxHighlighter), la stratégie utilisée est celle de la révélation Contrôlée.
Dans _theme_init.html.twig, ce listener est actif :
window.addEventListener('load', () => {
// requestAnimationFrame garantit que le code s'exécute
// juste avant le prochain rafraîchissement d'écran
requestAnimationFrame(() => {
document.documentElement.classList.add('loaded');
});
});
Et dans le CSS, cette classe interdit les transitions au chargement :
/* app.css */
html:not(.loaded) * {
transition: none !important; /* Interdit les animations au démarrage */
}
html:not(.loaded) .complex-component {
opacity: 0; /* Masque les composants non initialisés */
}
Cela empêche les éléments de glisser ou de changer de taille pendant l'initialisation de la page (ce qui causerait du CLS).
4. Fonts et AssetMapper
L'utilisation d'AssetMapper permet de gérer les assets efficacement.
Dans base.html.twig, cet attribut est crucial :
{{ importmap('app', { fetchpriority: 'high' }) }}
fetchpriority="high" signale au navigateur que le fichier app.js (et ses imports CSS) est critique. Il le place en haut de la file d'attente réseau, avant les images.
Concernant les polices, le choix radical pour la performance est : pas de web fonts. La "System Font Stack" est utilisée (San Francisco sur Mac, Segoe UI sur Windows, etc.).
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto...
Avantages :
- 0ms de latence : La police est déjà sur le device de l'utilisateur.
- FOUT impossible : Pas de texte invisible ou de saut de police.
- Respect de l'OS : L'interface semble native.
Le mot de la fin
Un site rapide n'est pas juste un site qui répond vite. C'est un site qui s'affiche de manière stable.
Le FOUC est un symptôme de négligence.
En insérant la logique critique de manière synchrone et en contrôlant l'état .loaded, une expérience solide comme le roc est garantie, même sur des connexions lentes.
C'est la différence entre un template HTML et une application professionnelle.