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:
// ✅ 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
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
import { defineAsyncComponent } from 'vue';
const RouteMapComponent = defineAsyncComponent(() => import('./RouteMapComponent.vue'));
export default {
components: {
RouteMapComponent,
},
};Dynamic Imports
Load optimization features on demand:
// 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:
{
"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:
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:
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:
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:
// 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
function RouteMapComponent() {
const { mapRef, clearRoute } = useRouteMap({ apiKey: 'YOUR_API_KEY' });
useEffect(() => {
return () => {
// Cleanup on unmount
clearRoute();
};
}, [clearRoute]);
return <div ref={mapRef} />;
}Vue
import { onUnmounted } from 'vue';
const { clearRoute } = useRouteMap({ mapElement, apiKey: 'YOUR_API_KEY' });
onUnmounted(() => {
clearRoute();
});Vanilla
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:
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:
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:
// In Google Cloud Console:
// 1. HTTP referrers for websites
// 2. IP addresses for servers
// 3. Android apps
// 4. iOS appsRequest Batching
Batch multiple optimization requests:
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:
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:
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:
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:
// 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:
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:
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:
# 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// 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:
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:
# React
npm run build
# Vue
npm run build
# Next.js
npm run build
npm start2. Enable Compression
Enable gzip/brotli compression:
// Next.js - next.config.js
module.exports = {
compress: true,
};
// Express
import compression from 'compression';
app.use(compression());3. Optimize Images
Use optimized marker images:
// 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:
// 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:
import { onCLS, onFID, onLCP } from 'web-vitals';
onCLS(console.log); // Cumulative Layout Shift
onFID(console.log); // First Input Delay
onLCP(console.log); // Largest Contentful PaintPerformance 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
- Learn how to solve issues in Troubleshooting
- Explore Customization options
- Check the API Reference