mapbox-web-performance-patterns
mapbox/mapbox-agent-skills
Performance optimization patterns for Mapbox GL JS web applications.
What is mapbox-web-performance-patterns?
This skill provides guidance for optimizing Mapbox GL JS applications across initialization, bundle size, rendering, and memory. Use it when building map-based web applications that need to handle large datasets, many markers, or demanding user interactions while maintaining fast load times and smooth performance.
- Eliminate initialization waterfalls by parallelizing data loading with map initialization
- Optimize bundle size by using Mapbox-hosted styles instead of inlining large style JSON
- Handle marker performance thresholds: HTML markers for <100, symbol layers for 100-10,000, clustering for 10,000+
- Implement GPU-accelerated symbol layers for rendering thousands of features smoothly
- Set precise initial viewport and defer non-critical features to improve time-to-interactive
- Use clustering strategies for high-density marker datasets up to 100,000+ features
How to install mapbox-web-performance-patterns
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-web-performance-patterns- Mapbox GL JS library installed and initialized
- Understanding of GeoJSON format for data sources
- Basic familiarity with Mapbox layer configuration
How to use mapbox-web-performance-patterns
- 1.Identify your marker/feature count and select the appropriate rendering strategy (HTML markers, symbol layers, or clustering)
- 2.Parallelize data fetching with map initialization by starting data requests before map.on('load')
- 3.Set precise center and zoom values upfront to avoid unnecessary tile fetches
- 4.Replace inlined style JSON with Mapbox-hosted styles or external style files
- 5.Defer non-critical features like terrain and 3D layers using requestIdleCallback or setTimeout
- 6.Monitor performance using map.once('idle') to track when initial viewport is fully rendered
Use cases
- Building a restaurant/location discovery app with 5,000+ points of interest
- Optimizing initial load time for a real estate map with large GeoJSON datasets
- Rendering a heatmap or analytics dashboard with thousands of data points
- Improving performance of a mobile-friendly map application on slow networks
- Implementing a clustering solution for city-scale location data
- Web developers building Mapbox GL JS applications
- Frontend engineers optimizing map-based user experiences
- Performance engineers addressing slow map load or interaction lag
- Full-stack developers handling large geographic datasets
mapbox-web-performance-patterns FAQ
Use HTML markers (Marker class) for fewer than 100 markers. For 100-10,000 markers, switch to GPU-accelerated symbol layers with GeoJSON sources. For 10,000+ markers, add clustering to reduce the number of rendered features.
Start data fetches immediately without waiting for map initialization to complete. Use Promise.all() or similar patterns to load data in parallel with map setup, then add the data to the map once both are ready.
Inlined style JSON can add 30-50% to your initial bundle size (500+ KB). Reference Mapbox-hosted styles or store custom styles in external files instead.
Symbol layers can smoothly render 10,000+ features with GPU acceleration. For 100,000+ features, use server-side clustering with vector tiles for optimal performance.
map.once('idle') fires when the initial viewport is fully rendered—all tiles, sprites, and resources are loaded and no transitions are in progress. Use it to measure time-to-interactive.
Full instructions (SKILL.md)
Source of truth, from mapbox/mapbox-agent-skills.
name: mapbox-web-performance-patterns description: Performance optimization patterns for Mapbox GL JS web applications. Covers initialization waterfalls, bundle size, rendering performance, memory management, and web optimization. Prioritized by impact on user experience.
Mapbox Performance Patterns Skill
This skill provides performance optimization guidance for building fast, efficient Mapbox applications. Patterns are prioritized by impact on user experience, starting with the most critical improvements.
Performance philosophy: These aren't micro-optimizations. They show up as waiting time, jank, and repeat costs that hit every user session.
Priority Levels
Performance issues are prioritized by their impact on user experience:
- 🔴 Critical (Fix First): Directly causes slow initial load or visible jank
- 🟡 High Impact: Noticeable delays or increased resource usage
- 🟢 Optimization: Incremental improvements for polish
🔴 Critical: Eliminate Initialization Waterfalls
Problem: Sequential loading creates cascading delays where each resource waits for the previous one.
Note: Modern bundlers (Vite, Webpack, etc.) and ESM dynamic imports automatically handle code splitting and library loading. The primary waterfall to eliminate is data loading - fetching map data sequentially instead of in parallel with map initialization.
Anti-Pattern: Sequential Data Loading
// ❌ 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() });
});
}
Timeline: Map init (0.5s) → Data fetch (1s) = 1.5s total
Solution: Parallel Data Loading
// ✅ 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'
});
});
}
Timeline: Max(map init, data fetch) = ~1s total
Set Precise Initial Viewport
// ✅ 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');
});
If you know the exact area users will see first, setting center and zoom upfront avoids the map starting at a default view and then panning/zooming to the target, which wastes tile fetches.
Defer Non-Critical Features
// ✅ 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: Significant reduction in time-to-interactive, especially when deferring terrain and 3D layers
🔴 Critical: Optimize Initial Bundle Size
Problem: Large bundles delay time-to-interactive on slow networks.
Note: Modern bundlers (Vite, Webpack, etc.) automatically handle code splitting for framework-based applications. The guidance below is most relevant for optimizing what gets bundled and when.
Style JSON Bundle Impact
// ❌ 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: Reduces initial bundle by 30-50% when moving from inlined to hosted styles
🟡 High Impact: Optimize Marker Count
Problem: Too many markers causes slow rendering and interaction lag.
Performance Thresholds
- < 100 markers: HTML markers OK (Marker class)
- 100-10,000 markers: Use symbol layers (GPU-accelerated)
- 10,000+ markers: Clustering recommended
- 100,000+ markers: Vector tiles with server-side clustering
Anti-Pattern: Thousands of HTML Markers
// ❌ 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);
});
Result: 5,000 DOM elements, slow interactions, high memory
Solution: Use Symbol Layers (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 features render in <100ms
Solution: Clustering for High Density
// ✅ 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 markers at 60 FPS with smooth interaction
Summary: Performance Checklist
When building a Mapbox application, verify these optimizations in order:
🔴 Critical (Do First)
- Load map library and data in parallel (eliminate waterfalls)
- Use dynamic imports for map code (reduce initial bundle)
- Defer non-critical features (terrain, custom 3D layers, analytics)
- Use symbol layers for > 100 markers (not HTML markers)
- Implement viewport-based data loading for large datasets
🟡 High Impact
- Debounce/throttle map event handlers (geocode inputs,
moveend) - Optimize queryRenderedFeatures with layers filter and bounding box
- Use GeoJSON for < 5 MB, vector tiles for > 20 MB
- Always call map.remove() on cleanup in SPAs / page teardown
- Attach
map.on('error', …)(or visible error UI) so style/tile/token failures are not silent - Reuse popup instances (don't create on every interaction)
- Use feature state instead of dynamic layers for hover/selection
- Cluster demos: generate enough points to stress clustering (thousands, not a few hundred)
Agent anti-pattern: happy-path only
First-pass agent code often ships a map with no map.on('error'), no map.remove(), and a tiny point set that never exercises cluster: true. Production demos need error visibility, teardown, and realistic scale.
🟢 Optimization
- Consolidate multiple layers with data-driven styling
- Add mobile-specific optimizations (circle layers, disabled rotation)
- Set minzoom/maxzoom on layers to avoid rendering at irrelevant zoom levels
- Avoid enabling preserveDrawingBuffer or antialias unless needed
Measurement
// 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)
Target metrics:
- Time to Interactive: < 2 seconds on 3G
- Frame Rate: 60 FPS during pan/zoom
- Memory Growth: < 10 MB per hour of usage
- Bundle Size: < 500 KB initial (map lazy-loaded)
Reference Files
For detailed patterns on specific topics, load the corresponding reference file:
references/data-loading.md— GeoJSON vs Vector Tiles decision matrix, viewport-based loading, progressive loading, vector tiles for large datasetsreferences/interactions.md— Debounce/throttle events, optimize feature queries, batch DOM updatesreferences/memory.md— Map cleanup patterns, popup/marker reuse, feature state vs dynamic layersreferences/mobile.md— Device detection, mobile-optimized layers, touch interaction, constructor optionsreferences/layers-styles.md— Consolidate layers with data-driven styling, simplify expressions, zoom-based visibility
Related skills
More from mapbox/mapbox-agent-skills and the wider catalog.

mapbox-android-patterns
Official integration patterns for Mapbox Maps SDK on Android. Covers installation, adding markers, user location, custom data, styles, camera control, and featureset interactions. Based on official Mapbox documentation.

mapbox-cartography
Expert cartographic guidance for designing effective, beautiful maps with Mapbox.

mapbox-data-visualization-patterns
Patterns for choropleth, heat, 3D, and animated data visualization on Mapbox maps

mapbox-geospatial-operations
Choose the right Mapbox geospatial tool based on problem type, accuracy needs, and performance constraints.

yt-dlp-downloader
Download videos from YouTube, Bilibili, Twitter, and thousands of other sites using yt-dlp. Use when the user provides a video URL and wants to download it, extract audio (MP3), download subtitles, or select video quality. Triggers on phrases like "下载视频", "download video", "yt-dlp", "YouTube", "B站", "抖音", "提取音频", "extract audio".

conventional-commit
Create conventional commit messages following best conventions. Use when committing code changes, writing commit messages, or formatting git history. Follows conventional commits specification.