Mesurer la performance JavaScript grâce aux DevTools : anatomie d’un bouton paresseux
Le principal mantra des expert·es en performance web est le suivant : « avant d’optimiser, il faut toujours mesurer ». Il est en effet facile de tomber sur un bout de code et de se dire qu’il pourrait être plus efficient, puis passer quelques heures à l’optimiser pour qu’à la fin le gain de performance soit tout à fait invisible pour les utilisateurs et utilisatrices du site. Il faut donc toujours prendre le temps de bien mesurer les performances d’un site avant de se décider à essayer de l’améliorer. Et pour pouvoir le faire, il est essentiel de bien maîtriser les outils qui nous servent à effectuer ces mesures.
Dans cet article nous allons nous concentrer sur un aspect bien précis des performances web : l’exécution du code JavaScript. Il peut arriver que notre code JavaScript pose des problèmes de performances, ce qui en général se traduit par un manque de fluidité de la page : les interactions ne sont pas immédiates, la page défile par à‑coups, les animations se traînent péniblement, parfois même la page s’arrête complètement de fonctionner. Nous allons voir à l’aide d’un exemple concret comment diagnostiquer ce genre de problèmes, et nous verrons quelques pistes pour les résoudre.
Petit avertissement avant de commencer : j’utilise beaucoup de mots anglais dans l’article, lorsque je trouve que l’équivalent français est moins clair et moins usité.
Prendre la mesure
Voici donc un exemple que j’ai repris du site d’un de mes clients. Il a bien sûr été simplifié à l’extrême pour que nous puissions nous concentrer sur l’essentiel. Il s’agit d’un simple bouton qui déplie un menu.
Si vous ne constatez pas de problème de performance, utilisez la jauge de CPU (Central Processing Unit, en français le processeur) qui simule un processeur plus lent. La plupart du temps lorsqu’on teste les performances d’un site internet, on le fait sur des ordinateurs qui sont bien plus puissants que ceux de nos utilisateur·rices, ce qui peut fausser l’idée qu’on se fait de la fluidité des pages testées. C’est d’ailleurs tellement courant que l’onglet Performance de Chrome vous permet lui aussi de simuler des terminaux moins puissants, via l’option de configuration « CPU Throttling ».
Maintenant que nous avons constaté que le bouton présente des problèmes de performance, nous allons essayer de comprendre ce qu’il se passe. La plupart des navigateurs proposent leur propres outils de développement, nous allons nous concentrer ici sur ceux de Chrome, tout simplement car ce sont ceux que je connais le mieux.
La première chose à faire est d’ouvrir la page en mode navigation privée, afin que vos éventuelles extensions Chrome n’interfèrent pas avec l’outil de profilage. Ensuite il faut ouvrir le panneau d’outils de développement, aller sur l’onglet « Performance », et lancer nouvel un enregistrement des performances.
Cette visualisation est ce qu’on appelle un « flamechart ». Elle permet de voir les appels de fonctions, et le temps pris par chaque fonction (plus une barre est large, plus on a passé de temps dans la fonction). Ici on peut le lire comme ceci (Je laisse volontairement de côté la barre principale Task
et ce que signifie Main
pour l’instant) :
- L’événement
click
sur notre bouton déclenche l’appel de la fonctiononMenuClick
. - Cette fonction appelle elle-même une fonction
sendAnalytics
, ainsi qu’une fonctionopenMenu
. - La fonction
sendAnalytics
appelle une fonctioncheckConsentCookie
, qui elle-même a appelé une fonctionexpensiveComputation
. - La fonction
openMenu
appelle elle aussi la fonctionexpensiveComputation
.
La fonction expensiveComputation
, que j’ai écrite, simule volontairement un code très lent.
En survolant chaque barre du flamechart, on peut également voir combien de temps on a passé dans cette fonction. Par exemple sur la capture d’écran on voit qu’on a passé 344 millisecondes dans la fonction sendAnalytics
.
En cliquant sur chacune des fonctions, il est possible d’accéder à leur implémentation. En cliquant sur onMenuClick
, on arrive sur ce code :
const onMenuClick = event => {
if (event.target.parentNode.getAttribute("aria-expanded") === "false") {
sendAnalytics("open-menu");
openMenu();
}
// ...
};
On peut en déduire que notre menu est lent à s’ouvrir parce qu’on envoie des statistiques d’utilisation avant de mettre à jour le DOM (nous allons voir qu’en réalité c’est un peu plus complexe que ça).
Premier essai : inverser les appels
La première idée qui peut venir à l’esprit en voyant ce code, c’est d’inverser les appels sendAnalytics
et openMenu
, pour obtenir le code suivant :
const onMenuClick = event => {
if (event.target.parentNode.getAttribute("aria-expanded") === "false") {
openMenu(); // d'abord on ouvre le menu
sendAnalytics("open-menu"); // ensuite on envoie les statistiques
}
// ...
};
De cette manière nous allons dans premier temps mettre à jour le DOM, puis envoyer les statistiques d’utilisation. Voyons ce que ça donne :
Si vous ralentissez assez votre CPU, vous devriez voir que notre bouton est toujours aussi lent. Lançons un nouveau profil pour comprendre ce qui se passe :
On voit que la fonction openMenu
est bien appelée avant la fonction sendAnalytics
, mais d’après nos tests ça ne suffit pas. Pour comprendre pourquoi, il faut nous intéresser aux deux éléments que nous avions laissés de côté : la barre grise nommée « Task » et le « Main ».
Comment s’exécute JavaScript
Jusqu’à maintenant, dans nos profils, nous avons uniquement regardé le flamechart qui se trouve dans l’onglet « Main ». Ce nom ne signifie pas que c’est l’onglet principal, mais que c’est l’onglet représentant ce qui se passe dans le « Main Thread » (fil d’exécution principal) du navigateur. De nombreuses opérations sont exécutées dans ce thread : notre code JavaScript, mais aussi le « parsing » du HTML, du CSS, les calculs de layout, et même la transformation de notre DOM en pixels à afficher à l’écran. Et pour exécuter toutes ces opérations, le navigateur empile des tâches à exécuter dans le Main Thread, qui les traitera ensuite dans l’ordre où elles sont arrivées. Par exemple, lorsqu’il a téléchargé un fichier JavaScript et qu’il souhaite l’exécuter, le navigateur ajoute une tâche. Ou bien lorsqu’un utilisateur clique sur un élément, le navigateur va créer une tâche pour exécuter tous les « écouteurs » attachés à cet événement (c’est ce que nous voyons pour notre fonction onMenuClick
sur le profil).
Le fait qu’il n’y ait qu’un seul thread principal est très important pour la performance : cela signifie que ces tâches ne peuvent pas s’exécuter en parallèle. Ces « tâches » sont donc l’unité d’exécution de notre code JavaScript. C’est-à-dire qu’une fois qu’une tâche a commencé, rien d’autre ne peut se passer dans le thread principal tant que celle-ci n’est pas terminée. C’est ce qu’il faut comprendre quand on entend dire que JavaScript est « single threaded » (n’a qu’un seul fil d’exécution).
On comprend donc maintenant pourquoi inverser les appels de fonction n’a eu aucun effet sur les performances de notre bouton : bien que l’on modifie le DOM plus tôt dans notre code, le navigateur ne pourra mettre à jour ce qui est affiché à l’écran qu’une fois la tâche en cours terminée, donc exactement au même moment qu’avec notre code de départ.
Deuxième essai : de belles promesses
Nous savons maintenant qu’il nous faut trouver un moyen d’exécuter notre code en deux tâches, une pour mettre à jour le menu, et une pour envoyer nos statistiques d’utilisation. Peut-être qu’en utilisant des promises (littéralement « Promesses »), qui permettent d’exécuter du code JavaScript de façon asynchrone, nous arriverons à nos fins :
const onMenuClick = event => {
if (event.target.parentNode.getAttribute("aria-expanded") === "false") {
new Promise((resolve, reject) => {
openMenu();
resolve();
}).then(() => {
sendAnalytics("open-menu");
});
//...
}
A priori le résultat n’a pas l’air meilleur. Relançons un profil pour comprendre ce qu’il se passe :
On voit qu’une nouvelle barre est apparue : « Run MicroTasks ». L’exécution des promises est en effet différée par rapport à une séquence d’exécution normale, mais plutôt que d’être exécutées dans une nouvelle tâche, elles le sont à la fin de la tâche en cours, dans ce qu’on appelle les « MicroTasks ».
Nous avons donc rendu notre code un peu plus complexe, mais du point de vue des performances rien n’a changé.
Troisième essai : remettre les choses à plus tard
Pour forcer le navigateur à exécuter notre code dans une nouvelle tâche, il faut utiliser une fonction que vous connaissez probablement déjà : setTimeout
.
const onMenuClick = event => {
if (event.target.parentNode.getAttribute("aria-expanded") === "false") {
openMenu();
setTimeout(() => {
sendAnalytics("open-menu");
}, 0);
}
//...
}
On a l’habitude d’utiliser setTimeout
pour retarder l’exécution d’une fonction d’un délai donné, et c’est en effet ce que nous souhaitons faire ici, même si c’est d’un délai de zéro milliseconde. Voyons ce que ça donne :
Normalement vous devriez voir une ouverture fluide du menu, quelle que soit la puissance simulée de votre processeur. Regardons ce qui se passe dans notre profil :
Comme prévu, notre fonction sendAnalytics
s’exécute maintenant dans une nouvelle tâche à la suite de la première. Les blocs violets et verts que nous voyons entre nos deux tâches correspondent aux tâches qui permettent de transformer le DOM et le CSS en pixels affichés à l’écran (c’est ce qu’on appelle la phase de « rendering »).
Donc pour schématiser nous pouvons dire qu’au clic sur le bouton du menu il se passe ceci :
- Le navigateur exécute notre fonction
onMenuClick
. -
onMenuClick
appelle la fonctionopenMenu
qui modifie le DOM. -
onMenuClick
demande d’ajouter une nouvelle tâche qui exécutera la fonctionsendAnalytics
. - La tâche est terminée, le thread principal passe à la suivante, l’affichage du menu ouvert.
- La tâche que nous avons retardée s’exécute, et la fonction
sendAnalytics
est appelée.
Alors c’est bon, on a fini ? Pas tout à fait. Bien que le menu soit fluide, nous avons toujours une tâche qui envoie nos statistiques d’utilisation qui peut prendre plus de 250 ms, ce qui veut dire qu’elle occupe le thread principal pendant 250 ms, pendant lesquelles le navigateur ne peut pas réagir aux interactions utilisateur·rices, ni mettre à jour ce qui est affiché sur la page 1.
Pour illustrer ce point, j’ai ajouté sur la page un élément animé :
Le menu se déplie immédiatement, par contre la boule noire reste figée un moment avant de reprendre sa course.
Nous avons vu tout à l’heure que les blocs violets et verts signifiaient la mise à jour de l’affichage, et nous voyons bien sur ce flamechart que l’exécution de notre tâche rend impossible l’apparition de ces blocs verts pendant toute la durée de son exécution. On peut également voir un triangle rouge à droite de la tâche, qui est partiellement hachurée de rouge. C’est la façon de Chrome de nous indiquer que c’est une « Long Task ».
Les Long Tasks
Pour qu’une interaction soit perçue comme instantanée, il faut qu’elle prenne moins de 100 ms. Donc pour que l’ouverture de notre menu nous paraisse instantanée, il faut que le navigateur ait affiché le menu ouvert en moins de 100 ms. Ce laps de temps comprend à la fois le temps d’exécution de notre code JavaScript, mais aussi le temps nécessaire au navigateur pour traiter les changements de DOM et le rendering. Selon la puissance de votre ordinateur et la complexité de la page, ces calculs prennent plus ou moins de temps. En comptant large, pour se garantir d’être performant même sur des téléphones vieux et/ou bas de gamme, on se garde 50 ms, ce qui laisse 50 ms à notre code JavaScript pour s’exécuter.
Les Long Tasks sont donc les tâches qui prennent plus de 50 ms à s’exécuter. Elles ne sont pas forcément toujours un problème, mais constituent un très bon indicateur pour voir rapidement si vous avez de potentiels problèmes de performance.
Diviser pour mieux régner
Une première méthode pour éviter notre Long Task est de la diviser en plusieurs tâches plus petites, chacune lancée grâce à un appel à setTimeout
. Pour ce faire il va falloir changer l’implémentation de sendAnalytics
.
const sendAnalytics = eventName => {
for (let i = 0; i < 40; i++) {
if (checkConsentCookie()) {
// send analytics
}
}
};
Ceci est une simplification du code que j’ai pu voir chez un client : une quarantaine de services de tracking collectaient des statistiques d’utilisation, et chaque service devait vérifier si l’utilisateur·rice avait bien accepté de voir ses données collectées. Entre l’accès aux cookies et les autres appels de fonctions, la fonction sendAnalytics
finissait par générer une Long Task à chaque appel 2.
Essayons donc de créer une tâche par service :
const sendAnalyticsInBatches = eventName => {
for (let i = 0; i < 40; i++) {
setTimeout(() => { // chaque itération crée une nouvelle tâche
if (checkConsentCookie()) {
// send analytics
}
});
}
};
}
Ça fonctionne mieux (la boule noire ne se fige plus complètement), mais on peut toujours constater un petit manque de fluidité dans l’animation. Encore une fois, faisons un nouveau profil pour comprendre ce qu’il se passe :
Nous pouvons voir que la Long Task a bien disparu au profit de nombreuses tâches plus courtes. Mais j’ai inclus dans la capture d’écran un onglet supplémentaire : les frames.
Une histoire de « framerate »
Nous l’avons déjà dit, pour afficher une page à l’écran, le navigateur doit transformer le DOM et le CSS en pixels. Lorsque la page n’est pas figée (ouverture d’un menu, défilement, animation, etc.) le navigateur doit produire plusieurs images. C’est comme au cinéma, on nous fait défiler devant les yeux des images à une certaine vitesse, et notre cerveau les relie entre elles pour en faire une vidéo. Le cinéma est en général à 25 images par seconde, le navigateur lui vise plutôt 60 images par seconde.
Cela signifie que pour que notre animation paraisse fluide, il faut que le navigateur puisse transformer notre DOM en image 60 fois par seconde, c’est-à-dire une fois toutes les 16,6 ms. Et comme vous l’avez peut-être retenu, c’est notre Main Thread qui s’en occupe, ce qui veut dire que le temps pris par notre code JavaScript est du temps en moins pour générer ces images. Lorsque le navigateur n’a pas le temps de générer une image dans le délai imparti, on dit qu’on a « laissé tomber une frame » (dropped a frame en anglais). Dans ce cas, le navigateur laisse affichée la frame précédente. Si ça arrive plusieurs fois d’affilée, ça finit par se voir, comme notre boule noire qui bouge par à‑coups.
L’onglet « Frame » permet de visualiser facilement les frames passées à la trappe (les carrés rouges). Sur notre capture d’écran on constate bien que de nombreuses frames n’ont pas pu être générées car elles étaient bloquées par nos tâches. C’est parce que nous empilons nos appels à setTimeout
sans laisser assez de temps au navigateur pour générer des images de temps en temps.
Cela dit, je ne m’explique pas pourquoi Chrome exécute de nombreuses fois des appels consécutifs à nos callbacks sans passer par une étape de « rendering » entre les deux : d’après la spécification le navigateur devrait pourtant passer par cette étape entre chaque tâche JavaScript. Si vous avez l’explication, je serais ravi de l’entendre (par ailleurs cet exemple fonctionne mieux sur Firefox, qui lui semble passer régulièrement par des étapes de rendering, ce qui pourrait indiquer des divergences d’implémentation du standard).
Notons également qu’en ajoutant une animation sur la page, j’ai ajouté beaucoup de pression sur le navigateur qui doit maintenant générer une frame toutes les 16,6 ms, alors qu’en l’absence d’élément animé, réagir en moins de 100 ms suffirait. Quand vous souhaitez optimiser les performances d’un site, il est important de prendre en compte ce contexte pour déterminer quoi optimiser et à quel point. Sur un vrai site il est tout à fait possible que j’arrête dès l’ajout du premier setTimeout
, en laissant la Long Task en place si j’estime qu’elle ne nuit pas à l’expérience utilisateur.
Vers une meilleure répartition des ressources
Essayons tout de même d’améliorer encore la fluidité de l’animation. Pour ce faire nous allons changer la façon dont nous créons nos tâches :
const sendAnalyticsInDistributedBatches = eventName => {
doSendDistributed(40);
};
const doSendDistributed = i => {
if (i === 0) {
return;
}
if (checkConsentCookie()) {
// send analytics
}
setTimeout(() => {
doSendDistributed(i - 1);
});
};
Si nous n’êtes pas familier des fonctions récursives il est possible que ce bout de code soit difficile à saisir. Ne vous en faites pas, ça n’empêche pas de comprendre le principe : avant nous avions une première tâche qui en programmait quarante d’un coup, maintenant chaque tâche a la responsabilité de programmer la suivante.
Le résultat est plus satisfaisant : nous avons toujours nos nombreuses tâches qui viennent remplacer la Long Task originale, mais elles sont maintenant légèrement plus espacées, ce qui laisse le temps au navigateur de générer nos frames. Il en manque encore quelques-unes, mais c’est parfois inévitable quand une de nos tâches prend plus de 16 millisecondes à s’exécuter.
Il existe cependant une dernière méthode dont j’aimerais dire un mot.
S’appuyer sur la plateforme
requestIdleCallback
est une fonction JavaScript relativement récente qui nous donne une nouvelle manière de résoudre le problème qu’on vient de voir. Comme setTimeout
, cette fonction permet de planifier une nouvelle tâche, à la différence majeure qu’on laisse le navigateur choisir le moment le plus opportun pour l’exécuter (« idle » signifie inoccupé).
Encore une fois ne vous inquiétez pas si vous ne comprenez pas tout le code, mon but n’est pas d’expliquer exactement comment utiliser la fonction requestIdleCallback
, mais de montrer à quoi elle peut servir.
const sendAnalyticsWithRequestIdleCallback = eventName => {
let i = 0;
const callback = deadline => {
while ((deadline.timeRemaining() > 0 && i < 40) {
if (checkConsentCookie()) {
// send analytics
}
i++;
}
if (i < 40) {
requestIdleCallback(callback);
}
};
requestIdleCallback(callback);
};
Cet exemple ne fonctionnera ni sur Safari ni sur iOS en général, requestIdleCallback
n’y ayant pas encore été implémenté.
Le résultat est très similaire, il nous manque quelques frames, mais on ne peut pas y couper dans la mesure où certaines de nos tâches prennent plus de 16 millisecondes à s’exécuter. L’avantage de requestIdleCallback
n’est pas très visible ici, mais c’est celui d’une approche « déclarative » plutôt qu” »impérative » : nous voulons exécuter notre code par morceaux, à des moments où le navigateur n’a rien de plus important à faire. Plutôt que de bricoler de façon impérative avec des appels à setTimeout
, nous laissons le navigateur décider du moment opportun d’exécution des tâches grâce à une API dédiée à ce cas d’usage. Dans le cas où il y aurait beaucoup de choses à exécuter en même temps que nos envois de statistiques, nous pouvons compter sur le navigateur pour jongler entre les différentes tâches de manière optimale.
Conclusion
Je n’ai fait ici qu’effleurer les fonctionnalités de outils de développement de Chrome, véritable mine d’information qui, même si elle peut être intimidante, vaut vraiment la peine d’être explorée quand on passe une partie non négligeable de ses journées de travail à écrire du code JavaScript.
Pour finir j’aimerais noter que les problèmes de performance liés à l’exécution de JavaScript ont à mes yeux une place un peu particulière dans le panel des problèmes de webperf. En effet, encore plus que les problèmes de Layout Shifts ou de connectivité, ils sont liés à la puissance du processeur et vont donc davantage se faire sentir sur les terminaux les moins chers, ce qui signifie qu’ils vont pénaliser plus systématiquement et plus fortement les utilisateur·ices les plus pauvres. J’espère vous avoir convaincu qu’il n’est pas si compliqué d’analyser les performances d’exécution de votre code javascript, et que vous vous sentirez capable de prendre un peu de temps pour vous assurer qu’il ne rend pas votre site inutilisable pour certaines personnes. Vos utilisateur·ices ne remarqueront probablement pas vos efforts, mais c’est bien là le but de la manœuvre.
Quelques ressources utiles (en anglais)
- Une présentation sur l’Event Loop, qui explique comment sont gérées les différentes activités du fil principal d’exécution
- Using requestIdleCallback
- Inside look at modern web browser
- Investigate animation performance with DevTools
En bonus
Les plus curieux d’entre vous seront peut-être allés voir l’implémentation de l’animation de la boule noire :
@keyframes circle {
0% {
transform: rotate(0deg) translate(-4rem) rotate(0deg);
}
50% {
}
100% {
width: 1rem;
transform: rotate(360deg) translate(-4rem) rotate(-360deg);
}
}
Pourquoi avoir ajouté cette propriété width
qui ne sert à rien ? Eh bien si on la retire, alors la seule propriété animée de notre élément est la propriété transform
, qui partage avec la propriété opacity
une caractéristique importante : elle est animée par le GPU (la carte graphique). Cela signifie que lorsque vous animez uniquement ces propriétés sur un élément, le Main Thread n’a pas à générer les 60 images par seconde qui montrent l’élément bouger, c’est la carte graphique qui s’en occupe.
Pour notre exemple cela signifie que si l’on retire la propriété width
, nous n’aurons plus aucun problème de fluidité sur l’animation, même si nous générons des Long Tasks avec notre code JavaScript.
- ↑ Dans les versions anciennes des navigateurs, une telle tâche aurait même bloqué le défilement de la page. C’était un problème tellement courant et irritant que les navigateurs utilisent aujourd’hui un fil d’exécution dédié au scroll, de sorte qu’il est plus difficile (même si c’est toujours possible) de ralentir le défilement de la page avec du code JavaScript.
-
↑ Ici il est possible d’optimiser le temps d’exécution de la fonction, par exemple en gardant dans une variable la valeur des cookies, pour ne pas avoir à y accéder à chaque itération. C’est toujours la première approche à avoir, plutôt que d’ajouter des appels à
setTimeout
un peu partout.
3 commentaires sur cet article
Stéphane Bortzmeyer, le 18 décembre 2021 à 11:19
Ce qui me frappe dans cet article, c'est le choix des exemples. Bien sûr, ce ne sont que des exemples mais ils me semblent vraiment mal choisis. Pourquoi du Javascript juste pour afficher un menu (bonjour, l'accessibiité) ? Et, surtout, pourquoi de la surveillance via les "analytics" ? Plutôt que d'optimiser les performances des mauvaises idées, il vaudrait mieux les remettre en cause. La surveillance exercée au profit de l'industrie publicitaire par les divers « traqueurs » est politiquement inacceptable, écologiquement ruineuse et en plus ça fait ramer le site !
Comme le note Hercule anome sur le fédivers, « On essaie d'améliorer des choses sans remettre en cause le bien-fondé de leur existence. »
PS : le menu ne marche pas sur mon Firefox, peut-être en raison de mon bloqueur de pub. Pas inclusif, comme exemple.
PPS : la liste de traqueurs marketing sur 24joursdeweb est impressionnante :-(
Julien W, le 18 décembre 2021 à 13:38
Merci pour cet excellent article qui permet d'en savoir plus le fonctionnement des event loops des navigateurs. Pour aller plus loin sur l'utilisation de requestIdleCallback, on peut aussi regarder du côté des js-coroutines: pour l'explication complète: https://dev.to/miketalbot/60fps-javascript-while-stringfying-and-parsing-100mbs-of-json-84l pour la lib: https://github.com/miketalbot/js-coroutines
Attention tout de même au danger de se dire que ça ne sert plus à rien d'optimiser son code si on peut le découper :-)
Attention: le code ne fonctionne pas avec ublock origin, en tout cas avec Firefox: le script "analytics.js" est bloqué, et comme il est chargé avec un "import" on dirait bien que ça empêche le reste du script de fonctionner.
Kim Laï Trinh, le 18 décembre 2021 à 15:46
Bonjour Stéphane,
J'ai choisis cet exemple car je l'ai vu chez un client, et que c'est une situation très courante sur les sites web (le fameux "burger menu" en navigation mobile). Il me semble qu'il y a d'ailleurs des débats dans le monde des expert·es accessibilité sur la meilleure manière d'implémenter ça de façon accessible. Ça me permet de signaler deux articles très intéressants à ce sujet : https://inclusive-components.design/collapsible-sections/ et https://piccalil.li/tutorial/build-a-fully-responsive-progressively-enhanced-burger-menu/.
Pour ce qui est de remettre en question le tracking, je n'en mets jamais sur mes sites personnels, et je propose toujours à mes clients de faire la même chose, mais force est de constater qu'il est pratiquement impossible de les convaincre dans la grande majorité des cas.
Et en effet, je n'ai pas précisé que la meilleure manière de ne pas avoir de problèmes liés à javascript c'est de s'appuyer au maximum sur HTML et CSS quand c'est possible, et c'est ce que je fais avec mes clients. Dans le cas d'un burger menu je crois vraiment que c'est impossible, mais bien sûr je peux me tromper :)
PS : j'ai réglé le problème des bloqueurs de pub.
Il n’est plus possible de laisser un commentaire sur les articles mais la discussion continue sur les réseaux sociaux :