Rendering Optimizations That Handle Thousands of Concurrent Markers in World Monitor

World Monitor combines HTML-only markers, pixel-radius clustering, zoom-aware LOD, render throttling, and CSS-based scaling to maintain 60 FPS when displaying tens of thousands of concurrent map markers.

The koala73/worldmonitor repository implements a sophisticated mapping architecture designed to handle massive datasets without sacrificing interactivity. These rendering optimizations ensure that even when sources like tech headquarters, protest data, or AIS vessel tracking feed tens of thousands of points into the system, the browser remains responsive.

HTML-Only Markers vs. SVG Overhead

Instead of rendering points as SVG paths, World Monitor draws all markers as plain <div> elements. In src/components/Map.ts around line 1400, methods like renderTechHQs, renderTechEvents, and renderProtests create HTML nodes rather than D3 SVG circles.

HTML elements are cheaper to instantiate and style with CSS. Position updates require only a single CSS transform property change, which the browser can composite on the GPU without triggering expensive layout recalculations.

Pixel-Radius Clustering Algorithm

Before any DOM node is created, raw coordinates pass through a clustering routine that merges nearby points in screen space. The private clusterMarkers<T> method in src/components/Map.ts (around line 1290) projects latitude/longitude pairs into pixel coordinates using the current D3 projection, then groups points that fall within a configurable pixel radius.

This reduces the DOM insertion count from thousands to hundreds. The algorithm runs in O(n log n) time using spatial bucketing, ensuring the clustering step itself does not become a bottleneck during pan or zoom operations.

Zoom-Aware Level of Detail (LOD)

The clustering radius dynamically adjusts based on the current zoom level to balance detail against performance. In src/components/Map.ts around line 1990, calls to clusterMarkers pass a radius calculated as:

const radius = this.state.zoom >= 4 ? 15 : this.state.zoom >= 3 ? 35 : 60;

When zoomed out, a larger radius creates fewer clusters, preventing visual clutter and DOM overload. As the user zooms in, the radius shrinks, gradually revealing individual markers only when the viewport contains a manageable number of them.

Render Throttling and Frame Budgeting

The map enforces a minimum render interval to prevent "render storms" during rapid interactions. In src/components/Map.ts at line 166, the constant MIN_RENDER_INTERVAL_MS defaults to 100 milliseconds (10 FPS cap).

The render() method checks this throttle at lines 961-967:

const now = performance.now();
if (now - this.lastRenderTime < MIN_RENDER_INTERVAL_MS) {
  this.pendingRender = requestAnimationFrame(() => this.render());
  return;
}
this.lastRenderTime = now;

For high-performance kiosks or gaming monitors, developers can lower this constant to 30 milliseconds (~33 FPS) to trade CPU usage for smoother animation.

CSS-Based Scaling and Batch Painting

Marker sizing uses CSS custom properties to enable browser batching. In src/components/Map.ts around line 3398, the code sets a CSS variable --marker-scale equal to 1 / zoom on the marker wrapper.

Because the scale is applied via a single CSS rule rather than individual inline styles, the browser can paint all markers in one batch operation. This avoids the per-element layout thrashing that would occur if each marker calculated its own pixel dimensions independently.

Native DOM Cleanup to Prevent Memory Leaks

After each render cycle, old layer groups are cleared using native DOM APIs rather than D3's .remove() method. In src/components/Map.ts at lines 1249-1253, the cleanup routine runs:

const node = this.dynamicLayerGroup.node();
while (node && node.firstChild) {
  node.removeChild(node.firstChild);
}

This approach eliminates hidden D3 references that could keep thousands of detached nodes alive in memory, ensuring that clustered markers are truly garbage collected when they disappear from the viewport.

SVG Layer Limits for Vector Work

To prevent heavy vector operations from degrading performance, the architecture caps SVG overlay layers. In src/components/Map.ts at line 434, the constant MAX_SVG_LAYERS is set to 9.

The UI disables toggles for additional SVG layers once this limit is reached, ensuring that expensive path rendering does not compete with the lightweight HTML marker pipeline for GPU and CPU resources.

Globe View: Batched HTML Elements via Deck.gl

The 3-D globe view uses Deck.gl's htmlElementsData API to batch markers into a single WebGL-driven texture. In src/components/GlobeMap.ts around lines 585-588, the implementation wires:

new HtmlOverlayLayer({
  htmlElementsData: markers,
  getPosition: d => d.position,
  getHTMLElement: d => d.element,
});

This avoids the per-element layout cost on the 3-D canvas, allowing the globe to display thousands of markers with the same fluidity as the 2-D map.

