Server-Side Rendering with Bootstrap: SSR, SSG, ISR Explained
Modern web development offers multiple rendering strategies that determine when and where your HTML is generated. When combined with a UI framework like Bootstrap, choosing the right rendering strategy can dramatically affect performance, SEO, and user experience. This tutorial walks through the three most popular server-side rendering approaches — SSR, SSG, and ISR — and shows you how to integrate Bootstrap effectively in each.
What Are Rendering Strategies?
Rendering strategies describe the moment and location where your application's HTML is produced. The three main server-side strategies are:
- SSR (Server-Side Rendering): HTML is generated on the server for every request.
- SSG (Static Site Generation): HTML is generated once at build time and reused for every request.
- ISR (Incremental Static Regeneration): Static HTML is generated at build time, but can be regenerated in the background at runtime when data changes.
Next.js is the most popular framework that supports all three strategies out of the box, so this tutorial uses Next.js (with the App Router) as the reference implementation. The concepts, however, apply to other frameworks like Nuxt, SvelteKit, and Remix.
Why Rendering Strategy Matters
Choosing the right strategy affects three critical areas:
- Performance: Static pages served from a CDN are faster than dynamically rendered pages.
- SEO: Search engines prefer fully rendered HTML available on the first request.
- Freshness: Some content changes frequently and needs runtime regeneration; other content is stable and benefits from caching.
Bootstrap, being a CSS framework, works seamlessly with any of these strategies — but how you import and use it differs slightly depending on whether your components render on the server or client.
Setting Up Bootstrap in a Next.js Project
First, create a new Next.js project and install Bootstrap:
npx create-next-app@latest my-bootstrap-app
cd my-bootstrap-app
npm install bootstrap
Import Bootstrap's CSS in your root layout file. In the App Router, this is app/layout.js:
// app/layout.js
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
export const metadata = {
title: 'Bootstrap SSR Demo',
description: 'SSR, SSG, and ISR with Bootstrap',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
With Bootstrap imported globally, every page — whether SSR, SSG, or ISR — will have access to Bootstrap classes and components.
Server-Side Rendering (SSR) with Bootstrap
What It Is
SSR generates HTML on the server for every incoming request. This ensures the user always sees the latest data, making it ideal for personalized dashboards, real-time data, and authenticated pages.
How to Use It
In the Next.js App Router, server components are the default. To force dynamic rendering on every request, you export a dynamic config or fetch data with caching disabled:
// app/dashboard/page.js
import 'bootstrap/dist/css/bootstrap.min.css';
export const dynamic = 'force-dynamic';
async function getStats() {
const res = await fetch('https://api.example.com/stats', {
cache: 'no-store',
});
return res.json();
}
export default async function Dashboard() {
const stats = await getStats();
return (
<div className="container py-5">
<h1 className="mb-4">Live Dashboard</h1>
<div className="row g-4">
<div className="col-md-4">
<div className="card text-bg-primary h-100">
<div className="card-body">
<h5 className="card-title">Active Users</h5>
<p className="card-text display-6">{stats.activeUsers}</p>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card text-bg-success h-100">
<div className="card-body">
<h5 className="card-title">Revenue Today</h5>
<p className="card-text display-6">${stats.revenue}</p>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card text-bg-warning h-100">
<div className="card-body">
<h5 className="card-title">Open Tickets</h5>
<p className="card-text display-6">{stats.tickets}</p>
</div>
</div>
</div>
</div>
</div>
);
}
When to Use SSR
- Pages with user-specific or session-based data.
- Real-time dashboards and admin panels.
- Pages that must never serve stale content.
Static Site Generation (SSG) with Bootstrap
What It Is
SSG pre-renders pages at build time. The resulting HTML files are served statically, typically from a CDN. This delivers the fastest possible load times and excellent SEO.
How to Use It
In the App Router, SSG is the default behavior for pages that do not use dynamic functions or request-time data fetching. You can explicitly mark a page as static:
// app/products/page.js
import 'bootstrap/dist/css/bootstrap.min.css';
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
cache: 'force-cache',
});
return res.json();
}
export default async function Products() {
const products = await getProducts();
return (
<div className="container py-5">
<h1 className="mb-4">Our Products</h1>
<div className="row row-cols-1 row-cols-md-3 g-4">
{products.map((product) => (
<div className="col" key={product.id}>
<div className="card h-100 shadow-sm">
<img
src={product.image}
className="card-img-top"
alt={product.name}
style={{ height: '200px', objectFit: 'cover' }}
/>
<div className="card-body">
<h5 className="card-title">{product.name}</h5>
<p className="card-text text-muted">{product.description}</p>
</div>
<div className="card-footer bg-white border-top-0">
<span className="h5 text-primary">${product.price}</span>
<a href="#" className="btn btn-outline-primary float-end">
View
</a>
</div>
</div>
</div>
))}
</div>
</div>
);
}
When you run npm run build, Next.js generates a static HTML file for this page. The page is then served instantly from the CDN edge.
When to Use SSG
- Marketing pages, blogs, and documentation.
- Product catalogs that change infrequently.
- Any content where maximum speed and SEO matter more than real-time data.
Incremental Static Regeneration (ISR) with Bootstrap
What It Is
ISR combines the speed of SSG with the freshness of SSR. Pages are generated statically at build time, but Next.js can regenerate them in the background after a specified time interval. The first visitor after the interval sees the cached version, and the regenerated page is served to subsequent visitors.
How to Use It
Enable ISR by passing a revalidate value (in seconds) to your fetch call or by exporting a revalidate constant:
// app/news/page.js
import 'bootstrap/dist/css/bootstrap.min.css';
export const revalidate = 60; // regenerate every 60 seconds
async function getArticles() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 60 },
});
return res.json();
}
export default async function News() {
const articles = await getArticles();
return (
<div className="container py-5">
<h1 className="mb-4">Latest News</h1>
<div className="list-group">
{articles.map((article) => (
<a
href={`/news/${article.slug}`}
className="list-group-item list-group-item-action"
key={article.id}
>
<div className="d-flex w-100 justify-content-between">
<h5 className="mb-1">{article.title}</h5>
<small className="text-muted">
{new Date(article.publishedAt).toLocaleDateString()}
</small>
</div>
<p className="mb-1 text-muted">{article.excerpt}</p>
<small>
<span className="badge bg-secondary me-1">
{article.category}
</span>
<span className="text-muted">
{article.readTime} min read
</span>
</small>
</a>
))}
</div>
</div>
);
}
On-Demand Revalidation
You can also trigger ISR regeneration manually using an API route. This is useful when you want to update a page immediately after content changes in your CMS:
// app/api/revalidate/route.js
import { revalidatePath } from 'next/cache';
export async function POST(request) {
const { path, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ message: 'Invalid secret' }, { status: 401 });
}
if (!path) {
return Response.json({ message: 'Path is required' }, { status: 400 });
}
revalidatePath(path);
return Response.json({ revalidated: true, path });
}
Call this endpoint from your CMS webhook whenever content is published or updated.
When to Use ISR
- News sites, blogs, and e-commerce catalogs with periodic updates.
- Pages where stale-while-revalidate behavior is acceptable.
- Content that updates more than once a day but not in real time.
Using Bootstrap Interactive Components
Bootstrap's CSS works perfectly in server components, but interactive JavaScript components (modals, dropdowns, carousels, tooltips) require client-side execution. In Next.js, you must mark these components with the 'use client' directive:
// app/components/BootstrapModal.js
'use client';
import { useEffect, useRef } from 'react';
import { Modal } from 'bootstrap';
export default function BootstrapModal({ title, children }) {
const modalRef = useRef(null);
const modalInstance = useRef(null);
useEffect(() => {
if (modalRef.current) {
modalInstance.current = new Modal(modalRef.current);
}
return () => {
if (modalInstance.current) {
modalInstance.current.dispose();
}
};
}, []);
const showModal = () => modalInstance.current?.show();
const hideModal = () => modalInstance.current?.hide();
return (
<>
<button
type="button"
className="btn btn-primary"
onClick={showModal}
>
Open Modal
</button>
<div
className="modal fade"
ref={modalRef}
tabIndex="-1"
aria-hidden="true"
>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{title}</h5>
<button
type="button"
className="btn-close"
onClick={hideModal}
aria-label="Close"
></button>
</div>
<div className="modal-body">{children}</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={hideModal}
>
Close
</button>
</div>
</div>
</div>
</div>
</>
);
}
You can then use this client component inside any server-rendered page:
// app/page.js
import BootstrapModal from './components/BootstrapModal';
export default function Home() {
return (
<div className="container py-5">
<h1 className="mb-4">Welcome</h1>
<p className="lead">This page is statically generated.</p>
<BootstrapModal title="Important Notice">
<p>This modal works because it is a client component.</p>
</BootstrapModal>
</div>
);
}
Best Practices
Choose the Right Strategy Per Page
Not every page needs the same strategy. A common pattern is to use SSG for marketing pages, ISR for content pages, and SSR for authenticated dashboards. Next.js lets you mix strategies freely within a single application.
Minimize Client-Side JavaScript
Bootstrap's CSS is framework-agnostic and works in server components. Only the interactive JavaScript pieces need to be client components. Keep your client components small and focused to preserve the performance benefits of server rendering.
Use Bootstrap via Sass for Customization
If you need to customize Bootstrap's theme variables, import Sass instead of the compiled CSS. Install Sass and create a custom stylesheet:
npm install sass
// app/custom-bootstrap.scss
$primary: #6f42c1;
$border-radius: 0.5rem;
@import 'bootstrap/scss/bootstrap';
// app/layout.js
import './custom-bootstrap.scss';
Optimize Images
Use Next.js's Image component instead of raw <img> tags for Bootstrap cards and media. This ensures automatic optimization, lazy loading, and responsive sizing:
import Image from 'next/image';
<Image
src={product.image}
alt={product.name}
width={400}
height={300}
className="card-img-top"
style={{ objectFit: 'cover' }}
/>
Handle Loading and Error States
For SSR and ISR pages, always provide loading and error UIs using Bootstrap's spinner and alert components:
// app/news/loading.js
export default function Loading() {
return (
<div className="container py-5 text-center">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
// app/news/error.js
'use client';
export default function Error({ reset }) {
return (
<div className="container py-5">
<div className="alert alert-danger" role="alert">
<h4 className="alert-heading">Something went wrong</h4>
<p>We could not load the news articles.</p>
<hr />
<button className="btn btn-outline-danger" onClick={reset}>
Try again
</button>
</div>
</div>
);
}
Cache Strategically
For ISR, choose your revalidate interval based on how stale content can be. A news site might use 60 seconds, while a product catalog might use 3600 seconds. Always pair time-based revalidation with on-demand revalidation for instant updates when content changes.
Conclusion
Server-side rendering strategies give you fine-grained control over the performance, freshness, and SEO of your Bootstrap-powered applications. SSG delivers the fastest possible experience for static content, SSR guarantees real-time data for dynamic pages, and ISR offers the best of both worlds for content that updates periodically. By understanding when to use each strategy and how to properly integrate Bootstrap's CSS and JavaScript components within Next.js's server and client component model, you can build applications that are both visually polished and performant. Start by auditing your existing pages, identify which strategy fits each one, and migrate incrementally — your users and your Lighthouse scores will thank you.