Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Two of our 1,029 page files are client components, and two pages 23 KB apart in HTML ship byte-identical JavaScript

I counted every 'use client' in this repository, expecting the answer to be "most of them" and to feel bad about it. directory files 'use client' components/ 1,062 586 lib/ 1,407 75 app/ 1,148 11 __tests__/

I counted every 'use client' in this repository, expecting the answer to be "most of them" and to feel bad about it.

directory files 'use client'
components/ 1,062 586
lib/ 1,407 75
app/ 1,148 11
__tests__/ 507 0
scripts/ 18 0
total 4,142 672

16 percent, which is unremarkable. The number that is not unremarkable is in the app/ row. This site has 1,029 page.tsx files, and exactly two of them are client components.

Both are behind the login, and both are a screen that builds its view in the browser out of a single fetch rather than rendering it on the server. Every other page in the repository, including the 991 public ones, is a server component.

What that buys, measured from outside

Two employer guides. One is a 130-line page file, the other 410 lines, and the rendered difference is about 23 KB of HTML. Fetch each page, collect its <script src> tags, and add up the bytes:

page HTML chunks JavaScript
/employers/eversource 138,471 19 1,332,041
/employers/australian-defence-force 161,150 19 1,332,041
/pricing 146,033 19 1,332,549

Byte-identical JavaScript, from the same 19 chunk URLs, for two pages with materially different content. /pricing, a completely different page, is 508 bytes apart.

That is the whole argument for server components on a content site, in one table. Across 211 employer guides there are 148,824 lines of page source, and adding another one costs zero JavaScript. Prose is not a dependency.

For contrast, a provider hub page, which does have an interactive component on it:

page HTML chunks JavaScript
/games/hirevue 362,047 21 1,407,456

Three extra chunks of 29,998, 46,027 and 25,256 bytes, one chunk dropped, net 75,415 more. You can see the cost of interactivity arrive as individual files, which is what makes this worth measuring rather than reasoning about.

The honest half: the floor is 1.33 MB

A page with no interaction beyond a navigation menu still ships 1.33 MB of JavaScript, and nothing about server components fixes that, because the floor is set by whatever the shared chrome needs on every route: a header with a dropdown, theme handling that has to run before paint, analytics, an icon runtime, an error boundary.

So the split is doing its job and the budget is still large. Those are two separate problems, and the census above only speaks to the first. The useful property is not that the number is small, it is that the number does not grow when the site does. 991 pages, one floor.

The 16 files that cannot be imported by a client

import 'server-only';

16 modules carry that marker. It is a package whose only job is to fail the build if a client component reaches it, so a bad import becomes a compile error rather than a leak. The list is short and, read as a group, it is an inventory of what we consider dangerous:

  • lib/db/db.ts, the database client itself
  • the three Supabase clients, including the service-role one
  • four calendar modules, which hold third-party refresh tokens
  • four application-tracker data modules
  • two admin data modules
  • the school email-domain index and the support screenshot storage

The strongest of those is the first. Marking the db handle means no client component anywhere in 4,142 files can import the thing that talks to Postgres, which is a guarantee no amount of code review gives you.

The gap is the obvious one: 16 markers against 1,407 files in lib/. The great majority of server-side modules are server-side by convention and by nobody having made a mistake yet. The marker is applied where somebody judged the blast radius worth a build error, and "dangerous" is a judgement that has to keep being made every time a module is added. It is a guard rail at the cliff edge, not a fence around the field.

A related trap worth knowing: a 'use client' module under lib/ looks like library code and is browser code. 68 of those 75 belong to the practice exercises themselves, which are interactive by definition, but the directory name tells you nothing, and an import from one of them into something server-side will silently drag a component tree into a bundle. Directory conventions do not survive a refactor; the server-only import does.

See it

The byte comparison reproduces in two commands. Collect the script URLs from a page and sum their sizes:

curl -s https://cogniprep.app/employers/eversource | grep -o '<script src="[^"]*"'

Do the same for cogniprep.app/employers/australian-defence-force, which is the longest employer guide we publish, and compare the two lists. The chunk filenames are identical. Then do it for cogniprep.app/games/hirevue and three new filenames show up.

If you run a content-heavy app, that diff is the fastest way to find out whether your pages are actually server components or whether something innocuous near the top of the tree has quietly turned a route into a client bundle. The answer is in the script tags, not in the source.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.