DEV Community

Timevolt
Timevolt

Posted on

The Next.js 14 Awakens: Server Components Rise

The Quest Begins (The "Why")

Honestly, I was tired of shipping React apps that felt like they were dragging a backpack full of rocks. Every time I added a new feature, the bundle grew, the first paint slowed, and users on slower connections stared at a blank screen while the JavaScript hustled to hydrate. I kept asking myself: Why do we have to send the whole UI to the browser just to render a bit of static markup?

One late‑night debugging session (you know the kind—coffee cold, eyes bleary) I stumbled upon a tweet about React Server Components. The idea was simple yet radical: let the server render pieces of UI that never need client‑side JavaScript, and stream the rest as needed. My inner nerd did a little happy dance. If this could work in Next.js, maybe I could finally slay that bundle‑size dragon and give users a snappy experience without rewriting everything from scratch.

The Revelation (The Insight)

Here’s the thing: Next.js 14 didn’t just add a new flag; it rethought the routing and rendering contract. Server Components are now the default for files inside the app directory, and you get to decide per‑file whether a component runs on the server or the client. The magic lives in three concepts:

  1. Server‑only components – No bundle sent to the browser. They can talk directly to databases, file systems, or any server‑side API.
  2. Client components – Marked with "use client"; they get bundled and hydrated like traditional React.
  3. Streaming SSR – The server can send HTML chunks as soon as they’re ready, letting the browser start painting while the rest of the tree streams in.

When I first saw a server component fetch data from PostgreSQL and render a list without any client‑side fetch, I felt like I’d discovered a secret level in a classic game. The server did the heavy lifting, the client stayed light, and the user got content instantly. That’s the shift: move data‑fetching and rendering work to where it belongs—the server—and let the browser focus on interactivity.

Wielding the Power (Code & Examples)

Let’s look at a practical example: a blog homepage that shows a list of posts. In the old pages‑router world, we’d fetch data in getServerSideProps and pass it down to a client component that still needed JavaScript to render the list. With the app router and server components, we can cut the client JavaScript out entirely for the list itself.

Before – The Struggle (pages router)

// pages/index.tsx
import type { GetServerSideProps } from 'next';
import PostList from '@/components/PostList';

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await fetch('https://my-blog-api.com/posts');
  const posts = await res.json();
  return { props: { posts } };
};

export default function Home({ posts }: { posts: Post[] }) {
  return <PostList posts={posts} />;
}

// components/PostList.tsx
export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map(p => (
        <li key={p.id}>
          <h2>{p.title}</h2>
          <p>{p.excerpt}</p>
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • PostList is a client component (by default) → its code ships to the browser even though it never interacts with state or events.
  • The fetch happens in getServerSideProps, but we still sent the component code down for hydration.

After – The Victory (app router, server component)

// app/page.tsx   <-- server component by default
import { PostCard } from '@/components/post-card'; // client component

export default async function Page() {
  // ✅ Server‑only: can hit DB, filesystem, private APIs
  const res = await fetch('https://my-blog-api.com/posts', {
    // nextjs automatically caches this request per‑route
    next: { revalidate: 60 },
  });
  const posts = await res.json();

  return (
    <section>
      <h1>Latest Posts</h1>
      {/* We keep the interactive card as a client component */}
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <PostCard post={post} />
          </li>
        ))}
      </ul>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/post-card.tsx  <-- marked as a client component
'use client';

import { useState } from 'react';

export function PostCard({ post }: { post: Post }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <article style={{ border: '1px solid #eee', padding: '1rem', marginBottom: '1rem' }}>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Show less' : 'Show more'}
      </button>
      {expanded && <p>{post.content}</p>}
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

What changed?

  • app/page.tsx is a server component (no "use client" needed). It fetches data directly and returns JSX. No JavaScript from this file ends up in the client bundle.
  • Only PostCard needs interactivity, so we mark it with "use client" and ship just that small piece.
  • The server streams the HTML for the list as soon as the first post arrives, giving users content instantly while the rest of the page loads in the background.

Traps to Avoid

  1. Accidentally making a server component a client component – If you add "use client" at the top of a file that only does data fetching, you’ll drag unnecessary code into the bundle. Keep the marker only where you truly need hooks, state, or event listeners.
  2. Assuming server components can use browser APIs – Things like window, document, or navigator are undefined. If you need them, move that logic to a client component or wrap it in a useEffect that runs only on the client.

Why This New Power Matters

With server components, the line between “static site” and “dynamic app” blurs in the best way. You can now:

  • Deliver instant content – No more flash of empty state; the server sends fully rendered markup for the parts that never change.
  • Trim the JavaScript payload – Only the interactive bits travel to the browser, which means faster TTI (time to interactive) and happier users on low‑end devices.
  • Simplify data fetching – No need for getServerSideProps or getStaticProps abstractions; just await fetch inside the component and let Next.js handle caching and streaming.

Imagine building a dashboard where the sidebar, navigation, and static widgets are server‑only, while the charts and filters that react to user input stay client‑side. You get the performance of a static site with the richness of a full‑blown React app—all without a complex micro‑frontend architecture.

Your Turn

Ready to give it a spin? Take a page that currently fetches data in getServerSideProps and move that fetch into a server component inside the app folder. Keep only the truly interactive pieces as "use client" components. Notice how the bundle size drops and the perceived speed climbs.

What’s the first feature you’ll move to the server side? Drop your thoughts in the comments—I’d love to hear how this new power changes your workflow! Happy coding! 🚀

Top comments (0)