← Back to results

One listener, 100k+ interactive elements

Case study · Swiss water-infrastructure GIS platform · Angular

In short: instead of giving every one of 100k+ map elements its own event listener, the map now has exactly one — backed by a registry that instantly knows what was clicked. Faster, lighter on memory, and less code than before.

Context

The same GIS platform as the rendering case study: pipes, devices and markers rendered as 100k+ interactive entities on a map. Every one of them is clickable, hoverable and draggable — and each can belong to a different feature with its own behavior.

Problem

Originally every element on the map owned its own listener — which is exactly what Google's own docs suggest: addListener per marker or polyline. That default is fine at hundreds of elements and falls apart at this scale. At best, listeners were detached for elements outside the viewport — which meant every pan and zoom triggered attach/detach churn over thousands of elements, and at full scale the map provider was holding six figures of listener closures. Interaction logic was scattered too: whoever attached the listener decided what a click meant, so the map layer got touched by every new feature.

The mechanism

Flip the ownership: elements don't listen — the map does, once, and a registry answers the only question that matters: what did the user actually hit?

1. A registry holds every entity, indexed twice: by id (hash map, O(1)) and by position (sorted lat/lng arrays, binary-search range queries).
2. The map gets exactly one listener.
3. On interaction, the listener takes the event's lat/lng and resolves it through the registry to the concrete marker or polyline — no scanning, no per-element handlers.
4. The resolved event goes through a per-type event bus to the controller that owns that feature. Each marker can belong to a different feature — and its handling lives in that feature's controller, not in the map layer.

Before / after

Aspect Listener per element One listener + registry
Listeners attached to the map one per element — 100k+ at full scale exactly one
Pan / zoom behavior constant attach/detach churn as elements enter and leave the viewport nothing to attach or detach — the registry already knows everything
Hit-test on click whichever element happened to own a listener registry lookup by lat/lng — O(1) by id, binary search by position
Feature coupling click handling scattered across the codebase, map layer touched by every feature per-type event buses route to the controller that owns the feature
Memory 100k+ listener closures held by the map provider one closure, one registry

Why it stays simple

Shipping a new map feature no longer touches the map layer at all: register your entities, provide a controller, subscribe to your event bus. The dispatch path is one code path for every feature — there's nothing per-feature to attach, detach or leak. That's the part that surprised the team most: the fastest version was also the least code.

100k listeners isn't a scaling problem — it's an ownership problem. Move the listening to the map and the knowing to a registry, and interaction cost stops depending on how many elements exist at all.

Related: 100k+ map entities, smooth at 60fps