30 Days of JavaScript: Server-Side Rendering with JavaScript, Day 27

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: Server-Side Rendering with JavaScript, Day 27

Boost web performance and SEO with Server-Side Rendering (SSR) in Next.js. Learn SSR basics, set up Next.js, and build a fully functional blog with caching.

Day 27 of the 30-day JavaScript journey. So far most of what we’ve built renders in the browser: the server ships a near-empty HTML shell, JavaScript boots up, and only then does the page fill in. Today we flip that around. With Server-Side Rendering (SSR) the server does the rendering work and sends real HTML on the first response. We’ll cover what SSR actually buys you (and what it costs), set up Next.js, and build a small blog that renders on the server.

Table of Contents

Understanding Server-Side Rendering (SSR)

What is Server-Side Rendering?

With SSR, the server generates a page’s HTML for each request and sends that fully-formed markup to the browser. The user sees content on the first response instead of waiting for JavaScript to download, parse, and build the DOM. That’s the opposite of client-side rendering (CSR), where the browser gets a bare HTML file plus a JavaScript bundle and paints the page itself.

One thing to be clear about: SSR isn’t the end of the story. The server-rendered HTML still needs to be hydrated on the client, meaning React attaches its event handlers to the existing markup so the page becomes interactive. So you’re not skipping the JavaScript, you’re changing when the user sees content.

Benefits of Server-Side Rendering

SSR earns its keep in a few real ways:

  • Faster first paint: The HTML arrives already rendered, so the browser can show content sooner. This mainly helps First Contentful Paint, the moment the user first sees something real.
  • Content that doesn’t depend on JS running: Anything that can’t or won’t execute your JavaScript still gets the full page. That covers users on flaky connections, some crawlers, and link-preview bots.
  • SEO for crawlers that don’t render JavaScript: Googlebot does execute JavaScript, so a well-built CSR app can rank fine. But not every crawler or social scraper runs JS, and server-rendered HTML is the safest way to guarantee they see your content and metadata.

Honest caveat: SSR is not automatically an accessibility win. A screen reader reads the rendered DOM either way, so a CSR app that renders correctly is just as accessible. What SSR helps with is getting meaningful content into that first response.

SSR vs. CSR vs. Static Site Generation (SSG)

Three approaches, three trade-offs:

  • CSR: The browser renders everything from JavaScript. Slower first paint, but great for highly interactive, app-like screens where SEO doesn’t matter (a logged-in dashboard, say).
  • SSR: Rendered per request on the server. Fast first paint and reliable HTML for crawlers, at the cost of doing that render work on every request, which is real server load.
  • SSG: Pages are pre-rendered once at build time and served as static files. Fastest to serve and cheapest to host, but the content is frozen until the next build, so it fits pages that don’t change per request.

Worth knowing: this line has blurred. Since Next.js 13, the App Router and React Server Components let you stream server-rendered HTML in pieces and mix server and client components on the same page, instead of choosing one whole-page strategy. We’ll use the older Pages Router here because it’s the clearest way to see SSR on its own, but reach for the App Router on new projects.

Setting Up an SSR Framework

Choosing an SSR Framework

You can wire up SSR by hand with a Node server and React’s renderToString, but almost nobody does that anymore because a framework handles the awkward parts (routing, hydration, bundling). The two you’ll hear about most:

  • Next.js: A React framework with SSR, static generation, API routes, and code splitting built in.
  • Nuxt: The equivalent for Vue, with the same SSR and SSG story.

We’ll use Next.js here since it’s the most widely deployed of the two.

Installing Next.js

You’ll need Node.js and npm installed. Then scaffold a project:

Installation:

Bash
npx create-next-app@latest my-ssr-app
cd my-ssr-app
npm run dev

That creates a project in my-ssr-app and starts the dev server. Open http://localhost:3000 to see it.

Understanding the Next.js File Structure

In the Pages Router, a couple of directories carry meaning:

  • pages/: Each file here becomes a route automatically. This is where the routing magic lives.
  • pages/api/: Files in here become API endpoints, not pages.
  • public/: Static assets like images and fonts, served from the root.

A components/ folder is a common convention too, but that one is just for your own organization, Next.js doesn’t treat it specially. The routing tied to pages/ is what does the work.

Creating Your First SSR Page

Let’s make a page that renders on the server. Create index.js in pages/:

Example:

