Part of choosing a frontend framework is understanding how pages can be delivered to your users. Choosing the right method can enable major performance improvements or introduce unwanted drawbacks in your app. Next.js allows us to choose different rendering methods on a per page basis, giving us the tools to iterate and deliver a better experience to users β¨ In this chat, we'll discuss the different rendering methods (listed below) Next.js provides so you can understand which method is right for your use case:
-
client-side rendering
-
server-side rendering
-
static site generation
-
incremental static regeneration
Client-side Rendering (CSR)
Client-side rendering allows you to statically generate parts of a page without data, and fetch that data on the client-side. This is useful for pages you don't want to server-side render but are still unique to each user. With this method you do get faster build times but it's important to note the fetched data is never cached and you won't have SEO indexing either.
Server-side Rendering (SSR)
Server-side rendering is when a page is pre-rendered on the server on every request. You'd want to use this approach for pages that are either unique to each user or updated frequently. This method creates a slower TTFB (time to first byte) but ensures that visitors requesting this page will always get the most up-to-date content!
Static Site Generation (SSG)
Static site generation allows Next.js to pre-render your pages at build time, regardless if they have data or not. All pages that don't export getServerSideProps
will be pre-rendered at build time. This is optimal when your content doesn't change, which means it should be delivered as quickly as possible. By using this method, pages are globally cached by a CDN and served instantly. This gives your site high availability, even if your backend is down β making this method more performant than SSR. A caveat is that your data could be stale at request time β but don't worry, we have a solution for that.
Incremental Static Regeneration (ISR)
Incremental static regeneration allows you to add and update statically pre-rendered pages incrementally after build time. This means you can deliver static pages with SSG and update them after they've been pre-built! You can simply add a revalidation time when you return from getStaticProps
like below:
export function getStaticProps() {
// ...
return {
props: {
// ...
revalidate: 60
}
}
}
This snippet is telling Vercel to rebuild and cache the page every
60
seconds. I go more in depth about ISR in this artcle.
You may experience unnecessary reads to your database if you use ISR on a page that doesn't frequently update. You can still deliver dynamic content in static pages with On-demand ISR. This feature allows you to revalidate a page whenever you want (via webhook, mutations via endpoints, etc.), giving you full control over the dynamism of your static pages. Here's how:
// pages/api/user.ts
export default function handler(req, res) {
const { userId, data } = req.body;
await updateUser(userId, data);
await res.unstable_revalidate(`/u/${userId}`);
res.status(200).end();
}
The
res.unstable_revalidate
method can be used anywhereres: NextApiResponse
is available and simply pass it the route you want to revalidate.
Using on-demand ISR eliminates any unnecessary reads to your database when regenerating static pages.
Let's Build π
We'll build an app that allows users to add their favorite Unsplash photos to a public gallery. This app will showcase the different rendering methods Next.js offers. We'll have the following routes:
/
: The index route will be client-side rendered to display all images from users.
/i/[id]
: A dynamic route for each individual image β this will be statically generated at build time.
Why should these dynamic routes be statically generated? Once uploaded, this data won't get updated, meaning it's static content and should be delivered as fast as possible β¨
/u/[id]
: A dynamic route for each user showing their uploaded imagesβ this will also be statically generated and updated using ISR. We're using this approach since users can upload new images over time but we still want the content to be fast!
Since the /
(index route) is being client-side rendered, we're data fetching when the component first loads via useEffect
like so:
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const res = await fetch('/api/links/getAll');
const data = await res.json();
setPosts(data);
setLoading(false);
};
fetchData();
}, []);
To generate the dynamic paths for /i/[id]
(dynamic image pages), we need to use getStaticPaths
to get all of the routes β this is pretty simple:
export async function getStaticPaths() {
const posts = await prisma.post.findMany();
const paths = posts.map((post) => ({
params: { id: post.id },
}));
return { paths, fallback: true };
};
π‘ If a user visits a route that hasn't been built yet (/i/random-route),
fallback: true
allows us to generate a loading state while the page gets rendered and cached globally!
Now that we have each route, we need to statically generate and pre-render the page:
export async function getStaticProps({ params }) {
const post = await prisma.post.findUnique({
where: { id: params?.id as string }
});
if (!post) {
return {
notFound: true,
revalidate: 10,
};
}
return {
props: {
post
}
};
};
π‘ If the user visits a route where there is no post (image), we redirect them to a 404 page and
revalidate
every 10 seconds in the case that post is added to the database.
We use a very similar approach with /u/[id]
(dynamic user pages):
export async function getStaticPaths() {
const users = await prisma.user.findMany();
const paths = users.map((user) => ({
params: { id: user.id },
}));
return { paths, fallback: true };
};
export async function getStaticProps({ params }) {
const user = await prisma.user.findUnique({
where: { id: params?.id as string }
});
if (!user) {
return {
notFound: true,
revalidate: 10,
};
}
return {
props: {
user
}
};
};
The only difference with this dynamic user page is that I have an API route that revalidates /u/[id]
whenever someone uploads a new image. It looks like this:
export default async function handler(req, res) {
const { url, email } = req.body;
const post = await prisma.post.create({
data: {
url,
userEmail: email,
},
});
await res.unstable_revalidate(`/u/${post.user.id}`);
res.status(200).json({ status: 'Success' });
}
π‘ Remember that
res.unstable_revalidate
rebuilds pages on-demand, which allows us to serve static user pages while always being up-to-date β‘
Wrap Up
Understanding how your pages are delivered to users is an important part of providing them with a great experience. I hope you have a better understanding of the different rendering strategies in Next.js β¨ You can view the app here and check out the full source-code here π.
β€οΈ Open Source Software