Mieux comprendre le chargement du Javascript dans une page web
Le cycle de chargement d’une page web est une étape fondamentale que tout développeur doit comprendre pour concevoir des applications performantes et réactives. Lorsque vous intégrez du JavaScript à vos projets, il est crucial de savoir quand et comment exécuter votre code pour éviter des erreurs ou des ralentissements inutiles. Pendant longtemps, des solutions comme $(document).ready
de jQuery ont facilité cette gestion, offrant une approche simple et robuste pour s’assurer que le DOM était prêt avant l’exécution des scripts.
Cependant, avec l’évolution des navigateurs modernes, des alternatives natives comme DOMContentLoaded
ou les modules ES6 permettent désormais d’aller plus loin sans dépendre de bibliothèques supplémentaires. Cet article se propose d’explorer les différentes méthodes pour gérer le chargement des contenus, de comparer leurs avantages et inconvénients, et de présenter des techniques avancées pour optimiser vos projets.
Le cycle de chargement d’une page web
Le cycle de chargement d’une page web ne se limite pas à afficher du contenu : il définit quand et comment vos scripts peuvent interagir avec les éléments HTML. Comprendre ces étapes est essentiel pour éviter des erreurs courantes, comme des tentatives de manipulation d’éléments inexistants ou non accessibles.
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
Cela est d’autant plus vrai dans des applications interactives où le contenu est souvent dynamique et dépendant d’appels réseau ou de modules chargés à la demande.
Pour manipuler une page web avec JavaScript, il est crucial de comprendre les grandes étapes de son cycle de vie :
- Chargement du HTML : Le navigateur commence à lire le document HTML et à construire le DOM (Document Object Model).
DOMContentLoaded
: Une fois le DOM complètement construit, l’événementDOMContentLoaded
est déclenché. À ce stade, les styles et les scripts externes peuvent encore être en cours de chargement.- Chargement des ressources : Toutes les ressources externes (images, vidéos, feuilles de style) sont chargées. Cet état est signalé par l’événement
window.onload
.
Ces étapes définissent différents moments où nos scripts peuvent s’exécuter, selon leurs besoins.
Placer tout le code dans document.ready
: une approche classique
L’utilisation de document.ready
était une approche naturelle pour encapsuler tout le code JavaScript et garantir une exécution ordonnée. Cela évitait des erreurs comme des éléments non accessibles au moment où le script s’exécutait. Cependant, cette méthode présente des limites dans des projets modernes.
Prenons un exemple d’erreur courante lorsque le code s’exécute trop tôt :
const element = document.getElementById('example');
element.textContent = 'Bonjour !'; // Erreur si l’élément #example n’existe pas encore.
L’encapsulation dans une fonction ready
résout ce problème :
$(document).ready(function() {
$('#example').text('Bonjour, DOM prêt !');
});
Cette approche garantit que tout ce qui est défini dans la fonction est exécuté uniquement lorsque le DOM est complètement chargé. Cela évite des erreurs comme Cannot read property of null
, qui se produisent lorsque le script tente de manipuler un élément HTML inexistant. Cependant, notez que cette méthode ne garantit pas que toutes les ressources externes (comme les images ou les feuilles de style) soient chargées. Pour cela, il faudrait utiliser window.onload
.
L’intérêt de cette méthode est clair : elle protège votre code et assure que tout est exécuté dans un contexte stable. Cependant, encapsuler tout le JavaScript dans une seule fonction ready
peut rapidement devenir une contrainte dans des projets complexes où l’organisation du code et la modularité sont essentielles. Si cette approche est robuste, elle devient moins pratique à mesure que le code JavaScript se complexifie. Tout regrouper dans une fonction unique limite la modularité et peut rendre le code difficile à maintenir.
Depuis les versions récentes de jQuery, il est courant d’utiliser cette syntaxe simplifiée, qui est équivalente à $(document).ready()
:
$(function() {
$('#example').text('Bonjour, DOM prêt !');
});
Comparer d’autres méthodes pour gérer le chargement
Historiquement, la gestion du chargement des contenus a donc toujours été un enjeu central en JavaScript. Si jQuery a longtemps dominé ce domaine avec sa méthode $(document).ready
, les API natives ont aujourd’hui évolué pour offrir des alternatives légères et performantes. Pour comprendre comment ces méthodes se comparent et dans quels cas les utiliser, explorons en détail trois options principales : DOMContentLoaded
, window.onload
, et document.readyState
.
DOMContentLoaded : Manipuler le DOM dès qu’il est prêt
L’événement DOMContentLoaded
est déclenché dès que le DOM est entièrement construit. Cela signifie que tous les éléments HTML sont accessibles pour les manipulations, mais que les ressources externes comme les images ou les feuilles de style peuvent encore être en cours de chargement.
document.addEventListener('DOMContentLoaded', function() {
const element = document.getElementById('example');
element.textContent = 'Le DOM est prêt à être manipulé !';
});
Première ligne : L’écouteur addEventListener
attend que l’événement DOMContentLoaded
soit déclenché. Ce moment garantit que le DOM est prêt, mais pas nécessairement les ressources externes.
Deuxième ligne : La méthode getElementById
sélectionne un élément HTML avec l’ID example
. À ce stade, l’élément existe et peut être manipulé.
Troisième ligne : La propriété textContent
est modifiée pour ajouter un texte au DOM.
DOMContentLoaded
est parfait pour des scripts légers qui dépendent uniquement du DOM, comme :
- Modifier des éléments HTML.
- Ajouter des écouteurs d’événements sur des boutons ou des liens.
- Initialiser une logique de base qui ne dépend pas des images ou des vidéos.
window.onload : Attendre que toutes les ressources soient prêtes
Contrairement à DOMContentLoaded
, l’événement window.onload
est déclenché uniquement lorsque tout le contenu de la page est chargé, y compris les images, les vidéos, et les styles CSS. Cette méthode est donc utile pour des scripts nécessitant l’accès à des dimensions ou des ressources externes.
window.onload = function() {
const img = document.querySelector('img');
console.log(`Image chargée : ${img.width} x ${img.height}`);
};
Dans cet exemple, le script mesure la largeur et la hauteur d’une image. Si l’image n’est pas encore entièrement chargée, ces dimensions pourraient être incorrectes ou nulles. L’utilisation de window.onload
garantit que toutes les ressources sont disponibles avant l’exécution.
window.onload
est recommandé pour des scénarios spécifiques :
- Calculer ou manipuler des dimensions d’images ou de vidéos.
- Initialiser des bibliothèques ou des carrousels qui dépendent des ressources multimédia.
- Effectuer des opérations nécessitant que tous les styles soient appliqués.
document.readyState : Vérifier manuellement l’état du DOM
Bien que DOMContentLoaded
et window.onload
soient suffisants pour la plupart des cas, il existe des scénarios plus complexes où il est nécessaire de vérifier l’état du DOM à différents moments. C’est là qu’intervient document.readyState
.
Avec document.readyState
, vous pouvez vérifier à tout moment si le DOM est en cours de chargement (loading
), prêt mais avec des ressources en cours de téléchargement (interactive
), ou entièrement chargé (complete
). Cela offre une flexibilité supplémentaire pour gérer des scénarios où le script est injecté dynamiquement.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM chargé dynamiquement.');
});
} else {
console.log('DOM déjà prêt.');
}
Si document.readyState
est encore à l’état loading
, un écouteur est ajouté pour attendre l’événement DOMContentLoaded
.Si le DOM est déjà prêt (interactive
ou complete
), le script s’exécute immédiatement sans ajouter d’écouteur inutile.
document.readyState
est utile dans des scénarios où le script peut être exécuté dans un environnement incertain, comme :
- Des scripts injectés dynamiquement.
- Des pages où le script peut être chargé à des moments différents (par exemple, via des CDN).
Comparaison des méthodes
Méthode | Moment d’exécution | Utilisation typique | Avantages | Inconvénients |
---|---|---|---|---|
DOMContentLoaded |
Dès que le DOM est prêt | Manipulation rapide du DOM | Rapide, natif, moderne | Ne gère pas les ressources externes |
window.onload |
Quand toutes les ressources sont chargées | Scripts nécessitant des images ou des médias | Pratique pour des besoins complets | Lent, inutile si seules des données DOM sont nécessaires |
jQuery.ready |
Dès que le DOM est prêt | Gestion simple de compatibilité DOM | API simple, rétrocompatibilité | Bibliothèque supplémentaire requise |
document.readyState |
Peut détecter plusieurs états | Vérifications manuelles des états DOM | Flexible, sans besoin d’écouteurs | Plus verbeux, rarement utilisé seul |
Note : Le choix de la méthode dépend du type de script et des besoins spécifiques du projet. |
Bien que chaque méthode ait son utilité, DOMContentLoaded
est généralement suffisante pour des besoins courants où seules les données du DOM sont nécessaires. Cependant, si vos scripts dépendent de ressources externes (comme des dimensions d’images), window.onload
reste une solution incontournable. Enfin, document.readyState
offre une flexibilité supplémentaire, bien qu’elle soit moins utilisée en pratique.
Pourquoi se passer de jQuery ?
Il est important de reconnaître l’impact qu’a eu jQuery sur le développement web : il a normalisé des comportements entre navigateurs, simplifié les manipulations du DOM, et introduit un modèle événementiel facile à comprendre. Mais aujourd’hui, les raisons de l’utiliser se réduisent considérablement.
Les navigateurs modernes offrent désormais des API puissantes et cohérentes, qui couvrent la majorité des cas d’usage pour lesquels jQuery était nécessaire. En évitant jQuery, nous pouvons :
- Réduire la taille des projets en éliminant une dépendance souvent lourde.
- Gagner en performance en utilisant des solutions natives optimisées.
- Faciliter la maintenance en restant aligné avec les standards actuels, largement adoptés par les frameworks modernes comme React, Vue ou Svelte.
Fonctionnalité | jQuery | JavaScript natif |
---|---|---|
Taille de la bibliothèque | ~80 Ko | Aucun poids supplémentaire |
Compatibilité avec les vieux navigateurs | Oui, rétro-compatibilité incluse | Limité aux navigateurs modernes |
Simplicité | API unifiée, rapide à apprendre | Plus verbeux pour certains usages |
Performance | Plus lent en raison de sa taille | Plus rapide, aucun coût supplémentaire |
Flexibilité | Très utile pour des projets complexes | Suffisant pour les besoins courants |
Note : Le choix entre jQuery et JavaScript natif dépend des besoins spécifiques du projet. |
Avec l’émergence des modules ES6 et des bundlers modernes comme Webpack ou Vite, ces méthodes peuvent être combinées à des approches modulaires et optimisées. Dans une prochaine section, nous explorerons comment les modules peuvent transformer la manière dont vous structurez et chargez vos scripts JavaScript.
Mais avant de plonger dans les modules et leurs possibilités, arrêtons-nous un instant sur des aspects fondamentaux : le positionnement des balises <script>
dans votre HTML et les techniques de chargement qui peuvent considérablement influencer les performances de vos pages.
Optimiser le chargement avec <head>
, <body>
, defer
et async
Le positionnement des balises <script>
dans une page HTML est un choix qui peut affecter les performances et l’expérience utilisateur. Deux emplacements sont courants : dans la section <head>
ou juste avant la fermeture de la balise </body>
. Chaque option présente des avantages et des inconvénients en fonction de l’objectif des scripts.
Placer le script dans <head>
Placer un script dans la section <head>
signifie qu’il sera téléchargé et exécuté avant la construction complète du DOM. Cela peut être utile pour des scripts critiques, comme des polyfills ou des bibliothèques nécessaires au rendu initial de la page. Cependant, cette approche bloque la construction du DOM jusqu’à ce que le script soit entièrement chargé et exécuté, ce qui peut ralentir l’affichage des contenus pour l’utilisateur.
<head>
<!-- Autres éléments d'entête -->
<script src="script.js"></script>
</head>
Placer le script avant </body>
?
En revanche, placer un script juste avant </body>
permet au navigateur de construire d’abord le DOM principal avant de s’occuper du téléchargement et de l’exécution des scripts. Cela accélère le rendu initial de la page et évite les erreurs où un script tenterait de manipuler des éléments HTML non encore chargés. Cette méthode est idéale pour des scripts interactifs ou manipulant le DOM.
<body>
<!-- Contenu de la page -->
<script src="script.js"></script>
</body>
Scripts différés avec defer
L’attribut defer
indique au navigateur de télécharger le script en parallèle à la construction du DOM, mais de différer son exécution jusqu’à ce que le DOM soit entièrement chargé. Cela garantit que les scripts n’interfèrent pas avec la construction du DOM, tout en préservant l’ordre d’exécution des scripts dans la page.
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
- Les scripts
script1.js
etscript2.js
sont téléchargés simultanément. - Ils ne s’exécutent qu’après la construction complète du DOM.
- L’ordre d’exécution est respecté :
script1.js
s’exécutera avantscript2.js
.
defer
est idéal pour les scripts dépendants du DOM, comme ceux qui manipulent des éléments HTML ou ajoutent des écouteurs d’événements.
Scripts asynchrones avec async
L’attribut async
permet également de télécharger le script en parallèle au DOM, mais son exécution est immédiate dès que le fichier est prêt, sans attendre la fin de la construction du DOM ni le chargement des autres scripts. Cela peut accélérer l’affichage des pages en réduisant les temps d’attente.
<script src="analytics.js" async></script>
<script src="ads.js" async></script>
- Les scripts
analytics.js
etads.js
sont téléchargés simultanément. - Ils s’exécutent dès qu’ils sont prêts, sans respecter d’ordre particulier.
async
est recommandé pour les scripts indépendants du DOM ou des autres scripts, comme les outils d’analyse (Google Analytics) ou les publicités.
Chargement standard sans defer
ni async
Par défaut, lorsqu’un script est inclus dans une page sans attribut defer
ou async
, son téléchargement et son exécution bloquent la construction du DOM jusqu’à ce qu’il soit entièrement exécuté.
<script src="script.js"></script>
- La construction du DOM s’arrête jusqu’à ce que le fichier
script.js
soit téléchargé et exécuté. - Si le fichier est volumineux ou provient d’un serveur distant lent, cela peut considérablement ralentir le rendu de la page.
En bref
L’optimisation du chargement des scripts repose sur une stratégie combinant leur placement et l’utilisation des attributs defer
et async
. En positionnant vos scripts soit dans <head>
avec ces attributs, soit avant </body>
, vous pouvez garantir un chargement efficace et fluide, adapté aux besoins spécifiques de chaque ressource. Associée à des pratiques modernes comme les modules ES6, cette approche optimise les performances globales et améliore l’expérience utilisateur.
Position ou attribut | Moment d’exécution | Impact sur le DOM | Cas d’utilisation typique |
---|---|---|---|
Sans attribut (dans <head> ) |
Immédiatement après le téléchargement | Bloque la construction du DOM | Scripts critiques mais non optimisés |
Sans attribut (avant </body> ) |
Après la construction du DOM principal | Ne bloque pas le DOM | Scripts interactifs simples |
defer |
Après la construction complète du DOM | Ne bloque pas le DOM | Scripts critiques mais respectant l’ordre |
async |
Dès que le script est prêt | Ne bloque pas le DOM | Scripts indépendants, comme les outils d’analyse |
L’attribut type
pour <script>
L’attribut type
de la balise <script>
détermine le type de contenu du script. Avec l’évolution des standards, son utilisation pour le JavaScript classique est devenue optionnelle, mais il reste essentiel dans certains cas spécifiques.
Pour le JavaScript standard, il n’est plus nécessaire de spécifier type="text/javascript"
. Par défaut, une balise <script>
est déjà interprétée comme du JavaScript. Par exemple, l’inclusion d’un fichier classique s’écrit simplement :
<script src="script.js"></script>
Si vous ajoutez explicitement type="text/javascript"
, cela fonctionne également, mais c’est désormais considéré comme redondant :
<script type="text/javascript" src="script.js"></script>
Pour les modules JavaScript (ES6), l’attribut type="module"
est requis. Cela permet d’utiliser des fonctionnalités modernes telles que import
et export
. Les modules JavaScript sont exécutés en mode strict par défaut et sont différés automatiquement (comme s’ils utilisaient defer
). Par exemple, pour importer une fonction depuis un fichier externe :
<script type="module">
import { myFunction } from './module.js';
myFunction();
</script>
En dehors du JavaScript, l’attribut type
peut être utilisé pour des formats spécifiques. Par exemple, si vous souhaitez inclure un objet JSON directement dans le HTML, vous pouvez le faire avec type="application/json"
:
<script type="application/json" id="config">
{ "setting": "value" }
</script>
De la même manière, pour des ressources comme WebAssembly, vous pouvez spécifier type="application/wasm"
:
<script type="application/wasm" src="script.wasm"></script>
En résumé, bien que l’attribut type
soit implicite pour le JavaScript standard, il reste important dans certains cas comme les modules ou les contenus non exécutables. Utilisez-le judicieusement pour garantir la clarté et la compatibilité de votre code avec les standards modernes.
Modules ES6 : Structurer son code de manière moderne
Les modules ES6 apportent une révolution dans la manière dont nous organisons le code. Contrairement à document.ready
, où tout est regroupé dans une fonction unique, les modules permettent d’isoler chaque fonctionnalité dans un fichier distinct.
Prenons un exemple simple : un fichier domUtils.js
contenant une fonction utilitaire :
export function updateTextContent(selector, text) {
const element = document.querySelector(selector);
if (element) {
element.textContent = text;
}
}
Les modules ES6 permettent de découper un projet en fichiers autonomes, où chaque fichier se concentre sur une fonctionnalité spécifique. Cela favorise la collaboration dans des équipes, car les développeurs peuvent travailler sur différentes parties du projet sans risque d’interférer avec les autres. De plus, ils encouragent une organisation claire et préparent le code pour une intégration avec des outils modernes. Ce module peut ensuite être importé dans un autre fichier, comme suit :
import { updateTextContent } from './domUtils.js';
document.addEventListener('DOMContentLoaded', () => {
updateTextContent('#example', 'Bonjour depuis un module ES6 !');
});
Cette approche favorise la réutilisabilité et la maintenabilité, surtout dans des projets complexes où les fonctionnalités sont nombreuses.
Techniques avancées pour le chargement des modules
Avec les modules ES6, nous pouvons aller encore plus loin pour optimiser nos projets et adapter les scripts aux besoins spécifiques.
Chargement conditionnel des modules
Les modules dynamiques permettent de charger du code uniquement lorsque cela est nécessaire. Cela améliore les performances en réduisant le temps de chargement initial de la page.
document.addEventListener('DOMContentLoaded', async () => {
if (document.body.classList.contains('dashboard')) {
const { initDashboard } = await import('./dashboard.js');
initDashboard();
} else {
const { initLandingPage } = await import('./landingPage.js');
initLandingPage();
}
});
Précharger des données dynamiques
Précharger des données dynamiques est une technique essentielle pour créer des interfaces modernes et interactives. En combinant l’événement DOMContentLoaded
avec des appels réseau, il devient possible de charger du contenu après la construction du DOM, sans attendre un rechargement complet de la page.
Prenons un exemple pratique : nous voulons afficher une liste d’utilisateurs obtenue depuis une API externe. L’objectif est de charger ces données dynamiquement dès que le DOM est prêt, en utilisant la méthode fetch
pour effectuer un appel réseau.
document.addEventListener('DOMContentLoaded', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
const userList = document.querySelector('#user-list');
users.forEach(user => {
const li = document.createElement('li');
li.textContent = `${user.name} (${user.email})`;
userList.appendChild(li);
});
});
Dans cet exemple :
- L’écouteur
DOMContentLoaded
garantit que le DOM est prêt avant d’ajouter du contenu. fetch
est utilisé pour effectuer une requête HTTP GET vers l’APIhttps://jsonplaceholder.typicode.com/users
.- La méthode
json()
convertit la réponse brute en un tableau d’objets utilisables en JavaScript. - Chaque utilisateur est ensuite ajouté dynamiquement à une liste HTML existante avec l’ID
user-list
.
Observer et manipuler le DOM dynamiquement
Dans le cadre de projets où le contenu est fréquemment modifié, où certaines ressources doivent être chargées en fonction des interactions utilisateur, ou encore où des tâches non prioritaires peuvent être différées, trois outils puissants peuvent être utiles : MutationObserver
, IntersectionObserver
, et requestIdleCallback
. Ces APIs offrent des solutions adaptées pour surveiller, différer, ou optimiser les performances liées au DOM et à l’exécution des tâches en arrière-plan.
MutationObserver
Le MutationObserver
permet de détecter et de réagir aux modifications structurelles ou visuelles dans le DOM, comme l’ajout ou la suppression d’éléments. Cela peut être utile pour charger dynamiquement des modules selon les changements observés.
const observer = new MutationObserver(async (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.target.id === 'dynamic-module') {
const { initModule } = await import('./dynamicModule.js');
initModule();
observer.disconnect(); // Arrêter l'observation après le chargement
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
Dans cet exemple, un module ES6 est chargé dès qu’un élément avec l’ID dynamic-module
est ajouté au DOM.
IntersectionObserver
L’IntersectionObserver
surveille la visibilité des éléments à l’écran et est particulièrement adapté au lazy loading des images ou au déclenchement d’animations. Il peut également être utilisé pour charger des modules en fonction de la visibilité des éléments.
const observer = new IntersectionObserver(async (entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { initAnimation } = await import('./animationModule.js');
initAnimation(entry.target);
observer.unobserve(entry.target); // Ne plus surveiller l’élément
}
}
});
const target = document.querySelector('#animation-trigger');
observer.observe(target);
Dans cet exemple, un module chargé à la demande déclenche une animation lorsqu’un élément spécifique devient visible à l’écran.
requestIdleCallback
L’API requestIdleCallback
permet de différer l’exécution de tâches non critiques jusqu’à ce que le navigateur soit inactif. Cela peut être particulièrement utile pour optimiser les performances en évitant d’interférer avec des tâches prioritaires comme le rendu ou les interactions utilisateur.
requestIdleCallback(() => {
console.log("Exécution d'une tâche non critique.");
fetch('/analytics')
.then(response => response.json())
.then(data => console.log("Données analytiques récupérées :", data));
});
Dans cet exemple, une requête pour récupérer des données analytiques est exécutée uniquement pendant une période d’inactivité du navigateur. Cela garantit que les tâches essentielles, comme le rendu des éléments visibles ou la gestion des événements utilisateur, ne sont pas perturbées.
Conclusion : Moderniser la gestion des scripts JavaScript
La gestion du chargement des scripts est une compétence essentielle pour créer des applications web modernes. En maîtrisant ces techniques, vous pourrez non seulement améliorer les performances de vos projets, mais aussi offrir une expérience utilisateur fluide et réactive. N’hésitez pas à expérimenter avec ces outils, à explorer les frameworks modernes comme React, Vue ou Svelte, et à tirer parti des concepts comme les modules ES6 pour construire des projets robustes et évolutifs.