Files
Matthew Podwysocki 3c3702c3d7 Add Prettier formatter for consistent code formatting
- Add prettier as dev dependency
- Create .prettierrc config with markdown-specific settings
- Add .prettierignore to exclude node_modules and generated files
- Add npm scripts:
  - format: Format all files with prettier
  - format:check: Check formatting without changes
- Update npm check script to include format:check
- Add format check step to GitHub Actions CI
- Format all existing files with prettier

Prettier config:
- Preserve line wrapping in markdown (proseWrap: preserve)
- 120 char print width for markdown
- Consistent formatting for JSON and JS files

This ensures all markdown, JSON, and JS files maintain consistent
formatting across the repository.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 14:17:21 -05:00
..
2026-01-29 16:59:42 -05:00

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