Building High-Performance Static Websites with Next.js

In the rapidly evolving landscape of web development, the pursuit of optimal performance has led developers to rediscover the power of static websites. Unlike traditional dynamic sites that generate content on-demand, static sites pre-build all pages and data during the build process, resulting in lightning-fast load times and enhanced user experiences. This approach represents a significant shift from server-side rendering to a more efficient model where content is prepared in advance and served directly to users without requiring server-side computation. Modern websites which does not change content frequently can benefit from this.
Benefits of Static Site Generation
Static site generation offers major performance improvements by serving pre-rendered HTML files, resulting in faster page loads and eliminating the need for real-time server processing. This enhances scalability, reduces server load, and lowers operational costs. With static assets distributed through CDNs, users experience minimal latency and consistent performance worldwide. Additionally, static sites improve SEO by making fully rendered content easily accessible to search engines, leading to better rankings and increased organic traffic.
What is [https://nextjs.org/docs](Next.js)?
Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.
It also automatically configures lower-level tools like bundlers and compilers. You can instead focus on building your product and shipping quickly.
Whether you're an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast React applications.
So let's get started
In this guide, we’ll build a simple blog-style post app that displays 10 posts on the homepage. Each post will be clickable, leading to its own individual page. We'll use the fake API provided by JSONPlaceholder to fetch the post data.
Our goal is to statically generate all 10 post pages at build time. This means each post will be pre-rendered into its own HTML file, improving performance and enabling us to export the entire app as static HTML.
Installing and Project Setup
To get started, we'll create a new Next.js project and set up the basic structure for our static post app.
Create a new Next.js app using the following command:
npx create-next-app@latest static-posts-app
You’ll be prompted with a series of configuration options. For this guide, choose the following responses:
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a src/
directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for next dev
? … No / Yes
✔ Would you like to customize the import alias (@/*
by default)? … No / Yes
✔ What import alias would you like configured? … @/*
Once the setup is complete, navigate into the project folder:
cd static-posts-app
Now start the development server:
npm run dev
Visit http://localhost:3000 in your browser. You should see the default Next.js homepage running.
We’re now ready to start building the static post app!
Let’s start by cleaning up the default code. Open src/app/page.tsx and replace its content to begin building our static post app.
// HomePage.tsx
import { Post } from '@/types/post'
import PostCard from '@/components/PostCard'
// Fetch 10 posts from the API
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_page=0&_limit=10')
return res.json()
}
export default async function HomePage() {
// Fetch posts data
// Note: This will run at build time for static generation
const posts = await getPosts()
return (
<main className="max-w-4xl mx-auto px-4 py-8">
{/* App header */}
<h1 className="text-3xl font-bold mb-6 text-center">Static Post App</h1>
{/* List of posts */}
<div className="grid gap-4">
{posts.map(post => (
// Render each post using the PostCard component
<PostCard key={post.id} post={post} />
))}
</div>
</main>
)
}
Next, create the Post type and build the PostCard component.
// This file defines the Post interface used in the application.
export interface Post {
userId: number
id: number
title: string
body: string
}
import { Post } from "@/types/post"
import Link from "next/link"
// PostCardProps interface to define the props for PostCard component
interface PostCardProps {
post: Post
}
// PostCard component to display individual post details
export default function PostCard({ post }: PostCardProps) {
return (
<Link href={`/posts/${post.id}`} className="bg-white shadow rounded-xl p-4 hover:shadow-md transition">
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-700 text-sm">{post.body}</p>
</Link>
)
}
Next, create the individual post page component inside the app/posts/[id]/page.tsx file. Make sure to follow Next.js’s App Directory structure for dynamic routes.
import { Post } from "@/types/post";
import React from "react";
type PostPageProps = {
params: Promise<{ id: string }>;
};
// This function generates static paths for the dynamic route [id].
export async function generateStaticParams() {
const posts = await fetch(
"https://jsonplaceholder.typicode.com/posts?_page=0&_limit=10"
);
const data: Post[] = await posts.json();
// Map the fetched posts to generate static paths
return data.map((post) => ({
id: post.id.toString(),
}));
}
// Fetch single post data by ID
async function getPost(id: string): Promise<Post> {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!res.ok) {
throw new Error("Failed to fetch post");
}
return res.json();
}
export default async function PostPage({ params }: PostPageProps) {
// Await the params to get the post ID
const { id } = await params;
// Fetch the post data using the ID
const post = await getPost(id);
return (
<main className="max-w-3xl mx-auto px-4 py-8">
{/* App header */}
<h1 className="text-3xl font-bold mb-6 text-center">Static Post App</h1>
{/* Post Details */}
<h2 className="text-3xl font-bold mb-4">{post.title}</h2>
<p className="text-gray-700 whitespace-pre-line">{post.body}</p>
</main>
);
}
Do you see the **generateStaticParams** method? Basically, this function tells Next.js to fetch all the post IDs ahead of time and then pre-build a static page for each one during the build process.
Think of it as giving Next.js a list of all the posts it needs to generate pages for—so when someone visits, the page is already ready and loads super fast. Without this, Next.js wouldn’t know which post pages to create in advance.
To enable Next.js to pre-build and export the app as a static site, update your next.config.ts file by adding the output key with the value 'export'.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: "export",
};
export default nextConfig;
Now, run the build command using npm run build. This will generate all the pages and output the static files into the out folder. You can serve this folder locally or deploy it to any static hosting provider—no server needed. By caching these files on a CDN, you can achieve blazing-fast page load times for your users.
You can find this code referenced in my GitHub repository https://github.com/sarifmiaa/static-posts-app
Unsupported Features in Static Export
While static site generation offers great performance and simplicity, it comes with certain limitations. Next.js's output: 'export' mode is designed to generate a fully static site that can be hosted without a Node.js server. As a result, any features that rely on server-side logic or dynamic behavior at runtime are not supported.
🔒 Unsupported Features
The following Next.js features do not work in static export mode:
Internationalized Routing – Locale-based routing and content switching are not supported.
API Routes – You cannot define serverless functions via /api routes.
Rewrites, Redirects, and Headers – These require a server to process incoming requests dynamically.
Middleware – Edge and server middleware are not executed in static builds.
Incremental Static Regeneration (ISR) – Static pages can't be updated after build time without a server.
Image Optimization with the default loader – The built-in image optimization uses a server, so you'll need to use a third-party image loader or manually optimize images.
Draft Mode – This requires server-side logic to bypass static rendering.
getStaticPaths with fallback: true or 'blocking' – Only fallback: false is supported in a fully static export.
getServerSideProps – As it runs on each request, it's not compatible with a static output.
Why These Limitations Exist
These features depend on dynamic behavior—either on the server or at the request level—which contradicts the nature of a purely static site. Since static export pre-renders everything at build time and serves HTML from a CDN or file system, there's no server runtime to handle these dynamic processes.
If your application needs any of these features, you'll need to deploy it using a serverful environment (like Vercel, Node.js server, or serverless functions), or reconsider your architecture to fit within the static export model.