
Mapbox Web Performance Patterns
Ship faster Mapbox GL JS apps by fixing load waterfalls, bundle bloat, and marker scaling before users bounce on slow maps.
Overview
Mapbox Web Performance Patterns is an agent skill for the Ship phase that applies a prioritized Mapbox GL JS optimization guide to eliminate load waterfalls, shrink bundles, and scale markers on production map apps.
Install
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-web-performance-patternsWhat is this skill?
- Critical pattern: parallel data fetch with map init to cut initialization waterfalls (500ms–2s cited savings)
- Bundle targets: under 500KB initial, under 200KB per route; dynamic import for geocoder and heavy features
- Marker decision tree: HTML markers under 100; symbol layers for 100–10,000+ GPU-accelerated points
- Prioritized checklist: Critical → High Impact → Optimization sections
- JavaScript patterns for mapbox-gl.css splitting and avoiding full-library imports
- Eliminating initialization waterfalls cited as saving roughly 500ms–2s on initial load
- Bundle size targets: under 500KB initial bundle and under 200KB per route
- Marker guidance: HTML markers under 100; symbol layers recommended from 100 up to 10,000 markers
Adoption & trust: 1.2k installs on skills.sh; 64 GitHub stars; 2/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your Mapbox map feels slow on first load or stutters with many markers because data fetching, bundles, and marker strategies were chosen for convenience—not GPU-friendly scale.
Who is it for?
SaaS dashboards, logistics UIs, and consumer apps already on Mapbox GL JS that need RUM- or Lighthouse-driven perf fixes.
Skip if: Greenfield map provider selection, native mobile SDK performance, or backend tile-generation tuning with no GL JS client.
When should I use this skill?
User optimizes Mapbox GL JS load time, bundles, markers, or asks for Mapbox web performance patterns on an existing GL JS integration.
What do I get? / Deliverables
You implement parallel fetch-on-init, route-level bundle budgets, and the correct marker/layer strategy so initial load and interaction meet sub-second targets on real data volumes.
- Refactored init/fetch pattern removing sequential waterfalls
- Bundle-splitting plan with per-route size budgets
- Marker/layer strategy matched to expected point count
Recommended Skills
Journey fit
Performance tuning for production map UIs belongs on the Ship shelf when the product is built but load and render metrics still miss targets. The skill is a prioritized perf playbook (critical → high → optimization), which maps directly to Ship → perf.
How it compares
Focused Mapbox GL JS perf recipes—not a general Core Web Vitals audit skill and not an MCP geocoding server.
Common Questions / FAQ
Who is mapbox-web-performance-patterns for?
Indie and solo frontend builders shipping Mapbox GL JS who need a short, impact-ordered perf checklist without hiring a WebGL specialist.
When should I use mapbox-web-performance-patterns?
Use it in Ship → perf when optimizing production map load time, bundle size, or marker counts; also during Build → frontend if you are implementing the map layer and want patterns baked in from day one.
Is mapbox-web-performance-patterns safe to install?
It is documentation-style guidance; review Security Audits on this page and validate any suggested dependency or CDN changes against your supply-chain policy before merging.
SKILL.md
READMESKILL.md - Mapbox Web Performance Patterns
# Mapbox GL JS Performance Optimization Guide Quick reference for optimizing Mapbox GL JS applications. Prioritized by impact: 🔴 Critical → 🟡 High Impact → 🟢 Optimization. ## 🔴 Critical Performance Patterns (Fix First) ### 1. Eliminate Initialization Waterfalls **Impact:** Saves 500ms-2s on initial load **Problem:** Sequential loading (map → data → render) **Solution:** Parallel data fetching ```javascript // ❌ Sequential: 1.5s total map.on('load', async () => { const data = await fetch('/api/data'); // Waits for map first }); // ✅ Parallel: ~1s total const dataPromise = fetch('/api/data'); // Starts immediately const map = new mapboxgl.Map({...}); map.on('load', async () => { const data = await dataPromise; // Already fetching }); ``` **Key principle:** Start all data fetches immediately, don't wait for map load. ### 2. Bundle Size Optimization **Impact:** 200-500KB savings, faster load times **Critical actions:** - Use dynamic imports for large features: `const geocoder = await import('mapbox-gl-geocoder')` - Code-split by route/feature - Avoid importing entire Mapbox GL JS if only using specific features - Use CSS splitting for mapbox-gl.css **Size targets:** <500KB initial bundle, <200KB per route ## 🟡 High Impact Patterns ### 3. Marker Performance **Impact:** Smooth rendering with many markers **Decision tree:** - **< 100 markers:** HTML markers (`new mapboxgl.Marker()`) - OK - **100-10,000 markers:** Symbol layers - GPU-accelerated, much faster - **10,000+ markers:** Symbol layers + clustering required - **100,000+ markers:** Vector tiles with server-side clustering ```javascript // ✅ For 100+ markers: Use symbol layer, not HTML markers map.addLayer({ id: 'points', type: 'symbol', source: 'points', layout: { 'icon-image': 'marker' } }); // ✅ For 10,000+ markers: Add clustering map.addSource('points', { type: 'geojson', data: geojson, cluster: true, clusterRadius: 50 // Relative to tile dimensions (512 = full tile width) }); ``` ### 4. Data Loading Strategy **Impact:** Faster rendering, lower memory **Decision tree:** - **< 5MB GeoJSON:** Load directly as GeoJSON source - **> 5MB GeoJSON:** Use vector tiles instead - **Dynamic data:** Implement viewport-based loading - **Static data:** Embed small datasets, fetch large ones **Viewport-based loading pattern:** ```javascript map.on('moveend', () => { const bounds = map.getBounds(); fetchDataInBounds(bounds).then((data) => { map.getSource('data').setData(data); }); }); ``` **Warning:** `setData()` triggers a full re-parse in a web worker. For small datasets updated frequently, use `source.updateData()` (requires `dynamic: true`) for partial updates. For large datasets, switch to vector tiles. ### 5. Event Handler Optimization **Impact:** Prevents jank during interactions **Rules:** - Debounce search/geocoding: 300ms minimum - Throttle move/zoom events: 100ms for analytics, 16ms for UI updates (move fires ~60fps) - Use `once()` for one-time events - Remove event listeners on cleanup ```javascript // ✅ Debounce expensive operations const debouncedSearch = debounce((query) => { geocode(query); }, 300); // ✅ Throttle frequent events const throttledUpdate = throttle(() => { updateAnalytics(map.getCenter()); }, 100); ``` ### 6. Memory Management **Critical for SPAs and long-running apps** **Always cleanup on unmount:** ```javascript // ✅ Remove map and all resources map.remove(); // Removes all event listeners, sources, layers // ✅ Cancel pending requests controller.abort(); // ✅ Clear references markers.forEach((m) => m.remove()); markers = []; ``` ## 🟢 Optimization Patterns ### 7. Layer Management **Rules:** - Use feature state instead of removing/re-adding layers for hover/selection - Batch style changes: Use `map.once('idle', callback)` after multiple changes - Hide layers with visibility: 'none' instead of removing - Minimize layer count: Combine similar layers with data-driven styling where po