Skip to content

A page with no JavaScript

The default is none. A client component is how a page asks for it.

Updated View as Markdown

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 clientJs in build-report.json is null when 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:

vite.config.tsts
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 bytes

It 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close