L Art du Pré-chargement : Éliminer le "Flash" de Thème avec JavaScript et CSS
Publié le 15 November 2025
- Résumé
- 1. Le Problème : Ce Maudit Clignotement de Thème
- 2. Pourquoi un Script Classique ne Suffit Pas ?
- 3. La Solution : Le Chargement Précoce (Early Loading)
- 4. Étape 1 : Sauvegarder le Choix de l’Utilisateur
- 5. Étape 2 : Le Script de Pré-chargement dans le
<head> - 6. Étape 3 : La Puissance des Sélecteurs d’Attributs CSS
Résumé
Dans cet article technique approfondi, nous explorons comment implémenter un sélecteur de thème (light/dark) sans le désagréable effet de clignotement qui survient lors du chargement de page. Nous analysons en détail le cycle de rendu du navigateur, les causes profondes du FOUC (Flash of Unstyled Content), et proposons une solution robuste basée sur le pré-chargement synchrone dans le <head>. Cette technique garantit une expérience utilisateur fluide et professionnelle.
1. Le Problème : Ce Maudit Clignotement de Thème
1.1. Un Cauchemar pour l’Expérience Utilisateur
Imaginez la scène : vous avez passé des heures à concevoir un magnifique thème sombre pour votre application web. Les couleurs sont parfaitement équilibrées, le contraste est optimal, et vos utilisateurs adorent cette option. Mais il y a un problème embarrassant : à chaque rechargement de page, pendant une fraction de seconde, le thème clair par défaut s’affiche avant que le thème sombre ne prenne le relais.
Ce "flash" visuel, bien que bref (parfois moins de 100ms), est immédiatement perceptible par l’œil humain et crée une expérience désagréable. Pour les utilisateurs qui ont choisi le thème sombre pour des raisons de confort visuel ou d’accessibilité, ce clignotement peut même être douloureux, particulièrement dans un environnement peu éclairé.
1.2. Le FOUC : Un Vieux Problème Web
Ce phénomène est une variante de ce que l’on appelle le "Flash of Unstyled Content" (FOUC), un problème classique du développement web qui remonte aux premiers jours du CSS. Le FOUC se produit lorsque le navigateur affiche temporairement du contenu HTML sans ses styles CSS appliqués, créant un flash de contenu non stylisé.
Dans notre cas spécifique, nous ne parlons pas d’un contenu complètement non stylisé, mais plutôt d’un Flash of Wrong Theme (FOWT) - le contenu est stylisé, mais avec le mauvais thème. C’est particulièrement frustrant car cela montre que notre application "oublie" la préférence de l’utilisateur à chaque chargement de page.
1.3. Impact sur la Perception de Qualité
Ce problème, bien que technique, a des répercussions importantes sur la perception de la qualité de votre application :
Manque de polish : Le clignotement donne l’impression d’une application inachevée ou mal optimisée. Les utilisateurs associent souvent ces petits défauts visuels à un manque de professionnalisme général.
Rupture de cohérence : L’application semble "oublier" les préférences de l’utilisateur, créant une sensation de désynchronisation entre l’interface et les attentes.
Fatigue visuelle : Pour les utilisateurs sensibles à la lumière ou souffrant de migraines, ce flash lumineux peut être plus qu’un simple désagrément esthétique.
Performance perçue : Ironiquement, même si votre site se charge rapidement, ce clignotement peut donner l’impression d’une application lente ou peu réactive.
1.4. Analyse Technique du Problème
Pour comprendre comment résoudre ce problème, il faut d’abord comprendre pourquoi il se produit. Le clignotement survient à cause d’un décalage temporel entre trois événements critiques dans le cycle de vie d’une page web :
-
Le parsing initial du HTML : Le navigateur lit et analyse la structure de votre page
-
L’application des styles CSS : Le navigateur applique les règles CSS et calcule le rendu visuel
-
L’exécution du JavaScript : Votre code qui change le thème s’exécute
Le problème survient lorsque l’événement n°3 (exécution du JavaScript) arrive après que le navigateur a déjà commencé ou terminé l’événement n°2 (application des styles). À ce moment-là, le navigateur a déjà pris une décision sur quel thème afficher, et votre code arrive trop tard pour l’influencer avant le premier rendu.
2. Pourquoi un Script Classique ne Suffit Pas ?
2.1. L’Approche Intuitive mais Inefficace
L’approche la plus naturelle pour un développeur serait de placer un script à la fin de notre <body> qui vérifie le thème préféré de l’utilisateur et l’applique. Cette approche suit les meilleures pratiques traditionnelles du web qui recommandent de charger les scripts en fin de page pour ne pas bloquer le rendu.
// À la fin de <body> - L'APPROCHE INSUFFISANTE
document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('preferred-theme');
if (theme === 'dark') {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
});
Cette approche semble logique à première vue. Nous attendons que le DOM soit prêt, puis nous appliquons le thème. Simple, non ? Malheureusement, cette simplicité cache un défaut fondamental lié au timing du cycle de rendu du navigateur.
2.2. Comprendre l’Événement DOMContentLoaded
L’événement DOMContentLoaded se déclenche lorsque le document HTML initial a été complètement chargé et analysé par le navigateur, sans attendre la fin du chargement des feuilles de style, des images et des sous-cadres. C’est un point important à comprendre.
Voici la séquence typique des événements :
-
Le navigateur commence à télécharger le HTML
-
Il analyse le HTML au fur et à mesure de sa réception
-
Il découvre les balises
<link>pour les CSS et commence à les télécharger -
Il découvre les balises
<script>et les exécute (selon leur type et attributs) -
Il construit le DOM (Document Object Model)
-
L’événement
DOMContentLoadedse déclenche -
Il continue d’appliquer les styles et de faire le layout
-
Le premier paint (affichage) se produit
-
L’événement
loadse déclenche quand toutes les ressources sont chargées
Le problème ? Entre l’étape 6 (DOMContentLoaded) et l’étape 8 (premier paint), le navigateur a déjà pris des décisions sur comment afficher la page. Si votre script de changement de thème s’exécute à l’étape 6, il est déjà trop tard pour éviter un premier affichage avec les styles par défaut.
2.3. Le Problème du Render Blocking
En réalité, le timing est encore plus complexe. Les navigateurs modernes utilisent des techniques d’optimisation sophistiquées pour améliorer la performance perçue. Ils essaient de faire le premier paint (First Contentful Paint) le plus rapidement possible pour que l’utilisateur voie quelque chose à l’écran.
Les CSS sont "render-blocking" par défaut, ce qui signifie que le navigateur attend d’avoir téléchargé et parsé les feuilles de style avant de faire le premier paint. C’est logique : on ne veut pas afficher du contenu non stylisé.
Mais voici le piège : quand le navigateur applique ces styles CSS pour la première fois, il le fait en se basant sur l’état actuel du DOM. Si l’attribut data-bs-theme n’est pas encore défini sur la balise <html>, le navigateur appliquera les styles par défaut (généralement le thème clair).
Ensuite, quand votre script s’exécute et change cet attribut, le navigateur doit :
-
Recalculer tous les styles affectés par ce changement
-
Refaire le layout si nécessaire
-
Repeindre les éléments affectés
Ce processus de recalcul et de repeinture est ce qui cause le clignotement visible.
2.4. Visualisation du Problème
Pour mieux comprendre cette séquence problématique, examinons un diagramme de séquence détaillé :
Ce diagramme illustre clairement le problème : le premier paint se produit avant que notre script n’ait eu la chance de définir le bon thème. Le repaint subséquent crée le clignotement visible.
2.5. Les Tentatives de Solution Inefficaces
Plusieurs approches ont été tentées pour résoudre ce problème, mais la plupart ont leurs propres inconvénients :
Approche 1 : Cacher le contenu jusqu’au chargement
body {
opacity: 0;
transition: opacity 0.3s;
}
body.loaded {
opacity: 1;
}
Cette approche cache tout le contenu jusqu’à ce que le JavaScript ait défini le bon thème. Le problème ? Cela retarde artificiellement l’affichage du contenu, donnant l’impression d’un site plus lent. De plus, si JavaScript est désactivé, l’utilisateur ne voit rien du tout !
Approche 2 : Utiliser un loader/spinner
Similaire à l’approche 1, mais avec un spinner de chargement. Cela masque le problème mais n’améliore pas la performance réelle et ajoute un délai perçu inutile.
Approche 3 : Défaut au thème sombre
Certains développeurs définissent le thème sombre comme défaut dans le CSS. Cela évite le clignotement pour les utilisateurs du thème sombre, mais crée le problème inverse pour les utilisateurs du thème clair !
Aucune de ces approches n’est satisfaisante car elles traitent le symptôme plutôt que la cause racine du problème.
2.6. La Vraie Solution : Agir Plus Tôt
La clé pour résoudre ce problème est de réaliser que nous devons définir l’attribut data-bs-theme avant que le navigateur ne commence à appliquer les styles CSS. Cela signifie que notre script doit s’exécuter plus tôt dans le cycle de vie de la page, et c’est exactement ce que nous allons explorer dans la section suivante.
3. La Solution : Le Chargement Précoce (Early Loading)
3.1. Le Principe Fondamental
La solution élégante à notre problème de clignotement repose sur un principe simple mais puissant : synchroniser l’état de l’application avec le processus de rendu du navigateur. Au lieu d’attendre que la page soit chargée pour définir le thème, nous devons le définir pendant le chargement, avant même que les styles CSS ne soient appliqués.
Cette approche s’appelle le "Early Loading" ou "Synchronous Preloading" dans le jargon du développement web. L’idée est d’exécuter notre logique de détection de thème le plus tôt possible dans le cycle de vie de la page, idéalement dans la balise <head>, avant même que le navigateur ne commence à télécharger les fichiers CSS.
3.2. Pourquoi le <head> est l’Endroit Idéal
Le <head> d’un document HTML est traité séquentiellement par le navigateur, de haut en bas. Chaque élément est traité dans l’ordre où il apparaît. Cette caractéristique est cruciale pour notre solution.
Lorsque le navigateur rencontre une balise <script> dans le <head> sans les attributs async ou defer, il :
-
Interrompt le parsing du HTML
-
Télécharge le script (si externe) ou le lit (si inline)
-
Exécute immédiatement le script
-
Reprend le parsing du HTML
Ce comportement, souvent considéré comme un problème de performance (d’où la recommandation habituelle de placer les scripts en fin de page), devient notre allié dans ce cas précis. En plaçant notre script de détection de thème au début du <head>, nous garantissons qu’il s’exécute avant que le navigateur ne rencontre les balises <link> de nos feuilles de style.
3.3. Architecture de la Solution en Trois Couches
Notre solution complète se compose de trois couches interdépendantes, chacune jouant un rôle spécifique :
Couche 1 : Persistence (localStorage) Cette couche est responsable de la sauvegarde et de la récupération du choix de l’utilisateur entre les sessions.
Couche 2 : Synchronisation Précoce (Script inline dans <head>)
Cette couche synchronise l’état de l’application avec le DOM avant le rendu initial.
Couche 3 : Styles Réactifs (CSS avec sélecteurs d’attributs) Cette couche définit les styles visuels basés sur l’état défini par la couche 2.
Explorons maintenant chaque couche en détail.
4. Étape 1 : Sauvegarder le Choix de l’Utilisateur
4.1. Le localStorage : Votre Mémoire Persistante
Le localStorage est une API Web Storage qui permet de stocker des paires clé-valeur dans le navigateur de manière persistante. Contrairement aux cookies, les données du localStorage :
-
Ne sont jamais envoyées au serveur automatiquement
-
Ont une capacité de stockage plus importante (généralement 5-10MB)
-
N’ont pas de date d’expiration (persistent jusqu’à suppression explicite)
-
Sont limitées au protocole et au domaine (Same-Origin Policy)
Pour notre cas d’usage, le localStorage est parfait car :
-
Nous n’avons pas besoin de partager cette information avec le serveur
-
Nous voulons que la préférence persiste indéfiniment
-
La taille de stockage requise est minime (quelques octets)
4.2. Implémentation de la Sauvegarde du Thème
Voici comment nous sauvegardons le choix de l’utilisateur lorsqu’il change de thème :
// Fonction complète pour changer le thème
function setTheme(newTheme) {
// Validation de l'entrée
if (!['light', 'dark', 'auto'].includes(newTheme)) {
console.error('Thème invalide:', newTheme);
return;
}
try {
// Sauvegarde dans localStorage
localStorage.setItem('preferred-theme', newTheme);
// Application immédiate dans le DOM
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Dispatch d'un événement personnalisé pour notifier d'autres composants
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: { theme: newTheme }
}));
console.log('Thème changé:', newTheme);
} catch (error) {
console.error('Erreur lors de la sauvegarde du thème:', error);
// Fallback : on applique quand même le thème visuellement
document.documentElement.setAttribute('data-bs-theme', newTheme);
}
}
// Exemple d'utilisation avec un bouton
document.getElementById('theme-toggle').addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
4.3. Gestion des Cas d’Erreur
Il est crucial de gérer les cas où le localStorage n’est pas disponible ou accessible. Plusieurs scénarios peuvent empêcher l’accès au localStorage :
Navigation privée stricte : Safari en mode navigation privée lève une exception QuotaExceededError lors de tentatives d’écriture dans le localStorage.
Paramètres de confidentialité : Certains navigateurs ou extensions de confidentialité peuvent bloquer l’accès au localStorage.
Limitations de domaine : Le localStorage n’est pas accessible sur le protocole file:// dans certains navigateurs.
Espace de stockage saturé : Bien que rare, l’espace de stockage peut être complètement rempli.
C’est pourquoi notre code utilise un bloc try…catch pour gérer ces cas gracieusement, en continuant d’offrir la fonctionnalité de changement de thème même si la persistance n’est pas disponible.
4.4. Stratégies Avancées de Persistance
Pour des applications plus sophistiquées, vous pouvez envisager des stratégies supplémentaires :
Synchronisation serveur (optionnelle)
async function setTheme(newTheme) {
// Sauvegarde locale immédiate
localStorage.setItem('preferred-theme', newTheme);
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Synchronisation serveur en arrière-plan (si l'utilisateur est connecté)
if (userIsAuthenticated()) {
try {
await fetch('/api/user/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: newTheme })
});
} catch (error) {
console.warn('Échec de la synchronisation serveur:', error);
// L'échec n'est pas critique car la préférence est déjà sauvegardée localement
}
}
}
Cette approche permet de synchroniser les préférences entre appareils pour les utilisateurs connectés, tout en gardant la réactivité locale immédiate.
5. Étape 2 : Le Script de Pré-chargement dans le <head>
5.1. Le Cœur de la Solution
C’est ici que la magie opère véritablement. Nous allons placer un petit script inline directement dans notre <head>, avant toutes nos balises <link> de feuilles de style. Ce script est volontairement minimaliste, autonome, et conçu pour s’exécuter le plus rapidement possible.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Site Incroyable</title>
<!-- ==========================================
NOTRE SCRIPT MAGIQUE DE PRÉ-CHARGEMENT
Ce script DOIT être le premier élément
dans le <head> après les meta tags
========================================== -->
<script>
// IIFE pour ne pas polluer le scope global
(function() {
'use strict';
try {
// Lecture de la préférence sauvegardée
const savedTheme = localStorage.getItem('preferred-theme');
// Si une préférence existe, on l'applique immédiatement
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
// Optionnel : Détecter la préférence système si aucune sauvegarde
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
// Sinon, le thème par défaut du CSS sera utilisé (généralement 'light')
} catch (error) {
// En cas d'erreur (localStorage bloqué, etc.), on log discrètement
// et on laisse le thème par défaut s'appliquer
console.warn('Impossible de charger la préférence de thème:', error);
}
})();
</script>
<!-- FIN DU SCRIPT MAGIQUE -->
<!-- Les feuilles de style sont chargées APRÈS le script -->
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/styles.css">
<!-- Autres ressources du head -->
<link rel="icon" href="favicon.ico">
</head>
<body>
<!-- Contenu de la page -->
</body>
</html>
5.2. Anatomie du Script : Chaque Ligne Compte
Décortiquons ce script ligne par ligne pour comprendre chaque décision de design :
L’IIFE (Immediately Invoked Function Expression)
(function() {
// ...
})();
Cette structure crée une fonction qui s’exécute immédiatement. Pourquoi ? Pour isoler nos variables dans un scope local et éviter de polluer le scope global. Même si nous n’utilisons que const (qui a un scope de bloc), l’IIFE est une bonne pratique qui rend nos intentions claires et protège contre d’éventuels conflits de noms.
Le Mode Strict
'use strict';
Cette directive active le mode strict de JavaScript, qui : - Interdit l’utilisation de variables non déclarées - Génère des erreurs pour les opérations dangereuses - Améliore les performances dans certains moteurs JavaScript
Pour un script critique comme celui-ci, nous voulons la sécurité maximale.
Le Bloc try…catch
try {
// Code principal
} catch (error) {
console.warn('Impossible de charger la préférence de thème:', error);
}
Ce bloc est absolument crucial. Il garantit que si quelque chose se passe mal (localStorage bloqué, erreur de syntaxe improbable, etc.), notre script ne bloquera pas le chargement de la page entière. L’utilisation de console.warn plutôt que console.error indique que c’est un problème non-critique.
La Lecture du localStorage
const savedTheme = localStorage.getItem('preferred-theme');
Cette ligne peut lever une exception dans certains contextes (navigation privée stricte de Safari). C’est pourquoi elle est dans un bloc try…catch.
L’Application Conditionnelle
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
Nous appliquons le thème seulement si nous en avons trouvé un sauvegardé. Sinon, nous laissons le CSS utiliser son thème par défaut. Cette approche est plus robuste qu’une valeur par défaut côdée en dur dans le JavaScript.
5.3. Détection de la Préférence Système (Bonus)
Une amélioration optionnelle mais élégante consiste à détecter la préférence de thème du système d’exploitation de l’utilisateur s’il n’a pas encore fait de choix explicite dans votre application :
else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
Cette fonctionnalité utilise la Media Query prefers-color-scheme pour interroger le système. Sur macOS, Windows 10+, iOS, et Android moderne, cette requête retourne la préférence système de l’utilisateur.
Avantages : - Expérience personnalisée dès la première visite - Cohérence avec l’environnement système de l’utilisateur - Aucun stockage nécessaire pour la première visite
Considérations :
- Tous les navigateurs ne supportent pas cette fonctionnalité (mais le support est excellent depuis 2020)
- La vérification window.matchMedia assure la compatibilité
- L’utilisateur peut toujours surcharger ce choix
5.4. Performance : Pourquoi ce Script est Rapide
Notre script de pré-chargement est conçu pour être extrêmement rapide :
Taille minime : Environ 300 octets non-minifiés, 200 octets minifiés. C’est négligeable comparé à n’importe quelle image ou librairie JavaScript.
Inline : Pas de requête HTTP supplémentaire. Le script est dans le HTML, donc il est immédiatement disponible.
Opérations synchrones simples : Lecture d’une clé dans le localStorage (opération ultra-rapide) et modification d’un attribut DOM (opération native du navigateur).
Aucune dépendance : Pas de framework, pas de librairie, juste du JavaScript vanilla. Pas de temps de démarrage, pas de parsing de dépendances.
Exécution unique : Ce script s’exécute une seule fois au chargement. Pas de listeners d’événements, pas de boucles, pas de calculs complexes.
En pratique, sur du matériel moderne, ce script s’exécute en moins de 1 milliseconde, un temps imperceptible qui n’a aucun impact sur la performance de chargement de la page.
5.5. Placement Optimal dans le <head>
L’ordre des éléments dans le <head> est important. Voici l’ordre recommandé :
<head>
<!-- 1. Métadonnées critiques -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 2. Notre script de pré-chargement (IMMÉDIATEMENT après les meta) -->
<script>
(function() { /* notre code */ })();
</script>
<!-- 3. Titre de la page -->
<title>Mon Site</title>
<!-- 4. Feuilles de style -->
<link rel="stylesheet" href="styles.css">
<!-- 5. Autres ressources (fonts, favicons, etc.) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="icon" href="favicon.ico">
<!-- 6. Autres scripts avec defer ou async -->
<script src="app.js" defer></script>
</head>
Cette ordre garantit que : 1. Le charset est défini avant tout traitement de texte 2. Notre script s’exécute avant le chargement des CSS 3. Les CSS sont chargés ensuite et appliquent directement le bon thème 4. Les autres ressources non-critiques sont chargées en dernier
6. Étape 3 : La Puissance des Sélecteurs d’Attributs CSS
6.1. Le Système de Thème de Bootstrap 5
Bootstrap 5 a introduit un système élégant de gestion des thèmes basé sur les custom properties CSS (variables CSS) et les sélecteurs d’attributs. Ce système utilise l’attribut data-bs-theme sur l’élément <html> pour déterminer quel ensemble de variables de couleur appliquer.
La beauté de ce système réside dans sa simplicité : au lieu de charger différentes feuilles de style ou de toggle des classes sur des milliers d’éléments, nous changeons simplement un attribut sur un seul élément, et le CSS fait le reste grâce à la cascade.
6.2. Structure CSS pour un Système de Thème
Voici une structure CSS complète pour implémenter un système de thème robuste :
/**
* SYSTÈME DE THÈME COMPLET
* Utilise les Custom Properties CSS pour une maintenance facile
*/
/* ============================================
THÈME PAR DÉFAUT (LIGHT)
Défini sur :root pour être le fallback
============================================ */
:root {
/* Couleurs de base */
--color-primary: #0d6efd;
--color-secondary: #6c757d;
--color-success: #198754;
--color-danger: #dc3545;
--color-warning: #ffc107;
--color-info: #0dcaf0;
/* Couleurs de fond */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
/* Couleurs de texte */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-tertiary: #adb5bd;
/* Couleurs de bordure */
--border-color: #dee2e6;
--border-color-subtle: #e9ecef;
/* Couleurs d'ombre */
--shadow-sm: rgba(0, 0, 0, 0.075);
--shadow-md: rgba(0, 0, 0, 0.15);
--shadow-lg: rgba(0, 0, 0, 0.25