DEV Community

Cover image for Implementing ISR: Elevating Your App Performance with Netlify's New Next.js 14 Runtime
professorjrod
professorjrod

Posted on

Implementing ISR: Elevating Your App Performance with Netlify's New Next.js 14 Runtime

Have you heard of ISR? Do you want to implement it in your Next app but don't know how? Or, have you already tried to implement ISR, but it didn't work on Netlify?

If you answered YES! to any of these questions, well this post is for you then!

Image description

Note: This blog post is accompanied by a YouTube video :)

ISR Overview

For the unfamiliar, Incremental Static Regeneration (ISR) is a feature that can greatly improve the performance and scalability of Next.js applications. ISR is a newly optimized approach to web development that combines the benefits of static generated content with dynamic data updates.

Normally, when you want to update a page, you might have to rebuild the whole website. But with ISR (Incremental Static Regeneration), you only need to update specific pages. So, instead of a potentially LONG build time (and a potential performance hit), you can quickly refresh just the pages that need to be updated. This helps keep your website fresh and responsive without a lot of extra work or waiting.

One significant advantage of using static pages is that they involve minimal data transfer, leading to faster loading times. Additionally, you can cache static pages, further enhancing their speed and efficiency when serving them to users. Because the pages being server are plain HTML, this also provides superior SEO in order to boost your page to the top of search engine results.

Image description

When you have an app with potentially millions of pages, like a blog site, or something like Wikipedia, build times can take AGES. So when you use ISR, you get to keep the benefits of Static Site Generation while also being able to scale to millions of pages.

ISR on Netlify

In the past, I have tried to implement ISR on my app, but for some reason it didn't work when I deployed on Netlify. I tried testing it using a simple two page app, using both a time-based revalidation as well as on-demand revalidation.

Turns out, ISR wasn't actually officially supported by Netlify- When I was desperately scrolling through their forums trying to make it work, I came accross threads like this one and was a bit dissapointed to see that ISR wasn't fully supported.

It kind of worked, like when you used a time based revalidation it seemed to work most of the time. But when you tried to revalidate on demand, by say hitting an API route, then you were out of luck.

However, coming back to the topic with my latest project, I was pleasantly surprised to see that it's now working with the latest runtime! It's actually open source, so you can check it out here if you're curious: https://github.com/netlify/next-runtime

When I tried it out I was pleasantly surprised. It definitely works with the newest Netlify runtime, and it works well!

My implementation

Time based revalidation

First, I had a blog app that was statically generated. I went to my index page and I added the revalidate keyword in my getStaticProps function

export const getStaticProps = async (context) => {
  const data = await getBlogPosts();

  return {
    props: {
      posts: data,
    },
    revalidate: 60,
  };
};
Enter fullscreen mode Exit fullscreen mode

This instantly began working, and like clockwork, every 60 seconds my page would be regenerated upon new content. I updated MongoDB and refreshed the page after a minute and I would see the new content, delivered to my browser as blazing fast HTML! 😎

But, what if a user uploads some content to my blog and they don't see their post immediately? Wouldn't that be super confusing and a not-so-great user experience?

I decided to implement out On-Demand revalidation!

On Demand Revalidation

On the Next Api, all I had to do was follow the official documentation and use the revalidate() property supported by Next.js

export default async (req, res) => {
  if (req.method === "POST") {
    console.log("POST REQUEST");
    if (req.body.title && req.body.content && req.body.industry) {
      const newPost = {
        title: req.body.title,
        content: req.body.content,
        industry: req.body.industry,
        author: Date.now(),
      };

      const insertedId = await addBlogPost(newPost);
      res.revalidate("/blog");
      res.revalidate("/blog" + "/insertedId")
      res.status(200).json(insertedId);
    } else {
      res.status(400).json({ error: "title,body,or content missing" });
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

As you can see, whenever a user hits my API with a valid POST request, I call the function to add a blog post to MongoDB.

When the post request is received and validated, we call res.revalidate("/blog"). This revalidate updates the index page which has all of the blog posts on it.

Then, we call another res.revalidate("/blog" + "/insertedId") which uses the same identifier I use in each individual page's slug.

Combining these two, whenever a user uploads to the API a new blog post, both of the relevant pages are statically generated as HTML without having to build any of the other static pages I use. Nice. Super painless to add to my app and it really improved the performance.

Taking advantage of Netlify's Cache-tagging and purge API

Netlify also supports the Cache-Tag and Netlify-Cache-Tag response headers, which allows you to associate an object objects—to a tag that can be purged simultaneously across all of Netlify’s Edge, without the need for a new deploy.

Here's a quick example you can implement in your app:

// pages/api/products/[productId].js

export default async function handler(req, res) {
  const {
    query: { productId },
  } = req;

  const resp = await fetch(`https://your-api.com/products/${productId}`);
  const json = await resp.json();
  const body = `<!doctype html><html>
    <head>
        <title>Product Details</title>
    </head>
    <body>
        <h2>Product Details</h2>
        <div>
          <h3>${json.name}</h3>
          <p>${json.description}</p>
          <p>Price: ${json.price}</p>
        </div>
    </body><html>`;

  res.setHeader("Content-Type", "text/html");
  res.setHeader("Cache-Control", "public, max-age=0, must-revalidate"); // Tell browsers to always revalidate
  res.setHeader("Netlify-CDN-Cache-Control", "public, max-age=31536000, must-revalidate"); // Tell Edge to cache asset for up to a year
  res.setHeader("Cache-Tag", `${productId},product-details,proxy-api-response`); // Tag all product details responses with these tags
  res.status(200).send(body);
}
Enter fullscreen mode Exit fullscreen mode

Content tagged with these headers can be purged using a Netlify Function to call Netlify’s Purge API.

In this example, this API call will purge all the site’s content tagged with product-details —while leaving the remaining site’s content cached on the CDN.

// pages/api/purge.js

import { purgeCache } from "@netlify/functions";

export default async function handler(req, res) {
  const cacheTags = ["product-details"]; // Include product details cache tags
  await purgeCache({ cacheTags });

  res.status(202).send("Purged successfully!");
}
Enter fullscreen mode Exit fullscreen mode

Now, all you have to do is hit http://website/purge and voila! Everything tagged "product-details" is purged, and the rest of your site remains cached on your CDN! 😎

Conclusion

I always use Netlify, and admittedly I was a bit disappointed when I first tried this feature as on-demand revalidation didn't work. Their caching is super helpful for serving super fast pages. But now that it works with the latest runtime, I am stoked!

If you combine Next.js ISR feature, along with Netlify's cache-tagging and purge API, you have some serious control over your site's content, which is great-- especially for e-commerce where SEO is king. I highly recommend giving it a try!

Anyways, that's the wrap of this post, this was kind of a longer article than I normally write. Leave a comment if you enjoyed it or have a request for anything else you would like to see discussed!

Top comments (0)