
⚡ Comment charger du JavaScript externe sans bloquer la page
Lorsque le navigateur rencontre une balise <script> sans attribut, il abandonne tout. Le rendu de la page s’arrête complètement jusqu’à ce que le script soit chargé et exécuté. Sur une connexion 4G lente, cela représente 2 à 3 secondes d’écran blanc.
L’utilisateur est déjà parti chez un concurrent. Les Core Web Vitals enregistrent un LCP en échec, Google relègue la page dans les résultats de recherche et vous perdez du trafic et des conversions. Pourtant, le problème peut se régler en trois lignes si vous savez où chercher.
Voici une méthode éprouvée pour charger du JavaScript externe sans blocage. De l’approche classique à deux fichiers jusqu’aux attributs modernes async/defer et à l’import() dynamique. Avec du code validé que vous pouvez copier-coller.
💡 Aperçu rapide:
- Comprendre le problème: comment une balise
<script>classique bloque l’analyse HTML et tue la vitesse de chargement - Maîtriser l’approche classique: un micro-chargeur (≤300 octets) qui injecte dynamiquement le fichier JS principal
- Apprendre les attributs natifs
asyncetdefer: quand et lequel utiliser - Explorer l’
import()dynamique pour charger des modules à la demande - Choisir une stratégie pour votre projet grâce à un tableau comparatif
Pourquoi JavaScript bloque le rendu
Quand l’analyseur HTML atteint <script src="app.js">, il fait exactement trois choses: il arrête l’analyse du document, télécharge le fichier, l’exécute. Ce n’est qu’ensuite qu’il reprend la construction du DOM.
La raison est architecturale. Un script peut contenir document.write(), qui modifie le HTML à la volée. Le navigateur ne sait pas à l’avance si un tel appel est présent, alors il joue la sécurité et attend le chargement complet et l’exécution. Résultat: même un script léger de 5 Ko ajoute des centaines de millisecondes au First Contentful Paint rien qu’à cause d’un seul aller-retour réseau.
Le problème n’est pas nouveau. En 2009, Nicholas Zakas décrivait déjà une technique de chargement dynamique non bloquant de JavaScript, et elle fonctionne encore aujourd’hui, moyennant quelques ajustements pour les API modernes. Avec l’arrivée de async, defer et des modules ES, les développeurs disposent désormais d’une boîte à outils complète. Examinons chacun de ces outils.
Approche classique: deux fichiers et chargement dynamique
L’idée est simple. Au lieu de mettre tout votre JS dans un seul fichier et de l’attacher à la page via <script src="...">, vous scindez le code en deux parties:
- Un micro-chargeur (200 à 300 octets après compression)
- Le fichier principal avec la logique applicative
Le chargeur est inséré en ligne en bas de page, juste avant </body>. Il crée une balise <script> de manière programmatique et l’ajoute au DOM; une telle balise ne bloque plus l’analyse car elle apparaît en dehors du flux principal du document. Dès que le fichier principal est chargé, l’initialisation s’exécute.
Version moderne de la fonction en JS pur, sans rétrocompatibilité IE:
1 function loadScript(url) { 2 return new Promise((resolve, reject) => { 3 const script = document.createElement('script'); 4 script.src = url; 5 script.onload = resolve; 6 script.onerror = reject; 7 document.head.appendChild(script); 8 }); 9 }
Neuf lignes. Pas de vérification readyState, pas de branches pour les vieux IE, pas de pyramide de l’enfer de callbacks. Juste une fonction qui retourne une Promise, pratique à combiner avec async/await.
L’utilisation dans la page ressemble à ceci (code en bas, avant la fermeture de </body>):
1 <script> 2 function loadScript(url) { 3 return new Promise((resolve, reject) => { 4 const script = document.createElement('script'); 5 script.src = url; 6 script.onload = resolve; 7 script.onerror = reject; 8 document.head.appendChild(script); 9 }); 10 } 11 12 loadScript('/js/app.js').then(() => { 13 // Initialize after main file loads 14 App.init(); 15 }); 16 </script>
Le premier script (en ligne) est le chargeur. Il est analysé et exécuté instantanément car il fait moins de 300 octets. Le second script (app.js) se charge de manière asynchrone et n’interfère pas avec le rendu.
Et si vous avez plus de deux fichiers? Combinez-les lors du build. Les bundlers modernes comme Vite et Webpack le font automatiquement: tree-shaking, découpage de code, minification en une seule passe. Gérer manuellement l’ordre de chargement d’une douzaine de fichiers, c’est s’exposer à des race conditions et des erreurs.
Async et defer: le déblocage natif
HTML5 nous a donné deux attributs qui résolvent le problème sans une seule ligne de JavaScript:
1 <script async src="analytics.js"></script> 2 <script defer src="app.js"></script>
Les deux chargent le fichier en parallèle de l’analyse HTML. La différence réside dans le moment de l’exécution:
Attribut | Chargement | Exécution | Ordre |
|---|---|---|---|
| Parallèle à l’analyse | Immédiatement après le chargement | Non garanti |
| Parallèle à l’analyse | Après l’analyse HTML complète | Garanti (comme dans le document) |
Règle générale:
asyncpour les scripts indépendants: analytics, publicités, compteurs. Ils n’ont pas besoin du DOM, ils ne se soucient pas de l’ordre.deferpour l’application principale: manipulation du DOM, initialisation de l’interface. Le script attend que la page soit prête et s’exécute dans le bon ordre.
En pratique, la combinaison est simple: placez defer sur tous les scripts dans le <head>, et ils se comportent comme s’ils étaient en bas de page tout en se chargeant plus tôt. Pas de magie, juste l’ordonnanceur du navigateur.
Et oui, vous pouvez combiner cela avec le chargement dynamique. Par exemple, chargez le cœur applicatif via <script defer>, et attachez les widgets lourds dynamiquement via loadScript() uniquement lorsqu’ils sont réellement nécessaires.
Import() dynamique: des modules à la demande
ES2020 a apporté l’import() dynamique, un moyen natif de charger un module de manière asynchrone, sans bundler et sans fonctions supplémentaires:
1 // Loads only when user clicked 2 button.addEventListener('click', async () => { 3 const { heavyChart } = await import('./chart-component.js'); 4 heavyChart.render(); 5 });
L’appel import() retourne une Promise. Le module se charge en arrière-plan, l’analyse n’est pas bloquée, la page reste réactive. Le code à l’intérieur du module s’exécute en mode strict et dans sa propre portée, les conflits de noms sont éliminés.
C’est l’outil idéal pour le découpage de code sans bundler. Les composants lourds (graphiques, éditeurs, cartes) sont déplacés dans des fichiers séparés et chargés à la première interaction. Un utilisateur qui n’ouvre jamais un graphique ne le paie pas en trafic et en temps de chargement.
Comparaison des approches
Chaque méthode a sa niche. Pour éviter les conjectures, nous avons compilé les caractéristiques dans un tableau:
Approche | Bloque le rendu | Nécessite JS | Ordre d’exécution | Pour quels scripts |
|---|---|---|---|---|
| Oui | Non | Garanti | À n’utiliser que si nécessaire |
| Non | Oui | Via chaîne | Chargement conditionnel, dépendances lourdes |
| Non | Non | Non garanti | Analytics, publicités, compteurs |
| Non | Non | Garanti | Application principale, manipulation du DOM |
| Non | Oui (module ES) | Via | Découpage de code, composants à la demande |
Principal enseignement: ne vous focalisez pas sur une seule méthode. Une configuration de production typique en utilise deux ou trois simultanément: defer pour le cœur, async pour les métriques, import() dynamique pour les composants lourds.
Courte vidéo de démonstration sur le sujet, décryptage de async et defer avec visualisation de la chronologie de chargement:
⁉️🤔 Foire aux questions
En quoi async diffère-t-il de defer en pratique?
Les deux ne bloquent pas l’analyse pendant le chargement. Mais
asyncexécute le script immédiatement après le chargement du fichier, même si le HTML n’est pas encore entièrement analysé, et sans garantie d’ordre.deferattend toujours que le DOM soit complètement prêt et préserve la séquence des scripts telle qu’elle figure dans le HTML. Pour le code applicatif principal, utilisezdefer; pour les compteurs isolés, utilisezasync.
Peut-on combiner le chargement dynamique avec defer?
Oui, c’est un scénario courant. Le cœur applicatif se charge avec
deferdans le<head>, il initialise l’interface. Les modules lourds ou rarement utilisés sont chargés vialoadScript()dynamique ouimport()lors d’une interaction utilisateur. Vous obtenez ainsi à la fois un démarrage rapide et un chargement différé du code secondaire.
Que dois-je utiliser pour un site WordPress?
WordPress ajoute automatiquement
deferouasyncviawp_enqueue_script()si vous passez l’argument approprié dans le cinquième paramètre:wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). Pour les scripts tiers (Google Analytics, publicités), l’approche la plus simple est l’attributasync. Les blocs interactifs complexes (calculateurs, filtres) doivent être déplacés vers unimport()dynamique à l’intérieur d’un module personnalisé.
Cela fonctionne-t-il avec les scripts tiers comme Google Analytics?
Oui. La balise GA4
gtag.jsse charge avecasyncpar défaut, elle ne bloque donc pas la page. Pour les autres services tiers, consultez la documentation: si le script n’a pas besoin d’un DOM prêt et ne dépend pas de l’ordre de chargement, utilisezasyncsans hésiter. S’il a besoin du DOM, utilisezdeferou un chargement dynamique avec un callback.
Comment vérifier qu’un script ne bloque vraiment pas la page?
Ouvrez Chrome DevTools → Performance → Enregistrer → rechargez la page. Sur la chronologie, cherchez les blocs jaunes «Scripting» avant le vert «First Contentful Paint». Si un script est chargé avec
deferouasync, son exécution se situera après le FCP. Lighthouse en mode «Performance» affichera la recommandation «Éliminer les ressources qui bloquent le rendu»; aucun script ne devrait figurer dans cette liste.
Vaut-il la peine de changer votre approche de chargement dès maintenant
Si vos scripts sont encore dans le <head> sans attribut, vous perdez des positions dans les moteurs de recherche et vous agacez vos utilisateurs. Ce n’est pas une hypothèse, Lighthouse et PageSpeed Insights affichent le problème en rouge dans les premières lignes du rapport.
Démarrage rapide: passez en revue les balises <script> de votre template, ajoutez defer pour le code principal et async pour les métriques. Cela prend cinq minutes, et le LCP peut s’améliorer de 300 à 500 ms. Ensuite, l’import() dynamique pour les composants lourds quand vous aurez le temps de refactoriser.
Laissez une seule méthode de chargement, <script defer> dans le <head>, et la page se chargera sans délais visibles. Testez-le sur votre projet dès aujourd’hui.



