Rapport de build
Un build réussi ne dit qu'une chose : il a produit quelque chose. Pas ce que ça pèse, ni ce qui reste à corriger, ni ce qu'un lecteur d'écran en fera.
bun run report # construit, puis écrit rapport.md
bunx speedster report # sur un dist déjà construit
À la demande, et c'est délibéré : un rapport produit à chaque build devient une
ligne dans .gitignore et un fichier que personne n'ouvre.
Ce qu'il contient
| Section | Ce qu'elle montre |
|---|---|
| Poids | le premier chargement, compressé, face à un budget |
| Problèmes | types et compilation, groupés par règle puis par fichier |
| Accessibilité | les manquements, à part |
| Traductions | la couverture de chaque langue, clé par clé |
| Routes | leur nombre, et les avertissements de la découverte |
Une section vide est dite, pas omise : « aucun problème » et « le vérificateur n'a pas tourné » ne veulent pas dire la même chose, et un rapport qui tait le second se lit comme le premier.
Le poids
| brut | gzip | brotli | |
|---|---|---|---|
| premier chargement | 21,2 ko | 8,8 ko | 7,8 ko |
tout dist |
21,2 ko | 8,9 ko | 7,8 ko |
Deux distinctions font tout l'intérêt du chiffre.
Compressé plutôt que brut. Ce qu'un visiteur paie est ce qui passe sur le réseau. Les deux compressions sont mesurées à leur niveau par défaut, celui qu'un serveur applique à la volée — un chiffre obtenu au maximum ne correspondrait à rien de ce qui est reçu.
Initial plutôt que total. Un morceau chargé par une route existe dans dist
sans peser sur la première page. Les confondre transformerait le découpage de code
en régression apparente.
La distinction est déduite, et non déclarée : la page est lue, ses scripts et ses
feuilles de style suivis par leurs imports statiques, et tout ce qui n'est atteint
que par un import() reste de côté. C'est la seule méthode qui donne le bon chiffre
sur un dossier découpé, où deux fichiers voisins n'ont pas du tout le même coût :
dist/main.js import statique → compte
dist/morceaux/main-a9qn.js import statique → compte
dist/morceaux/+page-zq4f.js import() → ne compte pas
L'analyse est textuelle, et c'est assumé : le pire qu'une expression régulière puisse
faire ici est de compter un morceau de trop, ce qui rend le rapport pessimiste, jamais
flatteur. --initial index.html,main.js permet de forcer la liste, et une page
introuvable fait tout compter — en le disant.
Le budget
100 ko compressés au premier chargement, par défaut. Le nombre n'est pas arbitraire — c'est à peu près ce qui se télécharge en une seconde sur une connexion lente — mais il n'est pas sacré : un budget qui n'est jamais dépassé n'apprend rien, un budget que personne n'a choisi finit ignoré.
bunx speedster report --budget 60
Les problèmes
Le vérificateur rend une liste plate, ce qu'il faut pour l'afficher à la suite. Un rapport répond à une autre question : par où commencer.
### Par règle
| code | nature | occurrences | exemple |
| --- | --- | --- | --- |
| `a11y-img-alt` | accessibilité | 7 | <img> n'a pas d'`alt`. …
| `TS2322` | type | 1 | Type 'number' is not assignable to type 'string'.
Sept occurrences d'une même règle sont un oubli, pas sept problèmes. Le classement par nature suit la même idée : un manquement d'accessibilité ne se corrige pas comme une erreur de type, et les mélanger dans une même liste fait remettre les deux à plus tard.
L'accessibilité a donc sa section, et ses règles restent celles du compilateur — statiques. Elles attrapent une image sans texte alternatif ou un bouton sans nom ; pas un contraste insuffisant, ni un parcours au clavier impraticable. Voir Accessibilité.
Les traductions
| langue | couverture | manquantes | en trop |
|---|---|---|---|
| fr | 100 % (6/6) | 0 | 0 |
| en | 67 % (4/6) | 2 | 1 |
Une clé absente n'empêche ni la compilation ni le build : le catalogue se replie sur la langue de référence. C'est la bonne conduite à l'exécution, et le meilleur moyen de ne jamais s'apercevoir de rien. Le rapport est l'endroit où cela se voit — clé par clé, avec celles qui sont en trop, qui trahissent une faute de frappe ou un reste d'une version antérieure.
En intégration continue
Le code de sortie vaut 1 s'il reste une erreur ou si le budget est dépassé :
- run: bun install
- run: bun run build
- run: bunx speedster report --budget 60 --a11y error --json
- uses: actions/upload-artifact@v4
with:
path: rapport.json
--json écrit le même objet, sans mise en forme : de quoi comparer deux builds ou
tracer une courbe de poids. Le nom par défaut suit — rapport.json — parce qu'un
fichier qui ne dit pas ce qu'il contient finit lu de travers.
En code
Le rapport est une bibliothèque, et tout y est pur sauf la mesure du disque : son contenu se vérifie donc sans build.
import { report } from '@ajustor/speedster/build'
const assets = await report.measureDirectory('dist', { initial: ['index.html', 'main.js'] })
const rapport = report.buildReport({
project: 'mon-app',
assets,
diagnostics, // ceux de `speedster check`
checked: fichiers.length,
locales: [{ code: 'fr', messages }, { code: 'en', messages: anglais }],
budget: { initialGzip: 60 * 1024 },
})
await Bun.write('rapport.md', report.markdown(rapport))
buildReport accepte les diagnostics sous la forme que rend speedster check, mais
la reprend structurellement plutôt qu'en important le vérificateur : n'importe
quelle source de diagnostics peut l'alimenter, et le paquet reste testable sans
TypeScript.
Ce qu'il ne fait pas
Aucune mesure d'exécution. Ni temps de démarrage, ni image la plus grande affichée, ni décalage de mise en page. Ces chiffres demandent un navigateur qui charge la page — Lighthouse le fait très bien, et le dupliquer mal serait pire que de ne pas le faire.
Aucun historique. Chaque rapport décrit un build. Comparer deux versions demande de garder les fichiers, ce que fait une intégration continue avec un artefact.
Il ne juge pas les images. Leur poids est compté, mais rien ne dit qu'un PNG de 400 ko aurait dû être un WebP de 40.