LUMEN&HOUSEStart a project
Engineering

Why we ship Server Components by default

By the engineering teamAug 20266 min read

Every framework has a default, and the default wins. Not because it is always the right call, but because it is what gets reached for when the ticket is small, the afternoon is late, and the deadline is Friday. Pick the default carefully and a codebase drifts toward good architecture on its own. Pick it carelessly and you spend the next two years arguing with it.

Ours is server-first. Components render on the server, ship no JavaScript to the browser, and stay that way unless something genuinely needs to be interactive. Interactivity is the opt-in.

A client component is a purchase

Marking a component "use client" is not a rendering preference. It is a commitment to ship that component's code, its imports, and its imports' imports, then hydrate all of it in the browser. The date library, the markdown renderer, the charting package — none of that was free, and your users pay the invoice on every visit.

The pattern we see in inherited codebases is almost always the same: a directive added at the top of a file to unblock one onClick, and three hundred kilobytes following it downstream. Nobody decided to ship that. It accumulated.

Nobody decides to ship a slow app. They decide forty small things that each seemed harmless.

Data belongs next to the thing that renders it

The quiet win of server rendering is not bytes, it is data flow. You await the query in the component that needs the result. No fetching effect, no loading flag, no request waterfall where the browser learns what to ask for only after the previous answer arrives.

app/invoices/page.tsx
// Runs on the server. Ships nothing to the browser.
export default async function Page() {
  const invoices = await db.invoice.findMany({
    where: { status: "open" },
    orderBy: { dueDate: "asc" },
  })

  // The table is a client island only because it sorts and filters.
  return <InvoiceTable rows={{invoices}} />
}

The database credential never leaves the server. The query is not an API endpoint somebody has to secure separately. And the page arrives as HTML, which is still the fastest thing a browser knows how to display.

Where we do reach for the client

  • Anything holding state a user can see change — filters, sorts, optimistic updates, multi-step forms.
  • Anything that needs the pointer, the keyboard, or an animation frame.
  • Anything reading browser-only APIs: storage, media devices, geolocation.
  • Third-party widgets that were never going to render anywhere else.

The discipline is to push that boundary as far down the tree as it will go. A page does not become interactive; one leaf does. Everything above it stays static HTML.

What this does not fix

Server components are not a performance strategy by themselves, and it is worth being blunt about that. Move the work to the server and you can still build a waterfall — it is simply a waterfall you no longer see in the network tab. Slow queries are still slow. A component that awaits four sequential calls is still four sequential calls.

So the default sits alongside the same checks as everything else: a budget per route, enforced in CI, as described in performance budgets that actually hold. The architecture makes the fast thing easy. The build gate is what makes it stay that way.

That is the whole argument. Not that client components are bad — they are how the interesting parts of the web get built — but that they should be a decision someone makes on purpose, and the default should be the thing that costs your users nothing.

Back to Thinking

Start here

Let's build the thing they can't stop using.

Send the brief. A senior engineer reads every one and comes back with an architecture, a plan and a quote written against your actual scope — usually within a day.