If you have not been following React closely for the past two years, one thing changed under your feet. In the Next.js App Router, every component is a Server Component by default. Not a client component that can also render on the server, but a server component until you explicitly say otherwise.
Most articles on this topic open by explaining what Server Components are. This one does not. This one is about the decision: how to choose server or client for a given component, what exactly breaks when you get it wrong, and what any of it means for a business that is buying a website and does not care about React. The examples come from the code behind this site, so you can open them and check.
What actually changed
The old model was simple and everyone knew it. The browser downloaded JavaScript, React booted up, the component rendered empty, called for data in a useEffect, and re-rendered once the data arrived. In the meantime, the user watched a spinner.
That model carried three costs we treated as facts of life for years:
- Every piece of code that fetched and shaped data had to ship to the browser, including the libraries for parsing, formatting and validating it.
- Data arrived after the JavaScript did. Bundle first, then request, then content. Three steps in sequence, not in parallel.
- Both Google and the user saw an empty page first.
The new mental model is about moving a boundary. Instead of asking "how do I get data into this component", you ask "where should this component run at all". A Server Component renders on the server, sends the finished result to the browser, and ships none of its own code. Not minified, not deferred. None.
That is the whole difference. Not faster JavaScript, but no JavaScript.
The boundary is declared with a 'use client' directive at the top of a file. Everything above it runs on the server; everything below it, plus everything such a file imports, goes to the browser. The directive is not a property of a component, it is a boundary in the tree, and that detail is where most mistakes happen.
A concrete example: the article listing on this site
Take the page you are on right now. The listing in our From Practice section reads nine MDX files from disk, parses their frontmatter, computes read time, and renders cards with a category filter.
Here is how it would look the old way:
'use client'
import { useEffect, useState } from 'react'
export function BlogIndex() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then((r) => r.json())
.then((data) => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <Spinner />
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>{post.title}</li>
))}
</ul>
)
}
It works. But it means an API endpoint to build and maintain, loading state, a spinner, an empty first render, and an extra round trip after React boots.
Here is what the site actually does today:
// app/(sk)/blog/page.tsx
// No 'use client'. This file never reaches the browser.
import { getAllPosts, getCategories } from '@/lib/blog'
import { BlogIndexPage } from './page-client'
export default function Page() {
const posts = getAllPosts()
// Article bodies get dropped here. Only what a card needs
// to render crosses into the browser.
const cards = posts.map(({ slug, title, description, publishDate, category, readTime }) => ({
slug, title, description, publishDate, category, readTime,
}))
return <BlogIndexPage posts={cards} categories={getCategories(posts)} locale="sk" />
}
No useEffect, no loading, no API endpoint. getAllPosts reaches straight into the file system through fs and gray-matter, which is unthinkable in a browser and completely ordinary on a server.
The comment in the middle is the important part. Those nine articles are roughly 68 kB of MDX source on disk, close to ten thousand words in total. The listing needs only a title, excerpt, date, category and read time. The server drops the bodies and sends nothing but card metadata across the boundary. With client side fetching you would either ship everything and trim it in the browser, or build a second endpoint returning a shortened version. Here it is one map.
The rest, meaning the category filter and the animations, stays on the client in page-client.tsx. That is the pattern in full: a server shell with client islands. Not "the whole page is server" or "the whole page is client", but a boundary pushed as far down the tree as it will go.
The rule of thumb that keeps working for us: 'use client' belongs on the smallest possible component, never on the page. Put it on the page and you drag everything that page imports below the client boundary, including things that never needed interactivity at all.
When not to reach for a Server Component
The server is not the default answer to everything. There are places where a client component wins outright, and trying to avoid one is wasted effort.
Stateful interactivity. Anything using useState, useReducer or event handlers. The category filter on the article listing, opening and closing an FAQ item, the language switcher. These respond to clicks, and clicks happen in the browser.
Browser APIs. window, localStorage, matchMedia, IntersectionObserver. None of them exist on the server. Our useMobile hook is built on matchMedia, so everything using it is necessarily a client component.
Scroll driven animation. The entire scroll architecture of this site, meaning Framer Motion and useScroll, is client side and cannot be anything else. The server has no idea where you are on the page.
Post render effects. Anything that has to run after an element is in the DOM.
So what does getting it wrong look like? Two typical cases. The first is loud: put 'use client' on a component that reads from the file system and the build fails, because fs does not exist in a browser. That is a good failure. You notice immediately.
The second is quiet, which makes it worse. You forget 'use client' on a component with an onClick, or you put the directive too high up the tree. In the first case the page renders, the button is visible, and nothing happens when you press it. In the second nothing breaks at all, you just quietly add code to your bundle that has no business being there. Nobody complains. The site is simply slower and nobody knows why.
This is why bundle size is worth checking after every build rather than twice a year. On this site the shared baseline is 115 kB and the article listing lands at 192 kB. When one of those numbers jumps, we can tie it to a specific change while that change is still fresh.
What this means for client projects
This is the part that matters to a business rather than a developer.
Faster first content. The user does not wait for JavaScript in order to see text. The HTML arrives finished. On a mobile connection, where downloading and parsing a bundle takes seconds, that is the difference between "the site loaded" and "the site is loading".
Less code to download. Every library that stays on the server is code your customer does not pay for in data or in time. The clearest illustration comes from our 3D experience: for visitors with reduced motion enabled, the scene is never loaded at all, which saves them 1461 kB of JavaScript. Same principle, different scale.
SEO without the caveats. Google does execute JavaScript, but with a delay and not always reliably. Content that sits in the HTML from the first response never has that problem. This entire site is statically generated, so a crawler receives a finished page.
The numbers back it up. For Adrilex this approach produced a Lighthouse performance score of 100, with 94 and above across the other categories. Not because we squeezed every last kilobyte out of the images, but because a large share of the work never reached the browser in the first place.
There is one thing worth taking away from all of it. Server Components do not mean you should rewrite your site. They mean the default answer to "where should this code run" has flipped from client to server, and that shipping code to the browser is now a decision you should be able to justify.
If you want to know what any of this is worth for your specific site, take a look at our web development work or the portfolio. And if you have a site that loads slowly and nobody can tell you why, get in touch. We will look at how much JavaScript it actually ships and how much of that does not need to be there. Sometimes the answer is that it is fine and the problem lies elsewhere. That is useful to know too.
