Le suivi des performances est un point important dans le cadre du développement d'un site internet.
Il existe tout un tas d'outils pour effectuer ce suivi, notamment :
- Lighthouse dans la console navigateur (Chrome et Edge)
- L'onglet « Performances » (Chrome et Edge)
- PageSpeed Insights
- Webpagetest
- Dareboost (ContentSquare)
- SpeedCurve
- etc.
Ces outils, payants ou gratuits, fournissent des informations précieuses. Encore faut-il les utiliser…
Pour ce qui est des outils dans la console, c'est simple : il faut ouvrir la console. Si si…
Les autres outils de mesure ne peuvent pas fonctionner tant que le serveur n'est pas accessible (ou presque).
L'inconvénient principal est que les actions à effectuer arrivent un tout petit peu trop tard.
L'idée de cet article est de pouvoir surveiller en temps réel d'éventuels petits débordements d'enthousiasme dans le développement.
Au passage, ça intègre des petits contrôles de qualité sur les éléments de formulaire, les images et le suivi du nombre d'éléments dans le DOM. C'est toujours utile.
Article Marques et mesures du temps utilisateur
La fonction Javascript
Rien de transcendant et du Javascript natif sans dépendance.
On va utiliser l'API Performance, plutôt bien supportée par les navigateurs. Mais aussi un peu comme chacun d'eux l'entend.
Et d'autres bricoles, comme la vérification des labels de champs, les informations sur les images et le nombre d'éléments dans la page.
(() => {
const rootSizeFont = parseInt(window.getComputedStyle(document.documentElement, null).getPropertyValue("font-size")) / 16;
window.localhost && console.info(`Taille de police du navigateur : ratio ${rootSizeFont} ${rootSizeFont * 16}px (${rootSizeFont}rem)`);
// Vérification de l'interface Performance
if (!window.performance) return;
// Initialisation de l'heure de départ
const executionStart = performance.now();
const logMessages = [];
// Fonctions pour formater les durées
window.roundMilliseconds = function (value, decimals = 3) {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
};
window.formatTime = function (ms) {
let get = 0;
if (ms !== 0) {
get = roundMilliseconds(ms / 1000, 5);
}
return get > 0 ? `${get}s` : '0s';
};
// raccourci pour utilisation dans des fonctions en dehors de PerformanceObserver
// exemple d'appel : window.formatTime && window.measure?.('Observer name', 'marker-start', 'marker-end');
window.measure = function (label, start, end) {
try {
console.info(`🕕 ${label} ${window.formatTime(performance?.measure(label, start, end || undefined).duration)}`);
} catch (e) {
console.warn('Erreur de fonction ' + e);
}
};
const supportedTypes = PerformanceObserver.supportedEntryTypes;
// Liste des types à exclure à paramétrer selon vos préférences
const excludeTypes = ['resource', 'mark', 'event', 'navigation', 'measure'];
// Filtre les types supportés en excluant ceux spécifiés
const typesToObserve = supportedTypes.filter(type => !excludeTypes.includes(type));
// Crée l'observateur
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
const name = entry.name || '(anonyme)';
const duration = entry.duration;
if (duration > 0) {
if (entry.entryType === 'longtask') {
roundMilliseconds(duration / 1000, 3) > .049
? logMessages.push({
text: `%c${entry.entryType} : ${name} ${formatTime(duration)}`,
style: 'color: orange'
})
: logMessages.push({
text: `%c${entry.entryType} : ${name} ${formatTime(duration)}`,
style: 'color: gray'
})
} else {
logMessages.push(`${entry.entryType} : ${name} ${formatTime(duration)}`);
}
} else {
logMessages.push({
text: `%c${entry.entryType} : ${name} ${formatTime(duration)}`,
style: 'color: limegreen'
});
}
});
});
observer.observe({entryTypes: typesToObserve});
// État du document readyState
document.onreadystatechange = function () {
const readyStateTime = performance.now();
logMessages.push(`Document readyState : '${document.readyState}' ${window.formatTime(readyStateTime - executionStart)}`);
};
// Événement DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
const domReadyTime = performance.now();
logMessages.push(`DOM prêt ${window.formatTime(domReadyTime - executionStart)}`);
(() => {
// suivi du nombre d'éléments dans le DOM
new MutationObserver((mutations) => {
const elementCount = document.body.getElementsByTagName('*').length;
const previousCount = parseInt(document.documentElement.dataset.elements) || 0;
if (elementCount !== previousCount) {
const color = elementCount > 1300 ? (elementCount < 2000 ? 'orange' : 'red') : 'inherit';
/**
* const date = new Date();
* const diff = elementCount - previousCount;
* const displayDiff = diff !== elementCount ? ` ${diff > 0 ? '+' + diff + `${diff > 1 ? ' éléments': ' élément'}` : diff}` : 'init';
* console.info(`%c${date.toTimeString().split(' ')[0]}:${date.getMilliseconds()}ms ${elementCount} éléments ${displayDiff}`, `color:${color};font-family:monospace;`);
*/
document.documentElement.setAttribute('data-elements', `${elementCount}`);
// si affichage sur la page dans un conteneur flottant
let tools = document.querySelector('.region-tools');
if (tools) {
const domCount = tools.querySelector('.region-tools-nodes');
domCount.textContent = `DOM ${elementCount}`;
if (elementCount > 1300) {
domCount.style.background = color;
}
}
}
}).observe(document.body, {subtree: true, childList: true, attributes: true});
})();
((root) => {
new ResizeObserver((entries) => {
let entry = entries[0];
if (entry.contentBoxSize) {
const {inlineSize: width, blockSize: height} = entry.contentBoxSize[0];
root.setAttributes({'data-document-width': `${width}`, 'data-document-height': `${height}`});
}
}).observe(root);
})(document.documentElement);
}, {once: true});
/*
Événement load (fin de chargement complet ≠ DOMContentLoaded)
*/
window.addEventListener('load', () => {
if (!sessionStorage.getItem('showMetrics')) {
// const pageLoadTime = performance.now();
// logMessages.push(`Chargement complet ${window.formatTime(pageLoadTime - executionStart)}`);
setTimeout(() => {
console.groupCollapsed(`Aperçu des performances`);
logMessages.forEach(message => {
typeof message === 'object'
? console.info(message.text, message.style)
: console.debug(`${message}`);
});
console.groupEnd();
}, 1000);
}
}, {once: true});
})();
Utilisation
Là, rien de plus simple : placer ce code directement dans le head de page, et hop.
Ça supporte la minification.
Versions en ligne :
Note : pour plus d'efficacité, si le script est appelé par une balise <script src="…">, il n'est pas inutile de faire un preload HTTP…
Sans autre ajout, on va récupérer des informations dans la console.
Suivant le navigateur utilisé, il y a des variantes, car les entryType ne sont pas normées. Le script est conçu pour récupérer ce que chacun sait ou veut gérer, avec la possibilité d'exclure des types.
Normalement, c'est expliqué dans les commentaires.
entryType par navigateurs
Testé avec Chromium 134+, Firefox 136+, Safari 18+
| EntryType | Chromium | Firefox | Safari |
|---|---|---|---|
element |
oui | x | x |
event |
oui | oui | x |
first-input |
oui | oui | x |
largest-contentful-paint |
oui | oui | x |
layout-shift |
oui | x | x |
long-animation-frame |
oui | x | x |
longtask |
oui | x | x |
mark |
oui | oui | oui |
measure |
oui | oui | oui |
navigation |
oui | oui | oui |
paint |
oui | oui | oui |
resource |
oui | oui | oui |
L'affichage
En gros, on va récupérer dans la console des choses qui ressemblent à ça :
Aperçu des performances : 10 messages
measure Initialisation menu accessibilité 0.0003s
measure Obfuscation des liens 0.0001s
Document readyState 'interactive' 0.1064s
DOM prêt 0.1108s
long-animation-frame long-animation-frame 0.16s
measure DOM noeuds 0.0001s
measure trackManager 0.0002s
measure Initialisation liste défilante 0.0036s
Document readyState 'complete' 0.2728s
Chargement complet 0.2729s
Ou bien, ouvrez la console, là, maintenant.
Dans cette version du script, on vérifie la présence de label associés aux différents types de champs input:not([type='hidden']), select, textarea, ainsi que la présence d'attributs alt sur les images et de role="img" sur les images vectorielles non masquées aux lecteurs d'écran.
Et là, on voit des choses habituelles comme le LCP, FCP, CLS et aussi des measure. Et ça, on va s'attarder dessus !
measure et mark
Ce qui peut être intéressant, c'est de connaitre l'impact d'un script sur le temps d'exécution d'une page.
Comment faire ?
Exemple. En haut de cette page, vous trouverez un bouton permettant de gérer l'affichage. Ce qu'on appelle une surcouche d'accessibilité.
C'est mignon de proposer ce genre de chose, mais est-ce que ça ne plombe pas les performances ? Et bien, on va mesurer ça.
Initialement, le code ressemble à ça :
(() => {
const a11yToggle = document.getElementById('button-toggle-options');
[~50 lignes de code…]
checkEnabledOptions();
})();
On va ajouter 3 choses dans le code.
window?.performance.mark('OverlayStart');au tout débutwindow?.performance.mark('OverlayEnd');à la fin des instructionswindow?.performance.measure('Overlay execution', 'OverlayStart', 'OverlayEnd');juste après.
Résultat, le même code et 3 lignes de plus. Ça reste gérable.
(() => {
window?.performance.mark('OverlayStart');
const a11yToggle = document.getElementById('button-toggle-options');
// toujours le même code
checkEnabledOptions();
window?.performance.mark('OverlayEnd');
window?.performance.measure('Overlay execution', 'OverlayStart', 'OverlayEnd');
})();
La méthode window?.performance.mark('OverlayStart'); crée un timestamp dans le tampon d'entrée de performance.
window?.performance.mark('OverlayEnd'); fait la même chose à la fin.
Note : le mark de fin est optionnel. performance.measure arrêtera la mesure dès qu'il sera appelé. C'est à utiliser au cas par cas, selon ce qu'on souhaite mesurer.
Et maintenant, on va pouvoir mesurer combien de temps le biniou a mis pour s'exécuter avec la méthode window?.performance.measure('Overlay execution', 'OverlayStart', 'OverlayEnd');.
Le script qui observe les performances va récupérer les measure et afficher dans la console :
Type : measure
Nom : Overlay execution
Durée : 0.0003s
En l'occurence, on peut dire que ça ne va pas trop mal.
Exemple d'erreurs
Liste avec images sans attributs alt et SVG sans aria-hidden="true" ni role et aria-label.
-
Image sans attribut alt -
SVG sans attribut role et visible -
Image sans attribut alt -
SVG sans attribut role et visible
Le classique pour les formulaires : le champ texte sans label avec un placeholder en guise d'étiquette.
Conclusion
Ce script est une base facilement modifiable pour l'adapter à vos contenus.
Mais rien ne remplacera une analyse approfondie.
That's all Folks !