Open our FAQ page, open the console, and ask Chrome what is listening:
const l = getEventListeners(document); console.log(Object.keys(l).length, "types,", Object.values(l).reduce((a, v) => a + v.length, 0), "listeners"); // 90 types, 152 listeners That page is prerendered HTML. It has no form, no dialog, no media element and nothing that changes after paint. It has 268 elements in the body. And 253 event listeners.
I walked every node of four live pages over CDP to find out where they come from.
Where they are
| page | elements | listeners | on document | on window | on elements |
|---|---|---|---|---|---|
| /faq | 268 | 252 | 151 | 52 | 49 |
| /pricing | 381 | 257 | 151 | 52 | 54 |
| /tools/round-planner | 370 | 274 | 151 | 52 | 71 |
| /legal/privacy | 191 | 213 | 150 | 49 | 14 |
Those counts exclude exactly one listener per page that is not ours. More on that at the end.
Every single one of the 252 was registered by the same file, the chunk that holds react-dom. Not one of our own components calls addEventListener on a public page.
The listener count barely moves between a page with 191 elements and a page with 381. That is the point of delegation, and it is working: the fixed cost is 203 listeners on document and window, and the variable cost is small.
89 event types on document
React registers its whole event vocabulary at the root, once, and routes from there. The types it registered on our privacy policy include:
canplaythrough durationchange emptied encrypted ended loadeddata loadedmetadata pause play playing ratechange seeked seeking stalled suspend timeupdate volumechange waiting There is no <audio> or <video> anywhere on our site. There are also handlers for fullscreenchange, beforetoggle, gotpointercapture, compositionupdate and textInput on a page that is 933 words of prose about data retention.
Most types appear twice, once capturing and once bubbling: 125 of the 252 are capture listeners. The ones that appear once are the media and lifecycle events, which React only needs in the capture phase because they do not bubble. None of the 252 is registered with once, and 36 are passive.
This is not a complaint about React. Registering 89 types at the root once is cheaper than attaching handlers per element, and the browser's dispatch cost for a type nobody fires is zero. It is just a striking thing to see printed out: the floor of an interactive framework on a page that is not interactive.
The 49 that are on elements
On the FAQ page:
- 34
clicklisteners on anchors. The page has 35 anchors. The one without a listener is the one that is not aLink: a plain<a>to another site. That is a reliable way to spot client-side navigation from the outside, no source needed. On the privacy page, 6 of 10 anchors have one, and the 4 that do not are the external links to the regulator and to Stripe. - 11
errorlisteners on<script>elements, from the loader. - 1
loadand 1erroron the one<img>, which is the header logo. - 1
keydownon an<ol>. That is the toast region, which is in the root layout and therefore on all 79 pages, and which no public page ever uses.
The round planner's extra listeners are the most literal: 15 invalid listeners, for 9 number inputs and 6 selects, plus 7 button clicks. Fifteen controls, fifteen listeners. That page is the round planner, and you can count the controls yourself.
The three pages that listen to less
The legal pages are the only ones with no scroll and no resize listener on window. They are not special-cased for performance; they use a compact footer, and the full footer is the thing that carries a scroll-driven wordmark animation. Two listeners on window are the visible trace of that component on every other page.
Method, and the one listener that was not ours
Per node: DOM.querySelectorAll for *, DOM.resolveNode for an object id, then DOMDebugger.getEventListeners. That returns a scriptId per listener, and the Debugger.scriptParsed events collected during the load map those ids to URLs. Attribution is what makes the census worth anything.
Because, on every one of the four pages, exactly one listener on document was registered by a file called content-main.js, served from a chrome-extension:// origin, for an event type that was a different random UUID each time:
cdfc27e4-cbf9-4839-bb63-403650b0e2f3 9df650d2-2a18-4b93-90e1-d18434b421c9 e57c5485-9b1e-4015-be72-f357d92c990a 2e61e7fd-1f49-499a-9... That is the ad blocker in the browser profile I measure with, talking to its own content script. It is not on your copy of the page, and it would have been in my numbers if I had counted listeners instead of attributing them. The same profile has previously made our pages look like they were touching document.cookie. Measure the site, not the browser.
Check it yourself
In the console on any of our pages:
// everything delegated at the root Object.entries(getEventListeners(document)) .map(([type, l]) => `${type} x${l.length}`).join(", "); // per element, and where it came from getEventListeners(document.querySelector("main a")); Or open the Elements panel, select an anchor, and read the Event Listeners tab. Try it on /legal/privacy (213), /faq (252) and /tools/round-planner (274) and watch how little the total moves for how much more page there is.

