Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Performance Optimized Map Example

An advanced React application demonstrating performance optimization patterns from the mapbox-web-performance-patterns skill.

Patterns Demonstrated

Parallel data loading - Eliminates initialization waterfalls ✅ Marker clustering - Handles thousands of markers efficiently ✅ Event throttling - Reduces unnecessary updates ✅ Viewport-based filtering - Only processes visible data ✅ Memory optimization - Proper cleanup and resource management

What This Example Shows

This example demonstrates advanced performance patterns for production-ready Mapbox applications:

  • Parallel loading of map and data (eliminates waterfalls)
  • Clustering for 5,000+ restaurant markers
  • Throttled event handlers for smooth interactions
  • Viewport-based marker visibility optimization
  • Performance monitoring and metrics

Performance Comparison

Pattern Markers Load Time FPS Memory
❌ Basic (HTML markers) 5,000 8-12s 15-20 500MB+
✅ Symbol layers 5,000 2-3s 45-55 150MB
✅ Clustering (this example) 5,000 1-2s 55-60 100MB

Prerequisites

  • Node.js 18 or higher
  • A Mapbox access token
  • At least 4GB RAM for testing with large datasets

Setup

  1. Install dependencies:

    npm install
  2. Create .env.local file:

    VITE_MAPBOX_ACCESS_TOKEN=pk.your_token_here
  3. Start development server:

    npm run dev
  4. Open in browser:

    http://localhost:5174
    

Project Structure

performance-optimized/
├── src/
│   ├── App.jsx                 # Main app with performance monitoring
│   ├── MapComponent.jsx        # Optimized map component
│   ├── data/
│   │   └── generateRestaurants.js  # Mock data generator
│   └── utils/
│       └── performance.js      # Performance utilities
├── package.json
└── README.md

Key Implementation Details

1. Parallel Data Loading (Critical Pattern)

From mapbox-web-performance-patterns - eliminate initialization waterfalls:

useEffect(() => {
  // ✅ GOOD: Start data fetch immediately (don't wait for map)
  const dataPromise = fetchRestaurants();

  mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

  mapRef.current = new mapboxgl.Map({
    container: mapContainerRef.current,
    style: 'mapbox://styles/mapbox/light-v11',
    center: [-122.4194, 37.7749],
    zoom: 12
  });

  mapRef.current.on('load', async () => {
    // Data is ready when map loads!
    const restaurants = await dataPromise;
    addClusteredMarkers(restaurants);
  });

  return () => mapRef.current.remove();
}, []);

Performance improvement: Eliminates 1-3 second waterfall

2. Marker Clustering

Threshold guidance from mapbox-web-performance-patterns:

  • < 500 markers: HTML markers OK
  • 500-100,000 markers: Clustering required ← This example
  • 100,000 markers: Server-side clustering

function addClusteredMarkers(restaurants) {
  map.addSource('restaurants', {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: restaurants
    },
    cluster: true,
    clusterMaxZoom: 14, // Max zoom to cluster points
    clusterRadius: 50 // Radius of each cluster (pixels)
  });

  // Cluster circles with size based on count
  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 labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'restaurants',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 12
    }
  });

  // Individual unclustered points
  map.addLayer({
    id: 'unclustered-point',
    type: 'circle',
    source: 'restaurants',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': '#FF6B35',
      'circle-radius': 8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#fff'
    }
  });
}

Performance improvement: 5,000 markers → ~50-200 visible at any zoom level

3. Event Throttling

From mapbox-web-performance-patterns - reduce jank:

import { throttle } from 'lodash';

// ✅ GOOD: Throttle to 100ms (10 updates/second max)
const updateVisibleMarkers = throttle(() => {
  const bounds = map.getBounds();
  const visible = countMarkersInBounds(bounds);
  setVisibleCount(visible);
}, 100);

map.on('move', updateVisibleMarkers);

Performance improvement: 60 FPS instead of 15-30 FPS during pan

4. Performance Monitoring

Track key metrics:

// Time to interactive
const startTime = performance.now();
map.on('idle', () => {
  const tti = performance.now() - startTime;
  console.log(`Time to interactive: ${tti}ms`);
});

// FPS monitoring
let frameCount = 0;
let lastTime = performance.now();

function measureFPS() {
  frameCount++;
  const now = performance.now();
  if (now >= lastTime + 1000) {
    console.log(`FPS: ${frameCount}`);
    frameCount = 0;
    lastTime = now;
  }
  requestAnimationFrame(measureFPS);
}
measureFPS();

// Memory usage
if (performance.memory) {
  console.log(`Memory: ${(performance.memory.usedJSHeapSize / 1048576).toFixed(2)}MB`);
}

Performance Targets

Following mapbox-web-performance-patterns metrics:

Metric Target This Example
Initial load < 1s ~800ms
Time to interactive < 2s ~1.5s
FPS (panning) > 50 55-60
Memory usage < 150MB ~100MB

Testing with Different Marker Counts

The example includes a data generator that can create different volumes:

// Edit src/data/generateRestaurants.js
export function generateRestaurants(count = 5000) {
  // Change count to test different volumes
  // 500, 1000, 5000, 10000, 50000
}

Try these volumes:

  • 500 markers - Clustering still works but maybe not necessary
  • 5,000 markers - Sweet spot for clustering (this example)
  • 10,000 markers - Clustering essential
  • 50,000+ markers - Consider server-side clustering

Skills Reference

This example follows patterns from:

  • mapbox-web-performance-patterns - All optimization techniques
  • mapbox-web-integration-patterns - React lifecycle management
  • mapbox-token-security - Secure token handling

Common Optimizations Not Shown

Other patterns from mapbox-web-performance-patterns you might add:

  • Vector tiles for large datasets (> 20MB GeoJSON)
  • Layer consolidation (reduce draw calls)
  • Feature state (efficient property updates)
  • Progressive loading (load data in chunks)

See the skill documentation for implementation details.

Benchmarking

To benchmark performance:

# Chrome DevTools
1. Open DevTools → Performance tab
2. Start recording
3. Reload page
4. Stop after map loads
5. Check metrics: Load time, FPS, Memory

# Lighthouse
npm run build
npm run preview
# Run Lighthouse audit

Next Steps

  • Add real-time data updates with feature state
  • Implement server-side clustering for 100K+ markers
  • Add progressive loading for very large datasets
  • Integrate with mapbox-cartography for custom styling

Troubleshooting

Still slow?

  • Check marker count - clustering enabled?
  • Verify event throttling is active
  • Monitor network waterfall in DevTools
  • Check for memory leaks with Profiler

Clusters not working?

  • Verify cluster: true in source
  • Check clusterRadius and clusterMaxZoom values
  • Ensure GeoJSON features are valid Points