JS
import React from 'react';
/**
 * A simple server-side rendered page in Next.js.
 */
const Home = ({ message }) => {
  return (
    <div>
      <h1>{message}</h1>
      <p>Welcome to your first SSR page with Next.js!</p>
    </div>
  );
};
export async function getServerSideProps() {
  return {
    props: {
      message: 'Hello from the server-side!',
    },
  };
}
export default Home;

The key piece is getServerSideProps. Export that function from a page and Next.js runs it on the server for every request, then passes what it returns as props. That’s the switch that turns a page into an SSR page.

Implementing Dynamic Routing in SSR

Dynamic routes work the same way. A file named [id].js in pages/ matches any single path segment and hands you the value. Let’s render a user profile by ID:

Example:

JS
import React from 'react';
/**
 * Renders user profile information based on dynamic routing.
 */
const UserProfile = ({ user }) => {
  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
};
export async function getServerSideProps(context) {
  const { id } = context.params;
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const user = await res.json();
  return {
    props: {
      user,
    },
  };
}
export default UserProfile;

The [id] in the filename becomes context.params.id. We fetch that user on the server and render the profile before the response goes out. In real code you’d also handle the case where the fetch fails or the ID doesn’t exist, but this keeps the shape clear.

Implementing SSR in a Sample Project

Planning the Sample Project

Let’s build something small but complete: a blog with a list of posts and a detail page for each one. Both render on the server, so the content and metadata are in the HTML from the first byte.

Setting Up the Blog Platform

Two files in pages/:

  • index.js: The list of posts.
  • post/[id].js: A single post’s details.

Example of index.js:

JS
import React from 'react';
import Link from 'next/link';
/**
 * Home component for displaying blog posts.
 */
const Home = ({ posts }) => {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link href={`/post/${post.id}`}>
              <a>{post.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};
export async function getServerSideProps() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await res.json();
  return {
    props: {
      posts,
    },
  };
}
export default Home;

This fetches the post list on the server and renders it, with each title linking to its detail page. One note if you’re on a recent Next.js: the nested <a> inside Link shown here is the older style. Since Next.js 13, Link renders the anchor for you, so you’d write the label directly inside it and drop the inner tag.

Example of post/[id].js:

JS
import React from 'react';
import Head from 'next/head';
/**
 * Post component for displaying a single blog post.
 */
const Post = ({ post }) => {
  return (
    <div>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.body.substring(0, 160)} />
      </Head>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
};
export async function getServerSideProps(context) {
  const { id } = context.params;
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await res.json();
  return {
    props: {
      post,
    },
  };
}
export default Post;

This is where SSR pays off for content sites. The title and description go into <Head> and render on the server, so a crawler or a social scraper sees per-post metadata in the initial HTML without running any JavaScript.

Optimizing Performance with Caching

Because getServerSideProps runs on every request, that per-request work is the cost of SSR. If a page doesn’t change often, cache it. You can cache the data in something like Redis, or set HTTP cache headers so a CDN can serve repeat hits. Here’s the data-caching version:

Example:

JS
export async function getServerSideProps(context) {
  const cacheKey = `post-${context.params.id}`;
  const cachedPost = await redis.get(cacheKey);
  if (cachedPost) {
    return {
      props: {
        post: JSON.parse(cachedPost),
      },
    };
  }
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${context.params.id}`);
  const post = await res.json();
  await redis.set(cacheKey, JSON.stringify(post), 'EX', 60 * 60); // Cache for 1 hour
  return {
    props: {
      post,
    },
  };
}

On a cache hit we skip the API call entirely; on a miss we fetch, store it for an hour, and move on. For content that’s static per request, though, ask whether you even need SSR: Static Site Generation renders it once at build time and is cheaper to serve.

Conclusion

That’s SSR in practice. The server renders real HTML per request, the user sees content faster, and crawlers get markup they can read without running JavaScript. The trade-off is honest work on every request, which is why caching (or reaching for SSG when the content allows) matters. And remember it’s not free: the client still hydrates the page to make it interactive.

If you take one thing away, it’s that CSR, SSR, and SSG aren’t a ranking of best to worst, they’re tools for different jobs. And on new Next.js projects, the App Router with React Server Components is where these ideas are heading.

What’s Next?

Day 28 is Web Workers and Multithreading, where we move heavy work off the main thread so the UI stays responsive.

Next: 30 Days of JavaScript: Web Workers and Multithreading, Day 28

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top