# Plan — Benchmark « Librairies de cartes web : Leaflet, pigeon-maps, OpenLayers, MapLibre »

**Date :** 2026-10-12
**Article cible :** `src/content/posts/2026-10-12-benchmark-librairies-cartes-web.md`
**Données :** `public/benchmarks/2026-10-12-benchmark-librairies-cartes-web/`
**Copie de travail :** `~/dev/benchmarks-work/2026-10-12-benchmark-librairies-cartes-web/`
**Statut :** validé le 2026-10-12

## Problème et décision

- Problème : quels compromis poids / temps de chargement entre les librairies de
  cartes web courantes, selon le type de tuiles (raster PNG, raster WebP,
  vectoriel), sur une carte de station de ski (zone Val d'Isère/Tignes,
  14/45.56911/6.83055) ?
- Décision à éclairer : aucune décision immédiate — article de compréhension
  des compromis.
- Lecteurs : public.
- Hypothèses a priori du demandeur : au chargement initial,
  pigeon-maps ≫ Leaflet > MapLibre (en légèreté) ; mais les tuiles PNG sont
  lourdes et, avec la navigation, le poids total téléchargé devient comparable.

## Solutions

| Solution                          | Version                                                 | Mode                     | Tuiles testées       | Remarques                                                 |
| --------------------------------- | ------------------------------------------------------- | ------------------------ | -------------------- | --------------------------------------------------------- |
| Baseline : page React vide        | React 19.3.0                                            | Vite 8.3.4 build prod    | —                    | isole le coût du cadre                                    |
| Leaflet 1.x                       | 1.9.4                                                   | lib, composant React min | PNG, WebP            | pas de vectoriel natif                                    |
| Leaflet 2.0                       | 2.0.0-alpha.1                                           | lib ESM                  | PNG, WebP            | pas de stable à date ; pas de vectoriel natif             |
| pigeon-maps                       | 0.22.1                                                  | composant React          | PNG, WebP            | overlay pistes via composant maison (code ajouté, publié) |
| Leaflet 1.9 + maplibre-gl-leaflet | 1.9.4 + 0.1.4 (+ maplibre-gl 6.14.0)                    | lib                      | PNG, WebP, vectoriel | raster rendu par MapLibre dans Leaflet                    |
| OpenLayers                        | ol 10.11.0 (+ ol-mapbox-style 13.5.1 pour le vectoriel) | lib                      | PNG, WebP, vectoriel | style MapLibre appliqué par ol-mapbox-style               |
| maplibre-gl-js                    | 6.14.0                                                  | lib, composant React min | PNG, WebP, vectoriel |                                                           |
| maplibre-gl + react-map-gl        | 6.14.0 + 8.1.3                                          | composant React          | PNG, WebP, vectoriel |                                                           |

18 cellules mesurées + baseline. Les combinaisons vectoriel × (Leaflet 1.9,
Leaflet 2.0, pigeon-maps) sont notées `status=unsupported` dans les données.

Écartées d'office : Mapbox GL JS (licence propriétaire, clé), Google/Apple Maps
(tuiles propriétaires), react-leaflet (wrapper au coût quasi identique au
composant minimal), plugins vectoriels Leaflet (protomaps-leaflet, VectorGrid :
ne lisent pas un style MapLibre, rendu non comparable).

Paramètres communs imposés : mêmes coordonnées et zoom initial, même taille de
conteneur (plein écran), options par défaut de chaque lib (pas de
`detectRetina`, tuiles raster 256 px), même build Vite de production, servi en
local avec compression (gzip/brotli) et en-têtes de cache identiques pour les
bundles hashés.

## Charge de travail

- Scénario par run : (1) chargement à froid (cache vide) jusqu'à « carte
  prête » ; (2) rechargement avec cache HTTP chaud jusqu'à « carte prête » ;
  (3) deux `zoomIn` animés successifs (animation par défaut de la lib ; 250 ms
  pour OpenLayers, durée de son contrôle Zoom), attente de « carte prête » à
  chaque palier. Zooms : 14 → 16 en convention MapLibre, soit 15 → 17 pour
  les libs en grille 256 px (voir journal).
