DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Half the JavaScript on our static FAQ page never runs, and 98 KB of it is the framework's server code

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%
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 a noModule core-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 @layer wrappers. 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)