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
- SSR (Server-Side Rendering): The HTML is generated on the server for every request. Best for highly dynamic, personalized content.
- SSG (Static Site Generation): HTML is generated once at build time. Best for content that rarely changes, like blog posts or marketing pages.
- ISR (Incremental Static Regeneration): A hybrid approach where static pages are regenerated in the background at a specified interval. Best for content that updates periodically without needing a full rebuild.
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
- Choose the right strategy per page: Don't default to SSR for everything. Use SSG for static content, ISR for periodically updated content, and SSR only when you need per-request personalization.
- Enable SWC minification: Set
swcMinify: truein your Next.js config to reduce bundle sizes with Rust-powered minification. - Cache aggressively: For SSR, use
Cache-Controlheaders and CDN caching where possible. For ISR, choose revalidation intervals that balance freshness with performance. - Handle errors gracefully: In
getStaticPropsandgetServerSideProps, always handle API failures with fallback content ornotFound: truerather than letting the page crash. - Use
fallback: 'blocking'for ISR: This avoids flash-of-loading-state for new pages and provides a better UX thanfallback: true. - Profile your build: Use
next buildwith the--profileflag to identify slow pages. SWC makes builds faster, but poorly optimized data fetching can still be a bottleneck. - Keep SWC updated: SWC receives frequent performance improvements and bug fixes. Pin your versions but upgrade regularly.
- Avoid unnecessary client-side JavaScript: Server-rendered HTML means users see content immediately. Don't bloat the client bundle with libraries that could run on the server.
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.