Mapbox Web Performance Patterns

作者 mapboxaab3a6fef5c6無授權條款80 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫7 天前更新

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.

AI 產生的概覽

針對 Mapbox GL JS 網頁地圖效能最佳化的指引,涵蓋載入、算繪、記憶體與行動裝置。

功能
此技能為 Mapbox GL JS 網頁應用程式提供依優先順序排列的效能模式,分為關鍵、高影響與漸進最佳化三類。它說明初始化瀑布、套件大小、標記數量、叢集、事件處理、記憶體清理與行動裝置算繪的反模式與修正方式,並附上程式碼範例與目標指標。它也指向資料載入、互動、記憶體、行動裝置以及圖層與樣式等參考檔案。
適用情境
在建立或檢閱 Mapbox GL JS 地圖,且目標是加快載入、讓平移縮放更順暢或降低記憶體用量時使用。適合涉及大量標記、大型 GeoJSON 或向量圖磚資料集,或行動裝置地圖檢視的工作。它只是指引,不是執行階段工具。
執行需求
不需要指令碼或套件,僅包含說明與參考文件。套用其中的範例需要一個 Mapbox GL JS 專案,以及所最佳化地圖的 Mapbox 存取權杖。

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

javascript
// ❌ BAD: Data loads AFTER map initializesasync 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

javascript
// ✅ GOOD: Data fetch starts immediatelyasync 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

javascript
// ✅ Set exact center/zoom so the map fetches the right tiles immediatelyconst 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

javascript
// ✅ Load critical features first, defer othersconst 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

javascript
// ❌ 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 stylesconst map = new mapboxgl.Map({  style: 'mapbox://styles/mapbox/streets-v12' // Fetched on demand});
// ✅ OR: Store large custom styles externallyconst 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

javascript
// ❌ BAD: 5,000 HTML markers = 5+ second render, janky pan/zoomrestaurants.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)

javascript
// ✅ GOOD: GPU-accelerated rendering, smooth at 10,000+ featuresmap.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

javascript
// ✅ GOOD: 50,000 markers → ~500 clusters at low zoommap.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 layermap.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 labelmap.addLayer({  id: 'cluster-count',  type: 'symbol',  source: 'restaurants',  filter: ['has', 'point_count'],  layout: {    'text-field': '{point_count_abbreviated}',    'text-size': 12  }});
// Individual point layermap.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

javascript
// Measure initial load timeconsole.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 ratelet 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

來源與署名

來源:mapbox/mapbox-agent-skills位於skills/mapbox-web-performance-patterns提交aab3a6f

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架