Our FAQ page is prerendered. It has no form, no widget, no video, and nothing on it changes after it paints. It references 19 JavaScript chunks, 18 of which the browser actually fetches.
Earlier today I counted how many of the stylesheet's rules match anything on that page: 213 of 1,250. This is the same question asked of the JavaScript, with the instrument that does work for JavaScript: V8 precise coverage.
The number
Over CDP: Profiler.enable, Profiler.startPreciseCoverage({ detailed: true }), navigate, wait five seconds, Profiler.takePreciseCoverage.
/faq 18 chunks 1,131,567 chars 545,968 executed 48.2%
/compare/kahoot-for-pub-quiz 18 1,131,567 545,314 48.2%
/pricing 19 1,135,478 549,798 48.4%
/tools/round-planner 19 1,141,353 555,675 48.7%
/legal/privacy 13 851,085 440,924 51.8%
Two repeat runs of the FAQ page gave 49.4 and 49.5 per cent, so call it "about half" rather than 48.2 exactly. The variation is timing: idle work and router prefetches land differently each load.
Functions, which is the less forgiving way to count: 2,140 of 4,656 ever got called.
The legal pages are the control. They ship six fewer chunks, 280 KB less code, and a higher proportion of it runs, because what is left is the framework and nothing else.
Per chunk, on the FAQ page
| chunk | characters | executed | per cent | what is in it |
|---|---|---|---|---|
| 3m0h3pqana6sj | 431,216 | 215,859 | 50.1 | react-dom, plus framer-motion |
| 0x277e9ttsha0 | 113,457 | 69,871 | 61.6 | app and router code |
| 02h14kn2-134j | 93,022 | 38,211 | 41.1 | shared app chunk |
| 3p62hpmlup8nl | 53,922 | 32,515 | 60.3 | |
| 38hyhkj5es3-t | 52,606 | 8,277 | 15.7 | framework server machinery |
| 3t4wa8-s9a2_t | 48,649 | 6,247 | 12.8 | the framer-motion projection engine |
| 304mvj7upv1ev | 47,648 | 46,848 | 98.3 | our own page components |
| 3kjuay1qddqa- | 45,255 | 7,556 | 16.7 | framework server machinery |
| 0v56fs-1wokxh | 41,303 | 15,383 | 37.2 | |
| 2m9zif7pslvu7 | 27,736 | 27,028 | 97.4 | our own code |
| 17zhs14x56jgd | 27,954 | 7,152 | 25.6 | Radix |
| 2x1jr4v8dirou | 26,678 | 2,148 | 8.1 | Radix dropdown |
The pattern is clean: the chunks that are ours run almost entirely (97 and 98 per cent), and the ones that drag the average down are libraries and framework internals that are present because something, somewhere in the app, needs them.
The 98 KB that is the server
The two chunks at 15.7 and 16.7 per cent carry no library copyright, no recognisable vendor name, and no app strings. Their string tables give them away:
"prerender" "prerender-ppr" "prerender-runtime" "prerender-client"
"prerender-legacy" "generate-static-params" "unstable-cache"
"private-cache" "validation-client" "set-cookie"
"Expected workStore to be initialized"
"unstable_rethrow" "unauthorized" "forbidden"
That is the framework's work-store and rendering-mode machinery, 97,861 characters of it, shipped to a browser that will never prerender anything or generate a static param. About a sixth of it executes, which will be the client half of the same modules.
I am not filing that as a bug against anyone. It is what a single bundle graph produces when server and client code share modules, and no amount of care in our own code changes it. It is worth knowing that it is there, though, because it is 8.6 per cent of the page's JavaScript and it is invisible in every dependency list we could print.
The chunk that is waiting for a click
2x1jr4v8dirou.js is the Radix dropdown, 26,678 characters, 8.1 per cent executed on load. That 8 per cent is the module body and the component definitions. The rest is the behaviour of a menu that has not been opened.
So I opened it. Profiler.takePreciseCoverage resets the counters, which makes the second call a delta: everything that ran since the first one.
click the header "Resources" menu, then scroll to the bottom
delta: 43,086 characters executed
2x1jr4v8dirou.js 15,120 (the dropdown, previously 2,148)
3m0h3pqana6sj.js 9,807 (react-dom, rendering the menu)
0v56fs-1wokxh.js 7,099
17zhs14x56jgd.js 6,939
One click on one menu runs seven times more of that chunk than the entire page load did. This is the honest defence of the 49 per cent figure: unused is not the same as useless, and a coverage number taken at load is a snapshot of one visitor who did nothing.
The counter-argument is just as honest: on a page of prose, most visitors do nothing, and they still paid for it.
Method notes
- Counts are characters, not bytes, because coverage ranges are source offsets and I size each script by fetching it and taking
text().length. Our chunks are ASCII, so the two agree closely. The cross-check: the earlier byte census of this page said 19 chunks and 1,244,171 bytes, and one of those 19 is anoModulecore-js build at 112,594 bytes that modern browsers never fetch. 1,244,171 minus 112,594 is 1,131,577, against the 1,131,567 characters coverage saw. Ten apart. - "Executed" is computed by filling a byte array per script, outer ranges first and inner ranges last, so the innermost range wins. That is the same rule the DevTools Coverage panel applies.
- Unlike CSS coverage, JavaScript coverage in DevTools is trustworthy: the ranges are function bodies, not
@layerwrappers. If you open the Coverage panel on one of our pages you should land within a point or two of these numbers.
Check it yourself
Open DevTools, Coverage panel, press the reload-and-record button, and load:
- /faq, prose only, about 49 per cent
- /legal/privacy, the smallest bundle we ship, about 52 per cent
- /tools/round-planner, the most interactive public page, about 49 per cent, and one extra 9,786 character chunk that is the tool itself and runs at 94 per cent
Then open the Resources menu in the header and watch the unused bar move.
Top comments (0)