# Rendering Optimizations That Handle Thousands of Concurrent Markers in World Monitor

> Discover how World Monitor achieves 60 FPS with thousands of concurrent map markers using HTML-only markers, clustering, LOD, throttling, and CSS scaling.

- Repository: [Elie Habib/worldmonitor](https://github.com/koala73/worldmonitor)
- Tags: performance
- Published: 2026-03-09

---

**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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/src/components/Map.ts) around line 1990, calls to `clusterMarkers` pass a radius calculated as:

```typescript
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`](https://github.com/koala73/worldmonitor/blob/main/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:

```typescript
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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/src/components/Map.ts) at lines 1249-1253, the cleanup routine runs:

```typescript
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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/src/components/GlobeMap.ts) around lines 585-588, the implementation wires:

```typescript
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`](https://github.com/koala73/worldmonitor/blob/main/src/components/Map.ts):

```typescript
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`](https://github.com/koala73/worldmonitor/blob/main/src/components/Map.ts):

```typescript
// 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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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`](https://github.com/koala73/worldmonitor/blob/main/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.