Skip to content

Performance Optimization

Learn how to optimize the performance of your route optimization maps for the best user experience.

Overview

This guide covers performance optimization strategies across:

  • Bundle size optimization
  • Runtime performance
  • Memory management
  • Network optimization
  • Rendering optimization

Bundle Size Optimization

Tree Shaking

Import only what you need:

typescript
// ✅ Good - Named imports enable tree shaking
import { useRouteMap, useMapControls } from '@route-optimization/react';

// ❌ Avoid - Default imports may bundle more than needed
import * as RouteMap from '@route-optimization/react';

Code Splitting

Split large components into separate chunks:

React

typescript
import { lazy, Suspense } from 'react';

// Lazy load the map component
const RouteMapComponent = lazy(() => import('./RouteMapComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading map...</div>}>
      <RouteMapComponent />
    </Suspense>
  );
}

Vue

typescript
import { defineAsyncComponent } from 'vue';

const RouteMapComponent = defineAsyncComponent(() => import('./RouteMapComponent.vue'));

export default {
  components: {
    RouteMapComponent,
  },
};

Dynamic Imports

Load optimization features on demand:

typescript
// Only load when user clicks "Optimize Route"
async function handleOptimize() {
  const { RouteOptimizer } = await import('@route-optimization/vanilla');
  const optimizer = new RouteOptimizer({ apiKey: 'YOUR_API_KEY' });
  // ...
}

Package Selection

Use the appropriate package for your framework:

json
{
  "dependencies": {
    "@route-optimization/react": "^1.0.0"
    // Don't include Vue or Vanilla if using React
  }
}

Runtime Performance

Memoization

React

Use useMemo and useCallback to prevent unnecessary re-renders:

typescript
import { useMemo, useCallback } from 'react';

function RouteMapComponent({ stops }: { stops: Stop[] }) {
  // Memoize route creation
  const route = useMemo(() => ({
    stops,
    options: {
      polylineOptions: {
        strokeColor: '#4CAF50',
        strokeWeight: 6,
      },
    },
  }), [stops]);

  // Memoize callbacks
  const handleRouteClick = useCallback((routeId: string) => {
    console.log('Route clicked:', routeId);
  }, []);

  const { mapRef, renderRoute } = useRouteMap({ apiKey: 'YOUR_API_KEY' });

  useEffect(() => {
    if (route) {
      renderRoute(route);
    }
  }, [route, renderRoute]);

  return <div ref={mapRef} />;
}

Vue

Use computed and cached refs:

typescript
import { computed, watch } from 'vue';

const route = computed(() => ({
  stops: props.stops,
  options: {
    polylineOptions: {
      strokeColor: '#4CAF50',
      strokeWeight: 6,
    },
  },
}));

// Only re-render when route changes
watch(route, (newRoute) => {
  renderRoute(newRoute);
});

Debouncing

Debounce frequent updates:

typescript
import { debounce } from 'lodash-es';

// React
const debouncedRenderRoute = useMemo(
  () =>
    debounce((route: Route) => {
      renderRoute(route);
    }, 300),
  [renderRoute]
);

// Vue
const debouncedRenderRoute = debounce((route: Route) => {
  renderRoute(route);
}, 300);

// Vanilla
function createDebouncedRender(routeMap: RouteMap, delay: number) {
  let timeoutId: number;

  return (route: Route) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      routeMap.renderRoute(route);
    }, delay);
  };
}

Virtualization

For large lists of stops, use virtualization:

typescript
// React with react-window
import { FixedSizeList } from 'react-window';

