There is no JavaScript on a page until something on it needs some. A marketing page, a changelog, an article, a policy: the build stores them as HTML and stops. No bootstrap script, so no React, no Flight client, no router.
○ /about no js
no client components, so ships no javascript; stylesheet inlined"use client" is the opt-in. A route that freezes whole, renders none of
your client components, and has no server action in its tree ships nothing.
Put a client component in it — a counter, a <Link>, an update prompt — and
that page has the runtime, because there is now something for the runtime to
do. That is the whole rule, and there is no switch beside it in either
direction, on purpose:
- Nothing can need the runtime without a client component or an action in its tree, so there is nothing an “on” switch could say that the tree does not already say.
- A page that must stay this way is an assertion, not a setting: the route’s
clientJsinbuild-report.jsonisnullwhen it shipped none, and a CI step can hold it there.
The check reads the rendered tree, not the source, so a client component
reached through a shared layout counts. Most pages under a layout with a
<Link> in its header keep the runtime; a route group with its own plain
layout drops it. That is the honest shape of it: the pages that can be
nothing are nothing, and the page with a nav is a page with a nav.
What a page without the runtime still does
Its stylesheet is inlined into the document when it is small on the wire (at most 10 kB gzipped), so the first paint waits on no request but the document itself. The build says so on the route’s line. Fonts are the one thing left to you, because only you know which face the first paint needs — the fonts guide shows the two-line preload.
The default is not always the right call. A site of many small pages — a blog, documentation — may prefer one stylesheet the browser caches once over the same 4 kB repeated in every document, and a sheet just over the cap may be worth inlining anyway. It is one setting on the plugin:
rscKit({ inlineStylesheets: 'auto' }) // the default: when at most 10 kB gzipped
rscKit({ inlineStylesheets: false }) // never — keep the link, cache one file
rscKit({ inlineStylesheets: true }) // always, whatever the size
rscKit({ inlineStylesheets: 24_000 }) // your own cap, in gzipped bytesIt still registers the service worker when the app has one: the runtime’s one-line registration is inlined in its place, so a visitor who lands here first gets the worker the second visit is for. What such a page cannot show is the update prompt — that is a client component, and a page that uses it has the runtime.
Navigation is unchanged in both directions. A page with no client
component has no <Link>, so its anchors were already full loads. A <Link>
elsewhere that points at it still fetches its flight payload and swaps the
segment, because that payload is written from the render with the runtime.
Only a page stored whole
With no client runtime there is nothing to fill a Suspense hole after the
shell arrives, so a partial prerender always keeps it, and so does a page
rendered per request. The line is what the runtime would do: hydrate a
client component, submit a <form action={serverFn}>, fill a hole. A page
with none of those is HTML, and is stored as HTML.