PluginBench
Skill
Official
Review
Audit score 70

mapbox-web-performance-patterns

mapbox/mapbox-agent-skills

Performance optimization patterns for Mapbox GL JS web applications, prioritized by user experience impact.

What is mapbox-web-performance-patterns?

This skill provides guidance for building fast, efficient Mapbox applications by eliminating initialization waterfalls, optimizing bundle size, and managing rendering performance. Use it when building map-heavy applications where load time, responsiveness, and memory usage directly affect user experience.

  • Eliminate initialization waterfalls by parallelizing data loading with map initialization
  • Optimize bundle size by referencing hosted styles instead of inlining large style JSON
  • Choose appropriate marker rendering strategies based on feature count (HTML markers, symbol layers, or clustering)
  • Set precise initial viewport to avoid wasted tile fetches and improve time-to-interactive
  • Defer non-critical features like terrain and 3D layers using requestIdleCallback
  • Implement GPU-accelerated symbol layers for 100-10,000+ features instead of HTML markers

How to install mapbox-web-performance-patterns

npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-web-performance-patterns
Claude Code
Cursor
Windsurf
Cline

How to use mapbox-web-performance-patterns

  1. 1.Identify your initialization pattern and refactor data fetches to run in parallel with map initialization using Promise.all or similar
  2. 2.Set explicit center and zoom values in map config to ensure correct initial tile fetches
  3. 3.Audit your marker count: if over 100, convert HTML markers to GeoJSON symbol layers
  4. 4.For 10,000+ features, enable clustering with appropriate clusterRadius and clusterMaxZoom settings
  5. 5.Defer non-critical features like terrain and custom 3D layers using requestIdleCallback with a timeout
  6. 6.Replace inlined style JSON with references to mapbox://styles or external style URLs

Use cases

Good for
  • Building a restaurant discovery app with 5,000+ locations using clustering instead of individual markers
  • Reducing initial load time by 30-50% by moving inlined style JSON to external hosted styles
  • Improving time-to-interactive on slow networks by parallelizing map initialization with data fetches
  • Rendering 50,000 geographic features smoothly at 60 FPS using symbol layers and clustering
  • Deferring terrain and 3D visualization layers until after critical map content loads
Who it's for
  • Frontend developers building Mapbox GL JS applications
  • Performance engineers optimizing map-heavy web applications
  • Product teams concerned with user experience on slow networks or mobile devices
  • Developers managing large datasets (100+ markers) on interactive maps

mapbox-web-performance-patterns FAQ

When should I use HTML markers vs. symbol layers?

Use HTML markers for fewer than 100 markers where you need custom DOM elements. For 100-10,000 markers, use GeoJSON symbol layers (GPU-accelerated). For 10,000+ markers, add clustering to reduce the number of rendered features.

What is the 'idle' event and when should I use it?

The 'idle' event fires when the initial viewport is fully rendered—all tiles, sprites, and resources are loaded and no transitions are in progress. Use map.once('idle') to know when the map is ready for interaction or to measure time-to-interactive.

How much does moving from inlined styles to hosted styles improve performance?

Moving from inlined to hosted Mapbox styles typically reduces initial bundle size by 30-50%, since style JSON can be 500+ KB. Hosted styles are fetched on demand rather than bundled upfront.

What clustering settings should I use for my data?

Start with clusterRadius: 50 (relative to 512-pixel tile width) and clusterMaxZoom: 14 (stop clustering at zoom 15). Adjust clusterRadius down for denser data, up for sparser data. Monitor frame rates during pan/zoom to tune.

How do I measure if my optimizations are working?

Use browser DevTools Performance tab to measure time-to-interactive, monitor frame rates during pan/zoom (aim for 60 FPS), and check memory usage in the Memory tab. Compare before/after for data loading, bundle size, and rendering performance.

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
  • 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
  • Reuse popup instances (don't create on every interaction)
  • Use feature state instead of dynamic layers for hover/selection

🟢 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 datasets
  • references/interactions.md — Debounce/throttle events, optimize feature queries, batch DOM updates
  • references/memory.md — Map cleanup patterns, popup/marker reuse, feature state vs dynamic layers
  • references/mobile.md — Device detection, mobile-optimized layers, touch interaction, constructor options
  • references/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.

MAmapbox-android-patterns logo

mapbox-android-patterns

Official
mapbox/mapbox-agent-skills

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.

768 installs
MAmapbox-cartography logo

mapbox-cartography

Official
mapbox/mapbox-agent-skills

Expert guidance on map design principles, color theory, visual hierarchy, typography, and cartographic best practices for creating effective and beautiful maps with Mapbox. Use when designing map styles, choosing colors, or making cartographic decisions.

1.2k installsAudited
MAmapbox-data-visualization-patterns logo

mapbox-data-visualization-patterns

Official
mapbox/mapbox-agent-skills

Patterns for visualizing data on maps including choropleth maps, heat maps, 3D visualizations, data-driven styling, and animated data. Covers layer types, color scales, and performance optimization.

1.2k installs
MAmapbox-geospatial-operations logo

mapbox-geospatial-operations

Official
mapbox/mapbox-agent-skills

Expert guidance on choosing the right geospatial tool based on problem type, accuracy requirements, and performance needs

1.3k installsAudited
YTyt-dlp-downloader logo

yt-dlp-downloader

mapleshaw/yt-dlp-downloader-skill

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".

606 installs
COconventional-commit logo

conventional-commit

marcelorodrigo/agent-skills

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

835 installs