Why ISR Alone Can Leave Users With Stale Data

Next.js's Incremental Static Regeneration (ISR) gives you the performance benefits of static generation while allowing pages to be periodically rebuilt in the background. There's a catch though: between the moment a user requests a page and the moment the server finishes revalidating it, the client receives old cached content. For many sites that's perfectly acceptable, but for dashboards, live feeds, and any page where users expect current information, that stale window can turn into a poor experience.

Hack Club's Scrapbook ran into exactly this problem. The site shows user updates in real time, so serving stale data wasn't an option. Simply switching to server-side rendering would have fixed the freshness issue, but at the cost of slowing down initial page loads — fetching large datasets before rendering adds noticeable latency. The solution the team landed on pairs ISR with SWR, Vercel's React Hooks library for data fetching. The client gets the fast, statically generated page first, then SWR fetches fresh data from an API route and updates the UI in place.

How SWR Complements ISR

SWR stands for stale-while-revalidate, a caching strategy defined in RFC 5861. The client renders with cached data immediately while a background request fetches the latest version. Unlike a one-shot revalidation, SWR can be configured to revalidate repeatedly — on a fixed interval, when the browser tab regains focus, when the network connection comes back, or programmatically from user interaction.

Using SWR alongside ISR in a Next.js app works like this:

  1. Next.js generates the page at build time using getStaticProps() and serves that cached version to the client.
  2. In the background, the server begins revalidating the static page per the ISR revalidate interval.
  3. On the client, after the page mounts, SWR issues a request to a Next.js API route that returns the same underlying data that getStaticProps() uses.
  4. When that request completes, SWR replaces the rendered data with the fresh response.

The performance difference can be dramatic. On Scrapbook, the ISR + SWR variant posted a Lighthouse speed index of 1.5 seconds, while the server-side rendered variant took 5.8 seconds and drew a warning about initial server response time. The tradeoff is that the page layout may shift a moment after load when new data arrives — an acceptable cost when the data update loop is handled cleanly, but one you should design around.

Choosing Whether SWR Fits

SWR is a strong fit for a handful of common site patterns:

  • Fast-moving live data. Sports scores and flight tracking need constant updates. Configure SWR to revalidate on a short interval, from one to five seconds.
  • Realtime feeds. Live news blogs, election coverage, and community scrapbooks benefit from periodic refresh. Use a higher interval, say 30 to 60 seconds, to keep API usage reasonable.
  • Passive data that users leave open. Weather pages and COVID case counters don't need constant polling. Revalidate on tab focus or network reconnect so users see current data the moment they come back.
  • Small interactive elements. For buttons like YouTube's Subscribe, revalidate programmatically after the user acts so the count reflects their contribution immediately.

These scenarios don't require ISR — SWR works fine on its own — but combining the two gives you fast initial loads without sacrificing freshness. On the flip side, SWR is wasted on data that barely changes; it adds network chatter and consumes mobile data for no benefit. And for pages behind authentication, server-side rendering is the safer route than ISR.

Building a Taxi Availability Page With ISR and SWR

To see the pattern in action, we'll build a small Next.js site showing how many taxis are currently available in Singapore, using the government's public taxi availability API. The project spans three files:

  • lib/helpers.js — exports getTaxiData(), which fetches and formats data from the external API;
  • pages/api/index.js — an API route whose default handler calls getTaxiData() and returns the result;
  • pages/index.js — the frontend, which uses getStaticProps() for ISR and SWR for client-side revalidation.

Sharing getTaxiData() between the API route and getStaticProps() prevents code duplication and guarantees both sides produce the same shape of data.

The Helpers File

Start by defining getTaxiData() in lib/helpers.js:

export async function getTaxiData(){
    let data = await fetch("https://api.data.gov.sg/v1/transport/taxi-availability").then(r => r.json())
    return {taxis: data.features.properties[0].taxi_count, updatedAt: data.features.properties[0].timestamp}
}

The API Route

Next, create the handler in pages/api/index.js:

import { getTaxiData } from '../../lib/helpers'
export default async function handler(req, res){
    res.status(200).json(await getTaxiData())
}

Nothing here is specific to SWR or ISR yet — the project structure is doing the heavy lifting so that the frontend can reuse the same fetch logic.

The Frontend With ISR and SWR

In pages/index.js, define getStaticProps() first. It calls getTaxiData() and returns the data along with configuration.

export async function getStaticProps(){
    const { getTaxiData } = require("../lib/helpers")
    return { props: (await getTaxiData()), revalidate: 1 }
}

The revalidate key in the returned object is what enables ISR. It tells the host that regenerating the page once per second is on the table; Next.js triggers that regeneration in the background whenever a client visits. Import SWR next:

import  useSWR from 'swr'

Then set up the React component that will receive props from getStaticProps():

export default function App(props){
}

With the props in hand, configure SWR inside the component:

const fetcher = (...args) => fetch(...args).then(res => res.json())
const { data } = useSWR("/api", fetcher, {fallbackData: props, refreshInterval: 30000})

SWR requires a fetcher argument so it knows how to retrieve data in your environment. In this case it's the standard utility from the SWR docs. The useSWR hook takes the API path to fetch from, the fetcher function, and an options object. That options object carries two settings:

  1. Fallback data. The data passed down from getStaticProps(), which guarantees the page renders immediately with content visible before the first revalidation completes.
  2. Revalidation interval. Tells SWR to refresh the data on a regular cadence.

Object destructuring pulls the latest data out of the hook. Finish by rendering it in a bit of JSX:

return <div>As of {data.updatedAt}, there are {data.taxis} taxis available in Singapore!</div>

The full source for this example is available in the nextjs-isr-swr-example repository. With that, the pattern is complete: ISR keeps the static page fast, SWR quietly keeps the displayed data fresh.

Further Reading

Smashing Editorial