function StopList({ stops }: { stops: Stop[] }) {
  const Row = ({ index, style }: { index: number; style: any }) => (
    <div style={style}>
      {stops[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      itemCount={stops.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Memory Management

Cleanup

Always clean up resources:

React

typescript
function RouteMapComponent() {
  const { mapRef, clearRoute } = useRouteMap({ apiKey: 'YOUR_API_KEY' });

  useEffect(() => {
    return () => {
      // Cleanup on unmount
      clearRoute();
    };
  }, [clearRoute]);

  return <div ref={mapRef} />;
}

Vue

typescript
import { onUnmounted } from 'vue';

const { clearRoute } = useRouteMap({ mapElement, apiKey: 'YOUR_API_KEY' });

onUnmounted(() => {
  clearRoute();
});

Vanilla

typescript
const routeMap = new RouteMap({ apiKey: 'YOUR_API_KEY' });

// Initialize
await routeMap.initialize(mapElement);

// Cleanup when done
window.addEventListener('beforeunload', () => {
  routeMap.destroy();
});

Marker Clustering

Group nearby markers to reduce rendering overhead:

typescript
import { MarkerClusterer } from '@googlemaps/markerclusterer';

function setupMarkerClustering(map: google.maps.Map, stops: Stop[]) {
  const markers = stops.map(
    (stop) =>
      new google.maps.Marker({
        position: stop.location,
        map: null, // Don't add to map yet
      })
  );

  // Use clusterer for better performance
  const clusterer = new MarkerClusterer({
    map,
    markers,
    algorithm: new SuperClusterAlgorithm({}),
  });

  return clusterer;
}

Viewport-Based Rendering

Only render markers in the visible viewport:

typescript
function renderVisibleMarkers(
  map: google.maps.Map,
  allStops: Stop[],
  addMarker: (location: LatLng) => void
) {
  const bounds = map.getBounds();
  if (!bounds) return;

  const visibleStops = allStops.filter((stop) => bounds.contains(stop.location));

  visibleStops.forEach((stop) => {
    addMarker(stop.location);
  });
}

// Listen for viewport changes
map.addListener('idle', () => {
  renderVisibleMarkers(map, allStops, addMarker);
});

Network Optimization

API Key Restrictions

Restrict API keys to prevent unauthorized usage:

typescript
// In Google Cloud Console:
// 1. HTTP referrers for websites
// 2. IP addresses for servers
// 3. Android apps
// 4. iOS apps

Request Batching

Batch multiple optimization requests:

typescript
async function batchOptimize(
  requests: OptimizationRequest[],
  optimizer: RouteOptimizer
): Promise<OptimizationResponse[]> {
  // Process in batches of 5
  const batchSize = 5;
  const results: OptimizationResponse[] = [];

  for (let i = 0; i < requests.length; i += batchSize) {
    const batch = requests.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map((req) => optimizer.optimize(req)));
    results.push(...batchResults);

    // Add delay between batches to avoid rate limits
    if (i + batchSize < requests.length) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  return results;
}

Caching

Cache optimization results:

typescript
class CachedRouteOptimizer {
  private cache = new Map<string, OptimizationResponse>();
  private optimizer: RouteOptimizer;

  constructor(config: RouteCalculatorConfig) {
    this.optimizer = new RouteOptimizer(config);
  }

  async optimize(request: OptimizationRequest): Promise<OptimizationResponse> {
    const cacheKey = this.getCacheKey(request);

    // Check cache first
    const cached = this.cache.get(cacheKey);
    if (cached) {
      return cached;
    }

    // Optimize and cache result
    const result = await this.optimizer.optimize(request);
    this.cache.set(cacheKey, result);

    return result;
  }

  private getCacheKey(request: OptimizationRequest): string {
    return JSON.stringify({
      shipments: request.shipments.map((s) => s.id).sort(),
      vehicles: request.vehicles.map((v) => v.id).sort(),
    });
  }

  clearCache(): void {
    this.cache.clear();
  }
}

Prefetching

Prefetch map tiles:

typescript
function prefetchMapTiles(map: google.maps.Map, bounds: MapBounds) {
  // Set bounds to trigger tile loading
  map.fitBounds({
    north: bounds.north,
    south: bounds.south,
    east: bounds.east,
    west: bounds.west,
  });

  // Wait for tiles to load
  google.maps.event.addListenerOnce(map, 'tilesloaded', () => {
    console.log('Map tiles prefetched');
  });
}

Rendering Optimization

Polyline Simplification

Simplify complex polylines:

typescript
import { simplify } from '@turf/simplify';
import { lineString } from '@turf/helpers';

function simplifyPolyline(coordinates: LatLng[], tolerance: number = 0.0001): LatLng[] {
  const line = lineString(coordinates.map((c) => [c.lng, c.lat]));

  const simplified = simplify(line, {
    tolerance,
    highQuality: false,
  });

  return simplified.geometry.coordinates.map((c) => ({
    lat: c[1],
    lng: c[0],
  }));
}

// Use simplified coordinates for rendering
const simplifiedStops = stops.map((stop) => ({
  ...stop,
  location: simplifyPolyline([stop.location], 0.0001)[0],
}));

Lazy Loading Maps

Load Google Maps API only when needed:

typescript
// React
function LazyMapComponent() {
  const [loadMap, setLoadMap] = useState(false);

  return (
    <div>
      {!loadMap && (
        <button onClick={() => setLoadMap(true)}>
          Load Map
        </button>
      )}
      {loadMap && <RouteMapComponent />}
    </div>
  );
}

Intersection Observer

Load map when it's in viewport:

typescript
import { useEffect, useRef, useState } from 'react';

function useIntersectionObserver() {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
    });

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, []);

  return { ref, isVisible };
}

function LazyMap() {
  const { ref, isVisible } = useIntersectionObserver();

  return (
    <div ref={ref}>
      {isVisible ? <RouteMapComponent /> : <div>Loading...</div>}
    </div>
  );
}

Monitoring & Profiling

Performance Metrics

Track key metrics:

typescript
class PerformanceMonitor {
  private metrics = new Map<string, number[]>();

  startTimer(label: string): () => void {
    const start = performance.now();

    return () => {
      const duration = performance.now() - start;
      this.recordMetric(label, duration);
    };
  }

  private recordMetric(label: string, value: number): void {
    const existing = this.metrics.get(label) || [];
    existing.push(value);
    this.metrics.set(label, existing);
  }

  getMetrics(label: string) {
    const values = this.metrics.get(label) || [];
    if (values.length === 0) return null;

    return {
      min: Math.min(...values),
      max: Math.max(...values),
      avg: values.reduce((a, b) => a + b, 0) / values.length,
      count: values.length,
    };
  }
}

// Usage
const monitor = new PerformanceMonitor();

const endTimer = monitor.startTimer('route-optimization');
await optimizer.optimize(request);
endTimer();

console.log(monitor.getMetrics('route-optimization'));
// { min: 1234, max: 5678, avg: 3456, count: 10 }

Bundle Analysis

Analyze bundle size:

bash
# Install bundle analyzer
npm install --save-dev @next/bundle-analyzer

# For Next.js
npm run build -- --profile

# For Vite
npm install --save-dev rollup-plugin-visualizer
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
});

