← Back to DevBytes

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

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

Server-side rendering has become a cornerstone of modern web development, enabling faster initial page loads, better SEO, and improved user experiences. When paired with SWC (Speedy Web Compiler), a super-fast TypeScript/JavaScript compiler written in Rust, the rendering pipeline becomes dramatically faster. This tutorial explores how to leverage SWC for three popular rendering strategies: Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR).

What Is SWC and Why Does It Matter?

SWC is a Rust-based platform for web development tooling. It can compile, bundle, minify, and transform JavaScript and TypeScript at speeds up to 20x faster than Babel. Frameworks like Next.js have adopted SWC as their default compiler because of its performance advantages and plugin ecosystem.

When it comes to rendering strategies, SWC plays a critical role in transforming JSX, optimizing imports, and stripping types during the build and runtime phases. This means your SSR, SSG, and ISR pipelines benefit from faster compilation, which translates to quicker deployments and shorter feedback loops.

Understanding the Three Rendering Strategies

Setting Up a Project with SWC

Let's start by setting up a minimal project that uses SWC for compilation. We'll use Next.js since it natively integrates SWC, but the concepts apply to custom setups as well.

# Create a new Next.js project (SWC is built-in)
npx create-next-app@latest swc-rendering-demo
cd swc-rendering-demo
npm install

If you want to configure SWC directly, create a .swcrc file in your project root:

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    },
    "target": "es2020"
  },
  "module": {
    "type": "es6"
  }
}

This configuration tells SWC to parse TypeScript with JSX support, use the automatic React runtime, and target ES2020.

Implementing SSG with SWC

Static Site Generation is the simplest and fastest rendering strategy. Pages are pre-rendered at build time, and SWC ensures the build process is as quick as possible.

// pages/blog/[slug].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';

interface Post {
  id: string;
  title: string;
  content: string;
}

interface PostPageProps {
  post: Post;
}

interface Params extends ParsedUrlQuery {
  slug: string;
}

export default function PostPage({ post }: PostPageProps) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

export const getStaticPaths: GetStaticPaths<Params> = async () => {
  // Fetch all post slugs at build time
  const res = await fetch('https://api.example.com/posts');
  const posts: Post[] = await res.json();

  const paths = posts.map((post) => ({
    params: { slug: post.id },
  }));

  return {
    paths,
    fallback: false, // 404 for non-pre-rendered pages
  };
};

export const getStaticProps: GetStaticProps<PostPageProps, Params> = async (context) => {
  const { slug } = context.params!;
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post: Post = await res.json();

  return {
    props: {
      post,
    },
  };
};

With SWC handling the compilation, the build step for hundreds of static pages completes in a fraction of the time compared to Babel-based setups. This is especially noticeable in CI/CD pipelines where build minutes matter.

Implementing SSR with SWC

Server-Side Rendering generates HTML on every request. This is ideal for pages that display user-specific data or content that changes frequently.

// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { verifyToken } from '../lib/auth';

interface DashboardProps {
  user: {
    name: string;
    email: string;
    lastLogin: string;
  };
  stats: {
    visits: number;
    orders: number;
  };
}

export default function Dashboard({ user, stats }: DashboardProps) {
  return (
    <main>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Last login: {user.lastLogin}</p>
      <section>
        <h2>Your Stats</h2>
        <ul>
          <li>Visits: {stats.visits}</li>
          <li>Orders: {stats.orders}</li>
        </ul>
      </section>
    </main>
  );
}

export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
  const token = context.req.cookies?.authToken;

  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  const user = await verifyToken(token);
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  const statsRes = await fetch(`https://api.example.com/stats/${user.id}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const stats = await statsRes.json();

  return {
    props: {
      user: {
        name: user.name,
        email: user.email,
        lastLogin: new Date().toISOString(),
      },
      stats,
    },
  };
};

Because SWC compiles the server-side code so quickly, cold starts in serverless environments are reduced. This is a meaningful advantage for SSR workloads deployed on platforms like Vercel or AWS Lambda, where cold start latency directly impacts user experience.

Implementing ISR with SWC

Incremental Static Regeneration gives you the best of both worlds: the speed of static pages with the freshness of server-rendered content. You specify a revalidation interval, and Next.js regenerates the page in the background when a request comes in after the interval has elapsed.

// pages/products/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next';

interface Product {
  id: string;
  name: string;
  price: number;
  inventory: number;
  updatedAt: string;
}

interface ProductPageProps {
  product: Product;
}

export default function ProductPage({ product }: ProductPageProps) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price.toFixed(2)}</p>
      <p>In stock: {product.inventory}</p>
      <small>Last updated: {product.updatedAt}</small>
    </div>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/products');
  const products: Product[] = await res.json();

  // Pre-render only the top 50 products at build time
  const paths = products.slice(0, 50).map((product) => ({
    params: { id: product.id },
  }));

  return {
    paths,
    // 'blocking' generates new pages on-demand and caches them
    fallback: 'blocking',
  };
};

export const getStaticProps: GetStaticProps<ProductPageProps> = async (context) => {
  const id = context.params?.id as string;
  const res = await fetch(`https://api.example.com/products/${id}`);

  if (!res.ok) {
    return {
      notFound: true,
    };
  }

  const product: Product = await res.json();

  return {
    props: {
      product,
    },
    // Revalidate every 60 seconds
    revalidate: 60,
  };
};