Practical Implementation Examples

Adding a New Marker Type with Clustering

When extending World Monitor to display new facility markers, leverage the existing clustering pipeline in src/components/Map.ts:

private renderNewFacilityMarkers(projection: d3.GeoProjection): void {
  if (!this.state.layers.newFacilities) return;

  // Filter raw data to viewport
  const visible = NEW_FACILITIES.filter(f => this.isInView(f.lat, f.lon));

  // Choose cluster radius based on zoom LOD
  const radius = this.state.zoom >= 4 ? 20 : this.state.zoom >= 3 ? 35 : 60;

  // Cluster by city to prevent unrelated facilities from merging
  const clusters = this.clusterMarkers(visible, projection, radius, f => f.city);

  // Render each cluster as a plain <div>
  clusters.forEach(c => {
    const div = document.createElement('div');
    const isCluster = c.items.length > 1;
    div.className = `new-facility-marker ${isCluster ? 'cluster' : ''}`;
    div.style.left = `${c.pos[0]}px`;
    div.style.top  = `${c.pos[1]}px`;

    if (isCluster) {
      const badge = document.createElement('span');
      badge.className = 'cluster-badge';
      badge.textContent = String(c.items.length);
      div.appendChild(badge);
    } else {
      div.title = c.items[0]!.name;
    }

    this.dynamicLayerGroup?.node()?.appendChild(div);
  });
}

This pattern utilizes clusterMarkers to reduce DOM count, applies zoom-based LOD for the radius, and uses HTML div elements for GPU-composited positioning.

Adjusting Render Throttle for High-Performance Displays

For kiosk deployments or high-refresh monitors, modify the throttle constant in src/components/Map.ts:

// Default is 100ms (10 FPS cap)
private readonly MIN_RENDER_INTERVAL_MS = 30; // ~33 FPS for smoother animation

This change affects the early-out guard in the render() method, allowing more frequent updates when hardware permits.

Summary

  • HTML-only markers in Map.ts replace expensive SVG paths with lightweight div elements styled via CSS transforms.
  • Pixel-radius clustering via clusterMarkers() reduces thousands of data points to hundreds of DOM nodes before insertion.
  • Zoom-aware LOD dynamically adjusts cluster radii based on zoom level to balance detail and performance.
  • Render throttling enforces a 100 ms minimum interval (configurable to 30 ms) to prevent frame drops during rapid interactions.
  • CSS-based scaling uses the --marker-scale custom property to batch paint operations across all markers.
  • Native DOM cleanup at lines 1249-1253 prevents memory leaks by avoiding D3's reference-retaining .remove() method.
  • SVG layer caps (MAX_SVG_LAYERS = 9) limit heavy vector rendering to preserve the HTML marker pipeline's performance.
  • Deck.gl batching in GlobeMap.ts uses htmlElementsData to render thousands of 3-D markers via a single WebGL texture.

Frequently Asked Questions

How does the pixel-radius clustering algorithm work?

The clusterMarkers<T> method in src/components/Map.ts projects geographic coordinates into screen pixels using the current D3 projection, then groups points that fall within a configurable pixel distance. It uses spatial bucketing to achieve O(n log n) complexity, merging nearby points into cluster objects that contain the original data items and a centroid position. This ensures that even with 10,000 input points, the DOM only receives a few hundred cluster div elements.

What is the default render throttle interval and how can it be changed?

By default, MIN_RENDER_INTERVAL_MS is set to 100 milliseconds in src/components/Map.ts at line 166, capping updates to 10 FPS to conserve CPU during rapid panning or zooming. Developers can lower this value—such as to 30 milliseconds for approximately 33 FPS—by modifying the constant in the class definition, which directly affects the early-out logic in the render() method at lines 961-967.

Why does World Monitor use HTML div elements instead of SVG for markers?

The map renders all point-based layers as plain HTML div elements rather than SVG paths because DOM nodes are cheaper to create, style, and move via CSS transform properties. This approach, implemented in methods like renderTechHQs and renderProtests around line 1400 of src/components/Map.ts, allows the browser to GPU-composite marker positions without triggering expensive layout recalculations that SVG path updates would incur.

How does the globe view handle thousands of markers differently than the 2D map?

While the 2D map uses D3 projections with HTML div clustering, the 3D globe view in src/components/GlobeMap.ts leverages Deck.gl's htmlElementsData API (lines 585-588) to batch every marker into a single WebGL-driven texture. This avoids per-element layout costs on the canvas, allowing the globe to display thousands of concurrent markers with the same fluidity as the 2D implementation while maintaining visual fidelity through CSS-scaled HTML elements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →