← Back to DevBytes

Server-Side Rendering with Babel: SSR, SSG, ISR

Introduction to Server-Side Rendering with Babel

Modern web development relies heavily on rendering strategies to balance performance, SEO, and user experience. While Client-Side Rendering (CSR) became the default with the rise of Single Page Applications (SPAs), the pendulum has swung back toward the server. Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) are now industry standards.

Babel, a highly configurable JavaScript compiler, plays a crucial role in this ecosystem. It allows developers to transpile modern JavaScript and JSX into compatible code that can run seamlessly on Node.js servers and browsers alike. This tutorial explores how to leverage Babel to implement SSR, SSG, and ISR from scratch.

What is SSR, SSG, and ISR?

Why Babel for Server-Side Rendering?

While frameworks like Next.js abstract away the complexity of these rendering strategies, understanding how to build them with Babel gives you ultimate control. Babel allows you to:

Setting Up the Environment

To begin, we need a basic Node.js environment with React, Express, and Babel. Initialize a new project and install the necessary dependencies.

npm init -y
npm install express react react-dom
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react @babel/register

Configuring Babel for SSR

Create a .babelrc file in the root of your project. This file tells Babel to use the environment and React presets to transpile our code.

{
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "current" } }],
    "@babel/preset-react"
  ]
}

Next, create a simple React component named App.js that we will use across all three rendering methods.

import React from 'react';

export default function App({ data }) {
  return (
    <div>
      <h1>Rendering Strategies with Babel</h1>
      <p>Data: {data}</p>
    </div>
  );
}

Implementing SSR (Server-Side Rendering)

For SSR, we will create an Express server that uses Babel to transpile our code on the fly using @babel/register. The server will render the React component to an HTML string on every request.

Creating the Express Server

Create a file named server.js. At the very top of the file, we must require @babel/register so that all subsequent import statements are transpiled correctly.

// server.js
require('@babel/register');

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('/', (req, res) => {
  // Fetch dynamic data (simulated)
  const dynamicData = `Current time is ${new Date().toLocaleTimeString()}`;

  // Render the React component to a string
  const appHtml = renderToString(<App data={dynamicData} />);

  // Inject the string into an HTML template
  const html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>SSR Example</title>
    </head>
    <body>
      <div id="root">${appHtml}</div>
    </body>
    </html>
  `;

  res.send(html);
});

app.listen(3000, () => {
  console.log('SSR Server is running on http://localhost:3000');
});

Run the server using node server.js. Every time you refresh the page, the server executes renderToString and sends fresh HTML to the client.

Implementing SSG (Static Site Generation)

SSG shifts the rendering process to build time. Instead of an Express server, we write a Node.js script that renders the React component once and writes the output to an HTML file.

Pre-rendering Pages at Build Time

Create a file named build.js. This script will use renderToStaticMarkup (which is slightly faster than renderToString as it omits React's internal attributes used for hydration).

// build.js
require('@babel/register');

import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import fs from 'fs';
import path from 'path';
import App from './App';

// Static data fetched at build time
const staticData = "This data was baked in at build time.";

const appHtml = renderToStaticMarkup(<App data={staticData} />);

const html = `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>SSG Example</title>
  </head>
  <body>
    <div id="root">${appHtml}</div>
  </body>
  </html>
`;

const outputDir = path.resolve(__dirname, 'public');
if (!fs.existsSync(outputDir)) {
  fs.mkdirSync(outputDir);
}

fs.writeFileSync(path.join(outputDir, 'index.html'), html);
console.log('Static site generated successfully in /public/index.html');

Run node build.js. You can now serve the public directory using any static file server, such as npx serve public. The page will load instantly, but the data will never change unless you run the build script again.

Implementing ISR (Incremental Static Regeneration)

ISR combines the speed of SSG with the freshness of SSR. We serve a static file, but we track its age. If the file is older than a specified threshold (stale-while-revalidate), we serve the stale file immediately but trigger a background rebuild.

Stale-While-Revalidate Strategy

Modify your server.js to implement ISR. We will check the file's modification time and regenerate it in the background if it's stale.

// server.js (ISR Implementation)
require('@babel/register');

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import App from './App';

const app = express();
const REVALIDATE_SECONDS = 10;
const htmlFilePath = path.resolve(__dirname, 'public', 'index.html');

function generateStaticPage() {
  const dynamicData = `Generated at ${new Date().toLocaleTimeString()}`;
  const appHtml = renderToStaticMarkup(<App data={dynamicData} />);
  const html = `<!DOCTYPE html><html><body><div id="root">${appHtml}</div></body></html>`;
  
  if (!fs.existsSync(path.dirname(htmlFilePath))) {
    fs.mkdirSync(path.dirname(htmlFilePath), { recursive: true });
  }
  fs.writeFileSync(htmlFilePath, html);
}

// Initial generation
generateStaticPage();

app.get('/', (req, res) => {
  if (fs.existsSync(htmlFilePath)) {
    const stats = fs.statSync(htmlFilePath);
    const ageInSeconds = (Date.now() - stats.mtimeMs) / 1000;

    // Serve the stale file immediately
    const html = fs.readFileSync(htmlFilePath, 'utf-8');
    res.send(html);

    // If stale, regenerate in the background
    if (ageInSeconds > REVALIDATE_SECONDS) {
      console.log('Page is stale, regenerating in background...');
      generateStaticPage();
    }
  } else {
    generateStaticPage();
    res.send(fs.readFileSync(htmlFilePath, 'utf-8'));
  }
});

app.listen(3000, () => {
  console.log('ISR Server is running on http://localhost:3000');
});

When you run this server and refresh the page, you will initially see the same time. After 10 seconds, your next refresh will still show the old time (served instantly), but the server will log that it is regenerating. On the *subsequent* refresh, you will see the newly generated time.

Best Practices

Conclusion

Understanding the mechanics behind SSR, SSG, and ISR is essential for building modern, high-performance web applications. By leveraging Babel to transpile JSX and modern JavaScript for Node.js, developers can manually orchestrate these rendering strategies without being locked into a specific framework. While tools like Next.js provide these features out of the box, building them from scratch demystifies the process, giving you the knowledge to debug complex rendering issues and optimize your application's architecture at a granular level.

— Ad —

Google AdSense will appear here after approval

← Back to all articles