Compétence Mapbox Performance Patterns
Cette compétence fournit des conseils d'optimisation des performances pour construire des applications Mapbox rapides et efficaces. Les patterns sont hiérarchisés selon leur impact sur l'expérience utilisateur, en commençant par les améliorations les plus critiques.
Philosophie des performances : Ce ne sont pas des micro-optimisations. Elles se manifestent par des temps d'attente, du saccadage et des coûts répétés qui affectent chaque session utilisateur.
Niveaux de priorité
Les problèmes de performance sont hiérarchisés selon leur impact sur l'expérience utilisateur :
- 🔴 Critique (À corriger en premier) : Cause directement un chargement initial lent ou du saccadage visible
- 🟡 Impact élevé : Délais perceptibles ou augmentation de l'utilisation des ressources
- 🟢 Optimisation : Améliorations progressives pour le polissage
🔴 Critique : Éliminer les cascades d'initialisation
Problème : Le chargement séquentiel crée des délais en cascade où chaque ressource attend la précédente.
Note : Les bundlers modernes (Vite, Webpack, etc.) et les imports dynamiques ESM gèrent automatiquement le code splitting et le chargement des bibliothèques. La cascade principale à éliminer est le chargement des données - récupérer les données cartographiques séquentiellement au lieu de les charger en parallèle avec l'initialisation de la carte.
Anti-Pattern : Chargement séquentiel des données
// ❌ BAD: Data loads AFTER map initializes
async function initMap() {
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// Wait for map to load, THEN fetch data
map.on('load', async () => {
const data = await fetch('/api/data'); // Waterfall!
map.addSource('data', { type: 'geojson', data: await data.json() });
});
}
Chronologie : Initialisation carte (0,5s) → Récupération données (1s) = 1,5s total
Solution : Chargement parallèle des données
// ✅ GOOD: Data fetch starts immediately
async function initMap() {
// Start data fetch immediately (don't wait for map)
const dataPromise = fetch('/api/data').then((r) => r.json());
const map = new mapboxgl.Map({
container: 'map',
accessToken: MAPBOX_TOKEN,
style: 'mapbox://styles/mapbox/streets-v12'
});
// Data is ready when map loads
map.on('load', async () => {
const data = await dataPromise;
map.addSource('data', { type: 'geojson', data });
map.addLayer({
id: 'data-layer',
type: 'circle',
source: 'data'
});
});
}
Chronologie : Max(initialisation carte, récupération données) = ~1s total
Définir une fenêtre initiale précise
// ✅ Set exact center/zoom so the map fetches the right tiles immediately
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 13
});
// Use 'idle' to know when the initial viewport is fully rendered
// (all tiles, sprites, and other resources are loaded; no transitions in progress)
map.once('idle', () => {
console.log('Initial viewport fully rendered');
});
Si vous connaissez la zone exacte que les utilisateurs verront en premier, définir center et zoom à l'avance évite que la carte démarre à une vue par défaut, puis se déplace/zoome vers la cible, ce qui gaspille les récupérations de tuiles.
Reporter les fonctionnalités non critiques
// ✅ Load critical features first, defer others
const map = new mapboxgl.Map({
/* config */
});
map.on('load', () => {
// 1. Add critical layers immediately
addCriticalLayers(map);
// 2. Defer secondary features
// Note: Standard style 3D buildings can be toggled via config:
// map.setConfigProperty('basemap', 'show3dObjects', false);
requestIdleCallback(
() => {
addTerrain(map);
addCustom3DLayers(map); // For classic styles with custom fill-extrusion layers
},
{ timeout: 2000 }
);
// 3. Defer analytics and non-visual features
setTimeout(() => {
initializeAnalytics(map);
}, 3000);
});
Impact : Réduction significative du temps d'interactivité, en particulier lors du report du terrain et des couches 3D
🔴 Critique : Optimiser la taille du bundle initial
Problème : Les gros bundles retardent le temps d'interactivité sur les réseaux lents.
Note : Les bundlers modernes (Vite, Webpack, etc.) gèrent automatiquement le code splitting pour les applications basées sur des frameworks. Les conseils ci-dessous sont particulièrement pertinents pour optimiser ce qui est bundlé et quand.
Impact du bundle JSON de style
// ❌ BAD: Inline massive style JSON (can be 500+ KB)
const style = {
version: 8,
sources: {
/* 100s of lines */
},
layers: [
/* 100s of layers */
]
};
// ✅ GOOD: Reference Mapbox-hosted styles
const map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v12' // Fetched on demand
});
// ✅ OR: Store large custom styles externally
const map = new mapboxgl.Map({
style: '/styles/custom-style.json' // Loaded separately
});
Impact : Réduit le bundle initial de 30-50% en passant des styles intégrés aux styles hébergés
🟡 Impact élevé : Optimiser le nombre de marqueurs
Problème : Trop de marqueurs provoque un rendu lent et un décalage d'interaction.
Seuils de performance
- < 100 marqueurs : Marqueurs HTML OK (classe Marker)
- 100-10 000 marqueurs : Utiliser les couches de symboles (accélération GPU)
- 10 000+ marqueurs : Clustering recommandé
- 100 000+ marqueurs : Tuiles vectorielles avec clustering côté serveur
Anti-Pattern : Des milliers de marqueurs HTML
// ❌ BAD: 5,000 HTML markers = 5+ second render, janky pan/zoom
restaurants.forEach((restaurant) => {
const marker = new mapboxgl.Marker()
.setLngLat([restaurant.lng, restaurant.lat])
.setPopup(new mapboxgl.Popup().setHTML(restaurant.name))
.addTo(map);
});
Résultat : 5 000 éléments DOM, interactions lentes, mémoire importante
Solution : Utiliser les couches de symboles (GeoJSON)
// ✅ GOOD: GPU-accelerated rendering, smooth at 10,000+ features
map.addSource('restaurants', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: restaurants.map((r) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [r.lng, r.lat] },
properties: { name: r.name, type: r.type }
}))
}
});
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'restaurants',
layout: {
'icon-image': 'restaurant',
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5],
'text-anchor': 'top'
}
});
// Click handler (one listener for all features)
map.on('click', 'restaurants', (e) => {
const feature = e.features[0];
new mapboxgl.Popup().setLngLat(feature.geometry.coordinates).setHTML(feature.properties.name).addTo(map);
});
Performance : 10 000 fonctionnalités se rendeent en < 100ms
Solution : Clustering pour haute densité
// ✅ GOOD: 50,000 markers → ~500 clusters at low zoom
map.addSource('restaurants', {
type: 'geojson',
data: restaurantsGeoJSON,
cluster: true,
clusterMaxZoom: 14, // Stop clustering at zoom 15
clusterRadius: 50 // Radius relative to tile dimensions (512 = full tile width)
});
// Cluster circle layer
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'restaurants',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 750, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40]
}
});
// Cluster count label
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'restaurants',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 12
}
});
// Individual point layer
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'restaurants',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6
}
});
Impact : 50 000 marqueurs à 60 FPS avec interaction fluide
Résumé : Checklist des performances
Lors de la construction d'une application Mapbox, vérifiez ces optimisations dans l'ordre :
🔴 Critique (À faire en premier)
- [ ] Charger la bibliothèque carte et les données en parallèle (éliminer les cascades)
- [ ] Utiliser les imports dynamiques pour le code carte (réduire le bundle initial)
- [ ] Reporter les fonctionnalités non critiques (terrain, couches 3D personnalisées, analytics)
- [ ] Utiliser les couches de symboles pour > 100 marqueurs (pas de marqueurs HTML)
- [ ] Implémenter le chargement des données basé sur la fenêtre pour les grands ensembles de données
🟡 Impact élevé
- [ ] Debounce/throttle des gestionnaires d'événements carte
- [ ] Optimiser queryRenderedFeatures avec filtre de couches et boîte englobante
- [ ] Utiliser GeoJSON pour < 5 MB, tuiles vectorielles pour > 20 MB
- [ ] Toujours appeler map.remove() au nettoyage dans les SPAs
- [ ] Réutiliser les instances de popup (ne pas créer à chaque interaction)
- [ ] Utiliser l'état des fonctionnalités au lieu des couches dynamiques pour le survol/sélection
🟢 Optimisation
- [ ] Consolider plusieurs couches avec style basé sur les données
- [ ] Ajouter des optimisations spécifiques au mobile (couches circle, rotation désactivée)
- [ ] Définir minzoom/maxzoom sur les couches pour éviter le rendu à des niveaux de zoom non pertinents
- [ ] Éviter d'activer preserveDrawingBuffer ou antialias sauf si nécessaire
Mesure
// Measure initial load time
console.time('map-load');
map.on('load', () => {
console.timeEnd('map-load');
// isStyleLoaded() returns true when style, sources, tiles, sprites, and models are all loaded
console.log('Style loaded:', map.isStyleLoaded());
});
// Monitor frame rate
let frameCount = 0;
map.on('render', () => frameCount++);
setInterval(() => {
console.log('FPS:', frameCount);
frameCount = 0;
}, 1000);
// Check memory usage (Chrome DevTools -> Performance -> Memory)
Métriques cibles :
- Temps d'interactivité : < 2 secondes sur 3G
- Fréquence d'images : 60 FPS lors du déplacement/zoom
- Croissance mémoire : < 10 MB par heure d'utilisation
- Taille du bundle : < 500 KB initial (carte chargée en lazy)
Fichiers de référence
Pour des patterns détaillés sur des sujets spécifiques, chargez le fichier de référence correspondant :
references/data-loading.md— Matrice de décision GeoJSON vs Tuiles vectorielles, chargement basé sur la fenêtre, chargement progressif, tuiles vectorielles pour les grands ensembles de donnéesreferences/interactions.md— Debounce/throttle d'événements, optimisation des requêtes de fonctionnalités, mise en lot des mises à jour DOMreferences/memory.md— Patterns de nettoyage de carte, réutilisation des popups/marqueurs, état des fonctionnalités vs couches dynamiquesreferences/mobile.md— Détection de dispositif, couches optimisées pour mobile, interaction tactile, options du constructeurreferences/layers-styles.md— Consolider les couches avec style basé sur les données, simplifier les expressions, visibilité basée sur le zoom