Yet, for all the joy Astro has brought me, there's one thing I've been really missing: the ability to incrementally regenerate static pages on the fly. This feature, commonly known as Incremental Static Regeneration (ISR), has become somewhat of a staple in modern web development frameworks.
In this post, I'm going to walk you through how I've managed to implement this feature in Astro and how you can do the same in your projects.
Understanding Incremental Static Regeneration (ISR)
Before we dive into the implementation, let's first take a moment to understand what ISR is and why it's so important. In traditional static site generation, your entire site is built at once, which can be time-consuming for large websites. ISR, on the other hand, allows developers to regenerate static pages on demand, after the initial build. This means that you can update individual pages without rebuilding the whole site, leading to faster build times and the ability to reflect content updates more quickly.
ISR is particularly useful for sites with a large number of pages or for those that update content frequently, such as e-commerce sites, blogs, and news platforms. It strikes a balance between the benefits of static generation (speed, security, and reliability) and the flexibility of server-side rendering (fresh content and dynamic updates).
So, Why Choose ISR?
- Large Sites: If your site has thousands of pages, ISR can significantly reduce build times compared to SSG.
- Infrequent Content Updates: For content that changes less often than every five minutes on average, ISR is ideal.
- High Traffic: When your site has many visitors, it's inefficient to render the same page for each one. ISR solves this by caching pages.
- Slow CMS/Database: If your backend is slow or charges per request, ISR can reduce the frequency of those requests.
When ISR Might Not Be for You
- Content Inside Your Project: If your content is part of your project, like markdown files, ISR might not be necessary.
- Avoiding Server Management: If you prefer not to run a server or use serverless platforms, you might want to skip ISR.
- Logged-in Experiences: When each user sees a slightly different version of a page, caching might not work.
- Frequent Layout/Styling Changes: If you often change your site's structure, you'll need full rebuilds, which makes ISR less useful.
How We're Going to Implement ISR in Astro
To implement ISR, all we want to do is cache the rendered content of each page for reuse in future requests for some predefined period.
There are a few different ways to implement ISR in Astro. For example, it's possible to rely on our CDN to cache individual pages using a
Cache-Control header. This works particularly well for sites that are hosted on a serverless platform and don't have access to a local and persistent cache storage.
Another approach is to use a local cache storage, such as Redis, to store the rendered content of each page. This approach is a bit more involved, but it's also more flexible and allows us to do some cool things like cache invalidation and background regeneration.
In this blog post, I will show you both approaches but will be focusing more on the latter.
Setting Up Our Project
To get started with implementing ISR in Astro, you'll first need to ensure that your Astro project is set up correctly. If you haven't already created an Astro project, you can do so by running the following command:
Once you've created your project, you'll need to create at least one page with some dynamic content. For this example, we're going to create a simple page that displays the current time.
Create a new file in the
src/pagesdirectory of your Astro project. You can name it
time.astroor whatever you like.
time.astrofile, you can write a simple component that outputs the current time. Here's an example:
This page will get the current time and display it in the browser. If you run
npm run dev and navigate to
http://localhost:4321/time, you should see something like this:
The page will default to
SSR mode, which means that the page will be rendered on the server and then sent to the browser. Now, let's see how we can implement ISR to cache the rendered content of this page so we don't have to re-render it on every request.
Implementing ISR with Cache-Control Headers
The first approach we're going to look at is using CDN caching to cache the rendered content of our page. As I mentioned previously, this approach is particularly useful for sites that are hosted on a serverless platform and don't have access to a local and persistent cache storage. This method takes advantage of the
Cache-Control header to cache the rendered content of our page.
Understanding the Cache-Control Headers
Before we implement ISR, it's crucial to understand Cache-Control headers, as they are the key to controlling CDN caching behavior. Cache-Control headers are used to define how, and for how long, the responses should be cached by browsers and CDNs. Here's a breakdown of some common Cache-Control directives:
max-age=<seconds>: Specifies the maximum amount of time a resource will be considered fresh. This applies to both shared caches (like CDNs) and private client-side caches (like browsers).
s-maxage=<seconds>: Similar to
max-age, but it only applies to shared caches. If
s-maxageis present, CDNs will use it instead of
public: Marks the response as cacheable by both shared caches and private client-side caches.
private: Indicates the response is intended for a single user and should not be stored by shared caches.
no-cache: Forces caches to submit the request to the origin server for validation before releasing a cached copy, every time.
no-store: The cache should not store anything about the client request or server response.
Setting Up Cache-Control Headers
To implement ISR on your Astro page using CDN caching, you need to set the appropriate
Cache-Control headers. Here's how you can do it:
- Modify your
time.astropage to include the Cache-Control header:
In this example,
s-maxage=60 tells the CDN to cache the page for 60 seconds.
stale-while-revalidate=30 allows the CDN to serve a stale version of the page for an additional 30 seconds while it fetches a fresh one in the background.
Deploy your site to a hosting provider that supports CDN caching with Cache-Control headers, such as Vercel, Netlify, or Cloudflare.
Once deployed, your Astro page will be served from the CDN cache. The CDN will automatically regenerate the page after the specified
s-maxageperiod has passed, ensuring that the content stays up to date.
Implementing ISR with In-Memory Caching
Alright, now let's look at how we can implement ISR using an in-memory cache within our Node.js server. This approach is useful if you don't have a CDN, or if you want more granular control over the caching behavior compared to what we can achieve with CDN caching.
For this approach, we will be taking advantage of Astro's middleware feature to intercept each request and either serve the cached response or regenerate the page if it's stale.
Setting up the Middleware
First, we'll create a middleware function in Astro that will intercept each request. To do so, create a new file called
middleware.ts in the root of your Astro project and add the following code:
This middleware function will be called for every request that comes into our Astro server. It will log the request URL to the console and then call the
next() function to continue processing the request.
If you run
npm run dev and navigate to
http://localhost:4321/time, you should see the following output in your console:
[Middleware] onRequest /time
Caching the Rendered Content
Let's start with a simple implementation of our cache. We will later add some additional logic to make it more flexible and robust.
Now, if we run
npm run dev and navigate to
http://localhost:4321/time, we should keep seeing the same output every time we refresh the page. This is because the middleware is caching the response and serving it from the cache on subsequent requests.
Adding Cache Invalidation
Now that we have our caching logic in place, we can add some additional logic to invalidate the cache after a certain period. This will ensure that the content stays up-to-date and that we don't serve stale content to our users.
To do so, we'll add an
expires property to our cached response object. This property will be set to the current time plus the number of seconds we want to cache the response for. We'll also add some logic to check if the cached response has expired before serving it to the client.
Now, if we run
npm run dev and navigate to
http://localhost:4321/time, we should see the same output every time we refresh the page. However, if we wait for 1 minute and refresh the page again, we should see a new output. This is because the cached response has expired and the middleware has regenerated the page.
This is a good start, but the issue is that the middleware is now aggressively caching the response for every page. However, we usually only want to cache pages that are expensive to render, and at the same time, we may want to change the duration of the cache for each page.
Adding Page-Specific Cache Control
To solve this issue, we can take advantage of Astro's locals feature. First, let's define a new function for our locals object that will allow us to set the cache duration for each page.
src/env.d.ts and add the following code:
This will expose a new
cache() function on the locals object that we can use to define the cache duration for each page. Now, let's update our middleware to use this new function.
We've updated the middleware to add a
cache() method to the
req.locals object. This method will allow us to set the cache duration for each page. We've also made the
ttl variable dynamic, so that unless the
cache() method is called, the middleware will not cache the response. This will allow us to control the caching behavior for each page.
If you refresh the
http://localhost:4321/time page, you should see a new output every time. This is because we haven't called the
cache() method yet, so the middleware is not caching the response.
Setting the Cache Duration for Each Page
Now that we have our middleware set up to allow us to set the cache duration for each page, let's update our
time.astro page to take advantage of this new feature.
Finally, if you refresh the
http://localhost:4321/time page, you should see the same output every time for 1 minute. This is because we've set the cache duration for this page to 1 minute.
Considerations for Production
While an in-memory cache works well for demonstration purposes, it has limitations in a production environment. Since the cache is stored in the memory of a single process, it won't be shared across multiple instances of your application, which is common in production deployments.
For a scalable production-ready caching solution, you would typically use a distributed caching system like Redis. This allows multiple instances of your application to share the same cache, ensuring consistency and reliability.
Additionally, you should also handle cache invalidation, which is the process of removing outdated content from the cache. This could be done based on certain events, like content updates, or using a more sophisticated caching strategy that combines both time-based expiry and active invalidation.
As you can see, implementing ISR in Astro is not as hard as it might seem. In fact, it's quite simple and straightforward. What I have shown you in this post is the very basic implementation of ISR, but you can, of course, take it much further and build a more robust solution that fits your needs.
I hope this post has helped you understand how to implement ISR in Astro and how you can use it to improve the performance of your site.
Interested in LogSnag?