In this example, the first 50 products are pre-rendered at build time. Any product beyond that list is generated on the first request (thanks to fallback: 'blocking') and then cached. Every 60 seconds, a new request triggers a background regeneration. SWC's fast compilation ensures that even on-demand generation is snappy.

On-Demand Revalidation

Sometimes a fixed interval isn't enough. You may want to regenerate a page immediately after content changes in your CMS. Next.js supports on-demand revalidation via an API route.

// pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const secret = req.headers.authorization;
  const { path } = req.query;

  if (secret !== `Bearer ${process.env.REVALIDATION_SECRET}`) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  if (!path || typeof path !== 'string') {
    return res.status(400).json({ message: 'Path is required' });
  }

  try {
    await res.revalidate(path);
    return res.json({ revalidated: true, path });
  } catch (err) {
    return res.status(500).json({ message: 'Revalidation failed', error: String(err) });
  }
}

Your CMS webhook can call POST /api/revalidate?path=/products/123 with the secret token, and the page will be regenerated instantly without waiting for the interval to expire.

Using SWC Plugins for Custom Transformations

One of SWC's most powerful features is its plugin system. You can write custom transformations in Rust or use community plugins via the Next.js configuration. Here's an example of enabling SWC-based styled-components support:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  swcMinify: true,
  compiler: {
    styledComponents: {
      displayName: true,
      ssr: true,
      fileName: true,
      pure: true,
    },
  },
  experimental: {
    swcPlugins: [
      ['@swc/plugin-emotion', {}],
    ],
  },
};

module.exports = nextConfig;

This configuration enables SWC-powered minification and styled-components SSR support without needing a Babel plugin, keeping your build fast.

Building a Custom SSR Server with SWC

If you're not using Next.js, you can still build a custom SSR server with SWC. Here's a minimal Express-based example:

// server.tsx
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import swc from '@swc/core';
import path from 'path';
import fs from 'fs';

const app = express();

// Compile a component on the fly using SWC
async function compileComponent(filePath: string) {
  const code = fs.readFileSync(filePath, 'utf-8');
  const result = await swc.transform(code, {
    filename: filePath,
    jsc: {
      parser: {
        syntax: 'typescript',
        tsx: true,
      },
      transform: {
        react: {
          runtime: 'automatic',
        },
      },
    },
  });

  // Write to a temp file and import it
  const tempPath = path.join(__dirname, '.temp', path.basename(filePath) + '.js');
  fs.mkdirSync(path.dirname(tempPath), { recursive: true });
  fs.writeFileSync(tempPath, result.code);

  // Clear cache and import
  delete require.cache[require.resolve(tempPath)];
  return require(tempPath).default;
}

app.get('*', async (req, res) => {
  try {
    const Component = await compileComponent('./src/App.tsx');
    const html = renderToString(React.createElement(Component, { url: req.url }));

    res.send(`
      <!DOCTYPE html>
      <html>
        <head>
          <title>SWC SSR Demo</title>
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/client.js"></script>
        </body>
      </html>
    `);
  } catch (err) {
    console.error('Render error:', err);
    res.status(500).send('Server error');
  }
});

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

This example demonstrates how SWC can be used programmatically to transform TSX files at runtime. In production, you'd pre-compile components during the build step and serve the compiled output, but this illustrates the core concept.

Best Practices

Performance Comparison: SWC vs Babel

To understand the impact of SWC, consider a project with 500 pages using SSG. With Babel, the build might take 120 seconds. With SWC, the same build often completes in 15-25 seconds. For SSR, the compilation of server-side code during development is near-instant with SWC's incremental compilation, compared to noticeable delays with Babel.

# Benchmark a Next.js build with SWC
time npm run build

# Typical output:
# Route (pages)                              Size     First Load JS
# ┌ ○ /                                      1.2 kB         87 kB
# ├ ○ /blog/[slug]                           2.1 kB         88 kB
# ├ ● /products/[id] (ISR: 60s)              1.8 kB         88 kB
# └ ƒ /dashboard                             2.5 kB         89 kB
# ○  (Static)  ●  (ISR)  ƒ  (Server)
#
# real    0m18.342s
# user    0m45.123s
# sys     0m3.456s

Conclusion

Server-Side Rendering with SWC combines the best of modern rendering strategies with cutting-edge compilation performance. By understanding when to use SSR, SSG, and ISR, and by leveraging SWC's Rust-powered toolchain, you can build web applications that are fast to build, fast to serve, and excellent for SEO. Whether you're using Next.js or building a custom server, SWC reduces compilation overhead and lets you focus on delivering great user experiences. Start by auditing your existing pages to ensure each one uses the optimal rendering strategy, then enable SWC's features like minification and plugins to squeeze out even more performance. The result is a rendering pipeline that scales with your content and your team.

— Ad —

Google AdSense will appear here after approval

← Back to all articles