- Tuiles :
  - raster PNG : `https://{a,b,c}.tile.openstreetmap.org/{z}/{x}/{y}.png` +
    overlay `https://tiles.opensnowmap.org/pistes/{z}/{x}/{y}.png` ;
  - raster WebP : les mêmes tuiles transcodées par un proxy local
    (sharp, WebP lossy q=80, alpha conservé) ;
  - vectoriel : style `https://tiles.ublo.app/styles/winter` (77 couches,
    sources planet, contours, hillshade raster WebP z6-12, winter-sports z≤14,
    3 sprites, glyphes). **Pas d'overlay opensnowmap** sur les cellules
    vectorielles : le style contient déjà ses couches sports d'hiver.
- Réseau : passe d'enregistrement unique en live remplissant un cache proxy
  local (corps + en-têtes d'origine), puis toutes les mesures en rejeu depuis
  ce cache avec débit simulé. Respecte la tile usage policy d'OSM.
- Données publiables : les tuiles enregistrées ne sont pas republiées (licences
  et volume) ; le harnais permet de refaire l'enregistrement.
- Points de charge : deux profils (voir Environnement).
- Cas limites : sans objet.

## Métriques

| Métrique                 | Définition                                                                                                                                                                                                                                                      | Unité | Sens | Mesure                                          | Agrégat          | Seuil élim.  | Poids |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ---- | ----------------------------------------------- | ---------------- | ------------ | ----- |
| Poids 1er chargement     | Octets transférés (encodés) de toute la page, cache vide, jusqu'à « carte prête »                                                                                                                                                                               | Ko    | ↓    | CDP `Network.loadingFinished.encodedDataLength` | médiane, CV      | aucun        | —     |
| Poids avec cache         | Idem, 2e visite cache HTTP chaud (en-têtes d'origine rejoués)                                                                                                                                                                                                   | Ko    | ↓    | idem                                            | médiane, CV      | aucun        | —     |
| Poids JS                 | Octets JS transférés au 1er chargement (dont part de la lib, via build analysé)                                                                                                                                                                                 | Ko    | ↓    | idem + taille des chunks du build               | médiane          | aucun        | —     |
| Tuiles initiales         | Octets et nombre de requêtes de tuiles au 1er chargement à z14 ; ressources de style (style, TileJSON, sprites, glyphes) comptées à part                                                                                                                        | Ko, n | ↓    | idem, classification par URL                    | médiane          | aucun        | —     |
| Poids par type de couche | Ventilation des octets de tuiles (initial et zoom) par catégorie : raster → `osm`, `opensnowmap` ; vectoriel → `vector` (planet, contours, winter-sports, aussi détaillé par source), `raster-hillshade`, `style-resources` (style, TileJSON, sprites, glyphes) | Ko, n | ↓    | idem, classification par hôte/chemin d'URL      | médiane          | aucun        | —     |
| Tuiles zoom              | Octets réellement transférés entre « prête z14 » et « prête z16 » (surzoom et cache non comptés : seuls les transferts réseau) ; + cumul initial+zoom                                                                                                           | Ko, n | ↓    | idem                                            | médiane          | aucun        | —     |
| Carte prête              | Navigation → 1er état toutes tuiles visibles chargées (`load` tile layer Leaflet, `idle` MapLibre, `rendercomplete` OL, équivalent pigeon)                                                                                                                      | ms    | ↓    | `performance.now()` dans la page                | médiane, p95, CV | timeout 60 s | —     |
| FCP / LCP                | Web Vitals                                                                                                                                                                                                                                                      | ms    | ↓    | PerformanceObserver                             | médiane, p95     | aucun        | —     |
| TBT                      | Somme des (durée − 50 ms) des long tasks jusqu'à « carte prête »                                                                                                                                                                                                | ms    | ↓    | PerformanceObserver `longtask`                  | médiane, p95     | aucun        | —     |
| Tas JS                   | `JSHeapUsedSize` à « carte prête »                                                                                                                                                                                                                              | Mo    | ↓    | CDP `Performance.getMetrics`                    | médiane          | aucun        | —     |
| FPS au zoom              | Images/s pendant les animations de zoom (rAF)                                                                                                                                                                                                                   | img/s | ↑    | compteur rAF dans la page                       | médiane, p5      | aucun        | —     |

Critères qualitatifs : hors score, traités en prose (support du vectoriel,
maturité, code ajouté pour l'overlay pigeon-maps).
Agrégation finale : pas de score unique — tableau multicritère + verdict par
cas d'usage.
Écart jugé significatif : ≥ 20 % sur les poids, ≥ 20 % et hors recouvrement
p5-p95 sur les temps.

## Environnement

- Machine : MacBook Apple M2 local, secteur, autres applications fermées.
- Navigateur : Chromium épinglé par Playwright 1.64.0, headless, contexte
  neuf par run.
- Profils : mobile 390×844 DPR 3, 4G simulée (9 Mb/s down, 170 ms RTT),
  CPU ×4 ; desktop 1440×900 DPR 2, sans throttling.
- Isolation : processus natifs, pas de Docker.
- Outils installables : npm local dans la copie de travail, Chromium de
  Playwright (~150 Mo dans `~/Library/Caches/ms-playwright`), sharp. Rien au
  niveau système.

## Protocole

- Warm-up / répétitions : 2 warm-up écartés + 10 mesures par cellule × profil
  → (18 + 1) × 2 × 12 = 456 runs.
- Ordre d'exécution : entrelacé aléatoire, seed fixe publiée.
- État initial : froid (cache vide) et chaud (rechargement) mesurés
  séparément dans chaque run.
- Timeout : 60 s par étape ; dépassement ⇒ `status=error`.

## Budget et contraintes

- Argent : sans objet (tout local, tuiles publiques gratuites, une seule passe
  live).
- Temps : campagne estimée 30-60 min, à confirmer par le pilote.
- Licences / confidentialité : tuiles © OpenStreetMap (ODbL), OpenSnowMap,
  ublo ; aucune clé d'API.
- Validation du pilote requise : oui.

## Publication

- Public / privé : public.
- Langue : fr.
- Articles liés : self-hoster OpenFreeMap, sprites MapLibre, glyphs SDF,
  éditeurs de style, extraction OSM / tuiles.
- Graphiques : oui, SVG générés par script, en plus des tableaux.

## Journal des décisions

- 2026-10-12 — WebP obtenu par transcodage local des tuiles OSM (aucun
  serveur OSM public ne sert du WebP) : seul le format varie.
- 2026-10-12 — Enregistrement puis rejeu des tuiles : stabilité des mesures et
  respect de la tile usage policy OSM.
- 2026-10-12 — OpenLayers ajouté à la demande du demandeur.
- 2026-10-12 — opensnowmap uniquement en overlay des cellules raster ; poids
  ventilé par type de couche (osm / opensnowmap ; vectoriel / hillshade /
  ressources de style).
- 2026-10-12 — Tuiles OSM/OpenSnowMap : téléchargement unique scripté des seules
  tuiles demandées (passe de découverte hors ligne), assumé par le demandeur
  malgré la tile usage policy OSM (échelle de quelques centaines de tuiles).
- 2026-10-12 — Convention de zoom : 14/45.56911/6.83055 est en convention
  MapLibre (512 px). MapLibre et react-map-gl : 14 → 16 ; Leaflet, pigeon-maps,
  OpenLayers, maplibre-gl-leaflet (piloté par Leaflet) : 15 → 17. Même zone
  affichée partout.
- 2026-10-12 — Chromium headless lancé avec `--use-angle=metal --enable-gpu`
  (sinon WebGL en SwiftShader) ; garde-fou qui arrête la campagne sinon.
- 2026-10-12 — MapLibre 6 : `setWorkerUrl()` obligatoire avec un bundler
  (procédure Vite officielle `?worker&url`).
- 2026-10-12 — Pilote (38 runs) : corrigé le conteneur MapLibre écrasé par
  `.maplibregl-map { position: relative }`, l'attente de `rendercomplete`
  d'OpenLayers avant l'ajout asynchrone des couches vectorielles, et enregistré
  les webfonts Noto Sans que ol-mapbox-style charge par défaut depuis
  cdn.jsdelivr.net (catégorie `webfonts`, comptée dans les ressources de style).
  Données du pilote non publiées.
- 2026-10-12 — Réseau simulé côté serveur (latence par requête + débit partagé)
  plutôt que par CDP, pour couvrir les requêtes des Web Workers MapLibre.
- 2026-10-13 — Campagne : 456 runs, 0 échec, 0 réponse manquante au rejeu.
  Incident : deux captures Chrome lancées à 08:09 UTC pendant la campagne ;
  les deux runs concernés étaient des warm-up (exclus), rien n'a été relancé.
- 2026-10-12 — Fiche validée (écartées, seuil 20 %, signal « carte prête »
  pigeon-maps reconstruit par chargement des images, `rendercomplete` OL).
