Aller au contenu principal

Le FOUC n'est pas un bug graphique, c'est une faille d'architecture !

Marre du contenu qui saute au chargement ? Découvrez comment éradiquer le FOUC (Flash of Unstyled Content), améliorer votre score CLS et optimiser l'UX de vos applications web modernes.
Catégorie

UX

Une application techniquement parfaite ne sert à rien si l'utilisateur s'y perd. Relevez le défi de l'interface intuitive et transformez vos lignes de code en une expérience fluide.

Lecture
4 min
Niveau
Intermédiaire
janv 12 2026
Partager

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 :

  1. Confiance brisée : Le cerveau reptilien perçoit l'instabilité comme un danger ("Ce site est cassé/lent").
  2. 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.

  1. Request : GET /.
  2. Parsing : Le navigateur reçoit le HTML et construit le DOM.
  3. Blocking : Il tombe sur <link rel="stylesheet">. Il arrête le rendu. Il attend.
  4. 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 :

  1. Peindre la page en Blanc.
  2. Exécuter le JS qui détecte prefers-color-scheme: dark.
  3. Ajouter la classe .dark.
  4. 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>.

HTML
<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 :

JavaScript
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 :

CSS
/* 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 :

Twig
{{ 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.).

CSS
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.

0 réaction

Poursuivre la lecture

Sélectionné avec soin pour vous.

L'injection de dépendance, ou comment être fainéant avec élégance

Découvrez comment Symfony vous aide à coder sans effort avec l'injection de dépendance. Dit adieu aux 'new' et bonjour à l'autowiring!

Composer : le manifeste du développeur moderne

Découvrez comment composer.json est l'équivalent développeur d'un buffet à volonté, avec humour piquant et ironie sur les dépendances PHP.

Votre terminal est bête : voici comment le rendre intelligent

Historique contextuel, autocomplétion Docker, switch PHP automatique... Découvrez comment transformer une fenêtre noire en véritable assistant de code.