← MapboxCONTENT HISTORY

Update to Mapbox

Snapshot Sep 30, 2026 · 23:11 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "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.",
  "included_files": [
    {
      "relative_path": "AGENTS.md",
      "size_in_bytes": 6039
    },
    {
      "relative_path": "evals/evals.json",
      "size_in_bytes": 5125
    },
    {
      "relative_path": "references/data-loading.md",
      "size_in_bytes": 3053
    },
    {
      "relative_path": "references/interactions.md",
      "size_in_bytes": 2554
    },
    {
      "relative_path": "references/layers-styles.md",
      "size_in_bytes": 3346
    },
    {
      "relative_path": "references/memory.md",
      "size_in_bytes": 3097
    },
    {
      "relative_path": "references/mobile.md",
      "size_in_bytes": 2345
    }
  ],
  "skill_md_contents": "---\nname: mapbox-web-performance-patterns\ndescription: 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.\n---\n\n# Mapbox Performance Patterns Skill\n\nThis 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.\n\n**Performance philosophy:** These aren't micro-optimizations. They show up as waiting time, jank, and repeat costs that hit every user session.\n\n## Priority Levels\n\nPerformance issues are prioritized by their impact on user experience:\n\n- **🔴 Critical (Fix First)**: Directly causes slow initial load or visible jank\n- **🟡 High Impact**: Noticeable delays or increased resource usage\n- **🟢 Optimization**: Incremental improvements for polish\n\n---\n\n## 🔴 Critical: Eliminate Initialization Waterfalls\n\n**Problem:** Sequential loading creates cascading delays where each resource waits for the previous one.\n\n**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.\n\n### Anti-Pattern: Sequential Data Loading\n\n```javascript\n// ❌ BAD: Data loads AFTER map initializes\nasync function initMap() {\n  const map = new mapboxgl.Map({\n    container: 'map',\n    accessToken: MAPBOX_TOKEN,\n    style: 'mapbox://styles/mapbox/streets-v12'\n  });\n\n  // Wait for map to load, THEN fetch data\n  map.on('load', async () => {\n    const data = await fetch('/api/data'); // Waterfall!\n    map.addSource('data', { type: 'geojson', data: await data.json() });\n  });\n}\n```\n\n**Timeline:** Map init (0.5s) → Data fetch (1s) = **1.5s total**\n\n### Solution: Parallel Data Loading\n\n```javascript\n// ✅ GOOD: Data fetch starts immediately\nasync function initMap() {\n  // Start data fetch immediately (don't wait for map)\n  const dataPromise = fetch('/api/data').then((r) => r.json());\n\n  const map = new mapboxgl.Map({\n    container: 'map',\n    accessToken: MAPBOX_TOKEN,\n    style: 'mapbox://styles/mapbox/streets-v12'\n  });\n\n  // Data is ready when map loads\n  map.on('load', async () => {\n    const data = await dataPromise;\n    map.addSource('data', { type: 'geojson', data });\n    map.addLayer({\n      id: 'data-layer',\n      type: 'circle',\n      source: 'data'\n    });\n  });\n}\n```\n\n**Timeline:** Max(map init, data fetch) = **~1s total**\n\n### Set Precise Initial Viewport\n\n```javascript\n// ✅ Set exact center/zoom so the map fetches the right tiles immediately\nconst map = new mapboxgl.Map({\n  container: 'map',\n  style: 'mapbox://styles/mapbox/streets-v12',\n  center: [-122.4194, 37.7749],\n  zoom: 13\n});\n\n// Use 'idle' to know when the initial viewport is fully rendered\n// (all tiles, sprites, and other resources are loaded; no transitions in progress)\nmap.once('idle', () => {\n  console.log('Initial viewport fully rendered');\n});\n```\n\nIf 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.\n\n### Defer Non-Critical Features\n\n```javascript\n// ✅ Load critical features first, defer others\nconst map = new mapboxgl.Map({\n  /* config */\n});\n\nmap.on('load', () => {\n  // 1. Add critical layers immediately\n  addCriticalLayers(map);\n\n  // 2. Defer secondary features\n  // Note: Standard style 3D buildings can be toggled via config:\n  // map.setConfigProperty('basemap', 'show3dObjects', false);\n  requestIdleCallback(\n    () => {\n      addTerrain(map);\n      addCustom3DLayers(map); // For classic styles with custom fill-extrusion layers\n    },\n    { timeout: 2000 }\n  );\n\n  // 3. Defer analytics and non-visual features\n  setTimeout(() => {\n    initializeAnalytics(map);\n  }, 3000);\n});\n```\n\n**Impact:** Significant reduction in time-to-interactive, especially when deferring terrain and 3D layers\n\n---\n\n## 🔴 Critical: Optimize Initial Bundle Size\n\n**Problem:** Large bundles delay time-to-interactive on slow networks.\n\n**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.\n\n### Style JSON Bundle Impact\n\n```javascript\n// ❌ BAD: Inline massive style JSON (can be 500+ KB)\nconst style = {\n  version: 8,\n  sources: {\n    /* 100s of lines */\n  },\n  layers: [\n    /* 100s of layers */\n  ]\n};\n\n// ✅ GOOD: Reference Mapbox-hosted styles\nconst map = new mapboxgl.Map({\n  style: 'mapbox://styles/mapbox/streets-v12' // Fetched on demand\n});\n\n// ✅ OR: Store large custom styles externally\nconst map = new mapboxgl.Map({\n  style: '/styles/custom-style.json' // Loaded separately\n});\n```\n\n**Impact:** Reduces initial bundle by 30-50% when moving from inlined to hosted styles\n\n---\n\n## 🟡 High Impact: Optimize Marker Count\n\n**Problem:** Too many markers causes slow rendering and interaction lag.\n\n### Performance Thresholds\n\n- **< 100 markers**: HTML markers OK (Marker class)\n- **100-10,000 markers**: Use symbol layers (GPU-accelerated)\n- **10,000+ markers**: Clustering recommended\n- **100,000+ markers**: Vector tiles with server-side clustering\n\n### Anti-Pattern: Thousands of HTML Markers\n\n```javascript\n// ❌ BAD: 5,000 HTML markers = 5+ second render, janky pan/zoom\nrestaurants.forEach((restaurant) => {\n  const marker = new mapboxgl.Marker()\n    .setLngLat([restaurant.lng, restaurant.lat])\n    .setPopup(new mapboxgl.Popup().setHTML(restaurant.name))\n    .addTo(map);\n});\n```\n\n**Result:** 5,000 DOM elements, slow interactions, high memory\n\n### Solution: Use Symbol Layers (GeoJSON)\n\n```javascript\n// ✅ GOOD: GPU-accelerated rendering, smooth at 10,000+ features\nmap.addSource('restaurants', {\n  type: 'geojson',\n  data: {\n    type: 'FeatureCollection',\n    features: restaurants.map((r) => ({\n      type: 'Feature',\n      geometry: { type: 'Point', coordinates: [r.lng, r.lat] },\n      properties: { name: r.name, type: r.type }\n    }))\n  }\n});\n\nmap.addLayer({\n  id: 'restaurants',\n  type: 'symbol',\n  source: 'restaurants',\n  layout: {\n    'icon-image': 'restaurant',\n    'icon-size': 0.8,\n    'text-field': ['get', 'name'],\n    'text-size': 12,\n    'text-offset': [0, 1.5],\n    'text-anchor': 'top'\n  }\n});\n\n// Click handler (one listener for all features)\nmap.on('click', 'restaurants', (e) => {\n  const feature = e.features[0];\n  new mapboxgl.Popup().setLngLat(feature.geometry.coordinates).setHTML(feature.properties.name).addTo(map);\n});\n```\n\n**Performance:** 10,000 features render in <100ms\n\n### Solution: Clustering for High Density\n\n```javascript\n// ✅ GOOD: 50,000 markers → ~500 clusters at low zoom\nmap.addSource('restaurants', {\n  type: 'geojson',\n  data: restaurantsGeoJSON,\n  cluster: true,\n  clusterMaxZoom: 14, // Stop clustering at zoom 15\n  clusterRadius: 50 // Radius relative to tile dimensions (512 = full tile width)\n});\n\n// Cluster circle layer\nmap.addLayer({\n  id: 'clusters',\n  type: 'circle',\n  source: 'restaurants',\n  filter: ['has', 'point_count'],\n  paint: {\n    'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 750, '#f28cb1'],\n    'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40]\n  }\n});\n\n// Cluster count label\nmap.addLayer({\n  id: 'cluster-count',\n  type: 'symbol',\n  source: 'restaurants',\n  filter: ['has', 'point_count'],\n  layout: {\n    'text-field': '{point_count_abbreviated}',\n    'text-size': 12\n  }\n});\n\n// Individual point layer\nmap.addLayer({\n  id: 'unclustered-point',\n  type: 'circle',\n  source: 'restaurants',\n  filter: ['!', ['has', 'point_count']],\n  paint: {\n    'circle-color': '#11b4da',\n    'circle-radius': 6\n  }\n});\n```\n\n**Impact:** 50,000 markers at 60 FPS with smooth interaction\n\n---\n\n## Summary: Performance Checklist\n\nWhen building a Mapbox application, verify these optimizations in order:\n\n### 🔴 Critical (Do First)\n\n- [ ] Load map library and data in parallel (eliminate waterfalls)\n- [ ] Use dynamic imports for map code (reduce initial bundle)\n- [ ] Defer non-critical features (terrain, custom 3D layers, analytics)\n- [ ] Use symbol layers for > 100 markers (not HTML markers)\n- [ ] Implement viewport-based data loading for large datasets\n\n### 🟡 High Impact\n\n- [ ] Debounce/throttle map event handlers (geocode inputs, `moveend`)\n- [ ] Optimize queryRenderedFeatures with layers filter and bounding box\n- [ ] Use GeoJSON for < 5 MB, vector tiles for > 20 MB\n- [ ] Always call map.remove() on cleanup in SPAs / page teardown\n- [ ] Attach `map.on('error', …)` (or visible error UI) so style/tile/token failures are not silent\n- [ ] Reuse popup instances (don't create on every interaction)\n- [ ] Use feature state instead of dynamic layers for hover/selection\n- [ ] Cluster demos: generate enough points to stress clustering (thousands, not a few hundred)\n\n### Agent anti-pattern: happy-path only\n\nFirst-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.\n\n### 🟢 Optimization\n\n- [ ] Consolidate multiple layers with data-driven styling\n- [ ] Add mobile-specific optimizations (circle layers, disabled rotation)\n- [ ] Set minzoom/maxzoom on layers to avoid rendering at irrelevant zoom levels\n- [ ] Avoid enabling preserveDrawingBuffer or antialias unless needed\n\n### Measurement\n\n```javascript\n// Measure initial load time\nconsole.time('map-load');\nmap.on('load', () => {\n  console.timeEnd('map-load');\n  // isStyleLoaded() returns true when style, sources, tiles, sprites, and models are all loaded\n  console.log('Style loaded:', map.isStyleLoaded());\n});\n\n// Monitor frame rate\nlet frameCount = 0;\nmap.on('render', () => frameCount++);\nsetInterval(() => {\n  console.log('FPS:', frameCount);\n  frameCount = 0;\n}, 1000);\n\n// Check memory usage (Chrome DevTools -> Performance -> Memory)\n```\n\n**Target metrics:**\n\n- **Time to Interactive:** < 2 seconds on 3G\n- **Frame Rate:** 60 FPS during pan/zoom\n- **Memory Growth:** < 10 MB per hour of usage\n- **Bundle Size:** < 500 KB initial (map lazy-loaded)\n\n---\n\n## Reference Files\n\nFor detailed patterns on specific topics, load the corresponding reference file:\n\n- **`references/data-loading.md`** — GeoJSON vs Vector Tiles decision matrix, viewport-based loading, progressive loading, vector tiles for large datasets\n- **`references/interactions.md`** — Debounce/throttle events, optimize feature queries, batch DOM updates\n- **`references/memory.md`** — Map cleanup patterns, popup/marker reuse, feature state vs dynamic layers\n- **`references/mobile.md`** — Device detection, mobile-optimized layers, touch interaction, constructor options\n- **`references/layers-styles.md`** — Consolidate layers with data-driven styling, simplify expressions, zoom-based visibility\n"
}

SHA-256: 52de8281f800914e841b3c1aa94e3035833cfb648bedf110ff44c57cd89d614c