React DevTools Profiler

Profile React components:

typescript
import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) => {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
};

function App() {
  return (
    <Profiler id="RouteMap" onRender={onRenderCallback}>
      <RouteMapComponent />
    </Profiler>
  );
}

Best Practices

1. Use Production Builds

Always use production builds in deployment:

bash
# React
npm run build

# Vue
npm run build

# Next.js
npm run build
npm start

2. Enable Compression

Enable gzip/brotli compression:

typescript
// Next.js - next.config.js
module.exports = {
  compress: true,
};

// Express
import compression from 'compression';
app.use(compression());

3. Optimize Images

Use optimized marker images:

typescript
// Use WebP format
const markerIcon = {
  url: '/icons/marker.webp',
  scaledSize: new google.maps.Size(40, 40),
};

// Or use SVG for scalability
const svgIcon = {
  path: 'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z',
  fillColor: '#FF0000',
  fillOpacity: 1,
  scale: 2,
};

4. Lazy Load Third-Party Libraries

Load heavy libraries only when needed:

typescript
// Load MarkerClusterer only when needed
async function enableClustering(map: google.maps.Map, markers: google.maps.Marker[]) {
  const { MarkerClusterer } = await import('@googlemaps/markerclusterer');

  return new MarkerClusterer({
    map,
    markers,
  });
}

5. Monitor Core Web Vitals

Track important metrics:

typescript
import { onCLS, onFID, onLCP } from 'web-vitals';

onCLS(console.log); // Cumulative Layout Shift
onFID(console.log); // First Input Delay
onLCP(console.log); // Largest Contentful Paint

Performance Checklist

  • [ ] Use named imports for tree shaking
  • [ ] Implement code splitting for large components
  • [ ] Add lazy loading for maps and heavy features
  • [ ] Use memoization to prevent unnecessary re-renders
  • [ ] Debounce frequent updates
  • [ ] Implement proper cleanup in useEffect/onUnmounted
  • [ ] Use marker clustering for large datasets
  • [ ] Implement viewport-based rendering
  • [ ] Cache optimization results
  • [ ] Batch API requests
  • [ ] Simplify complex polylines
  • [ ] Use production builds
  • [ ] Enable compression
  • [ ] Optimize images (WebP, SVG)
  • [ ] Monitor bundle size
  • [ ] Track Core Web Vitals

Next Steps

Released under the MIT License.