Mapbox Performance Challenge

Challenge Overview

TLDR: Implement a solution to efficiently render over 10,000 map entities on a Mapbox map while maintaining with caching and clustering.

Key Performance Considerations

  • Data clustering and aggregation
  • Level of detail (LOD) management
  • Viewport-based rendering
  • WebGL optimization
  • Memory management

Implementation Strategies

  • Data Optimization:
    • Use GeoJSON simplification
    • Implement data batching
    • Apply spatial indexing
  • Rendering Techniques:
    • Implement marker clustering
    • Use heatmaps for dense areas
    • Apply viewport culling
  • Performance Monitoring:
    • Frame rate tracking
    • Memory usage monitoring
    • Load time optimization

Technical Implementation

// Example of viewport-based rendering
const visibleFeatures = features.filter(feature => {
  const [x, y] = map.project(feature.geometry.coordinates);
  return (
    x >= 0 && x <= map.getCanvas().width &&
    y >= 0 && y <= map.getCanvas().height
  );
});

// Example of marker clustering
const clusterOptions = {
  radius: 50,
  maxZoom: 15,
  minPoints: 2,
  style: {
    color: '#00ff00',
    size: 20
  }
};

// Example of data batching
const batchSize = 1000;
for (let i = 0; i < features.length; i += batchSize) {
  const batch = features.slice(i, i + batchSize);
  map.addSource(`batch-${i}`, {
    type: 'geojson',
    data: {
      type: 'FeatureCollection',
      features: batch
    }
  });
}

Viewport-Based Clustering

Dynamic clustering based on the current viewport to optimize rendering performance and improve user experience.

// Viewport-based clustering implementation
class ViewportCluster {
  constructor(map, options = {}) {
    this.map = map;
    this.options = {
      clusterRadius: options.clusterRadius || 50,
      maxZoom: options.maxZoom || 15,
      minPoints: options.minPoints || 2,
      ...options
    };
    this.clusters = new Map();
    this.updateViewport = this.updateViewport.bind(this);
    this.map.on('moveend', this.updateViewport);
  }

  updateViewport() {
    const bounds = this.map.getBounds();
    const zoom = this.map.getZoom();
    const visibleFeatures = this.getVisibleFeatures(bounds);
    
    // Clear previous clusters
    this.clearClusters();
    
    // Create new clusters based on viewport
    this.createClusters(visibleFeatures, zoom);
  }

  getVisibleFeatures(bounds) {
    return this.features.filter(feature => {
      const [lng, lat] = feature.geometry.coordinates;
      return bounds.contains([lng, lat]);
    });
  }

  createClusters(features, zoom) {
    const clusters = new Map();
    
    features.forEach(feature => {
      const clusterKey = this.getClusterKey(feature, zoom);
      if (!clusters.has(clusterKey)) {
        clusters.set(clusterKey, {
          features: [],
          center: feature.geometry.coordinates,
          count: 0
        });
      }
      
      const cluster = clusters.get(clusterKey);
      cluster.features.push(feature);
      cluster.count++;
      
      // Update cluster center
      cluster.center = this.calculateClusterCenter(cluster.features);
    });

    // Filter and process clusters
    clusters.forEach((cluster, key) => {
      if (cluster.count >= this.options.minPoints) {
        this.createClusterMarker(cluster);
      } else {
        this.createIndividualMarkers(cluster.features);
      }
    });

    this.clusters = clusters;
  }

  getClusterKey(feature, zoom) {
    const precision = Math.pow(10, Math.floor(zoom / 2));
    const [lng, lat] = feature.geometry.coordinates;
    return `${Math.floor(lng * precision)},${Math.floor(lat * precision)}`;
  }

  calculateClusterCenter(features) {
    const sum = features.reduce((acc, feature) => {
      const [lng, lat] = feature.geometry.coordinates;
      return {
        lng: acc.lng + lng,
        lat: acc.lat + lat
      };
    }, { lng: 0, lat: 0 });

    return [
      sum.lng / features.length,
      sum.lat / features.length
    ];
  }

  createClusterMarker(cluster) {
    // Implementation for creating a cluster marker
    const marker = new mapboxgl.Marker({
      element: this.createClusterElement(cluster.count)
    })
      .setLngLat(cluster.center)
      .addTo(this.map);

    marker.getElement().addEventListener('click', () => {
      this.handleClusterClick(cluster);
    });
  }

  createIndividualMarkers(features) {
    features.forEach(feature => {
      const marker = new mapboxgl.Marker()
        .setLngLat(feature.geometry.coordinates)
        .addTo(this.map);
    });
  }

  createClusterElement(count) {
    const el = document.createElement('div');
    el.className = 'cluster-marker';
    el.innerHTML = `<div class="cluster-content">${count}</div>`;
    return el;
  }

  handleClusterClick(cluster) {
    const zoom = this.map.getZoom();
    if (zoom < this.options.maxZoom) {
      this.map.flyTo({
        center: cluster.center,
        zoom: zoom + 2
      });
    } else {
      this.showClusterPopup(cluster);
    }
  }

  clearClusters() {
    // Implementation for clearing existing clusters
    this.clusters.forEach(cluster => {
      if (cluster.marker) {
        cluster.marker.remove();
      }
    });
  }
}

// Usage example
const viewportCluster = new ViewportCluster(map, {
  clusterRadius: 60,
  maxZoom: 16,
  minPoints: 3
});

Key Features

  • Dynamic clustering based on current viewport
  • Automatic cluster updates on map movement
  • Configurable clustering parameters
  • Efficient memory management
  • Smooth transitions between zoom levels

Results and Metrics

  • Initial load time under 2 seconds
  • 60 FPS during pan and zoom operations
  • Memory usage under 500MB for 10,000+ entities
  • Smooth interaction with clustered markers

Welcome to our site!

Let's start with a quick question:

What is the capital of Israel?