If you’re here, you’ve likely come across **SSR** (Server-side Rendering), **SSG** (Static Site Generation), and **CSR** (Client-side Rendering). Here’s a quick look at what they mean:
  • SSR: In server-side rendering, each page request triggers a server-side render (often involving API calls) before the HTML is sent back to the client.

  • SSG: Static site generation pre-renders all pages during the build step. When a page is requested, the server simply returns the pre-generated static file for that route.

  • CSR: Client-side rendering performs the page rendering and any associated API calls at runtime, directly on the user’s device.

What is ISR and what issue does it address?

Consider an e-commerce platform with a large catalog and a huge user base. Each product has its own details page. Because this is a store, you'd want those pages server-side rendered—probably via Angular Universal—so that crawlers can index the content for SEO. When a user directly hits one of those product pages, the server must fetch the data from the backend, render the HTML, and deliver the page. This entire sequence repeats for every single visit. Now picture thousands of customers landing on the same product page simultaneously. The server (along with the backend) is likely to be overwhelmed, forcing you to scale up resources. Ironically, the server would be performing the same redundant work for every client just to serve them an identical page.

The contribution of SSG so far

With static site generation, each product details page was pre-rendered at build time. The data-fetching step was done just once, and users got static files. This drastically reduced the runtime load on the server, which only needs to serve files without any additional processing. That approach worked well—until the product information changed. At that point, you had to rebuild, regenerate all pages, and redeploy, just to update a single price. What if you needed to update 100 prices every hour? You'd be stuck repeating the entire build-and-deploy cycle countless times.

This is where ISR comes in

ISR merges the best of both SSR and SSG. The first time a page is requested, it’s rendered on the server and stored in the cache. All subsequent requests for that page are served straight from this cached copy. Refreshing the cache is handled either on a set time interval or through on-demand regeneration.

ISR is essentially SSG, but executed at runtime!

Ready? Let’s implement ISR in Angular

To begin, you’ll need an Angular application with Angular Universal already configured. Next, install the ngx-isr package—a library I’ve created. **ngx-isr** provides an easy-to-use and extensible API (inspired by Next.js) for managing ISR in your Angular app. npm install ngx-isr Once installed, a few small tweaks are needed. Start by setting up an ISRHandler instance in your server.ts file:
import { ISRHandler } from 'ngx-isr';

const isr = new ISRHandler({
  indexHtml, // <-- Is the path to the index.html
  invalidateSecretToken: 'MY_TOKEN', // replace with env secret key
  enableLogging: !environment.production
});
Then, swap out the default Angular SSR call for ISR rendering. Replace the existing code:
server.get('*',
  (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  }
);
with this:
server.get('*',
  // Serve page if it exists in cache
  async (req, res, next) => await isr.serveFromCache(req, res, next),
  // Server side render the page and add to cache if needed
  async (req, res, next) => await isr.render(req, res, next),
);

Note: The ISRHandler automatically provides the APP_BASE_HREF token. If you pass providers to ISRHandler’s methods, you’ll need to provide this token yourself.

Next, add the endpoint handler for invalidation:
server.get(
  "/api/invalidate", 
  async (req, res) => await isr.invalidate(req, res)
);
Finally, import the NgxIsrModule into your AppServerModule:
import { NgxIsrModule } from 'ngx-isr'; // <-- Import module

@NgModule({
  imports: [
    ...
    NgxIsrModule  // <-- Use it in module imports
  ]
})
export class AppServerModule {}

Importing the module registers the NgxIsrService, which listens to route changes—only on the server side. This ensures no extra logic is bundled for the browser.

**That’s all the setup you need!**

How to use it

Simply define a revalidate key in the route’s data:
{
  path: "example",
  component: ExampleComponent,
  data: { revalidate: 5 }
}

Note: Routes without a revalidate key in their data won’t be handled by ISR and will fall back to the standard Angular SSR pipeline.

To manually trigger a regeneration, send a GET request to the /revalidate endpoint:
GET /api/invalidate?secret=MY_TOKEN&urlToInvalidate=/example

How it works

The revalidate value in route data defines the time interval ISR uses to determine when a route should be regenerated. Here are your options:
  • No value set: The route won’t be cached, and it will always be server-rendered (like traditional SSR).
  • 0: The first request is server-rendered; every subsequent request is served from the cache (mimicking SSG).
  • A positive number (e.g., 5): The first request is server-rendered, and the cache is refreshed every 5 seconds (counted from the last request).
**A more advanced example:**
const routes: Routes = [
  {
    path: "one",
    component: PageOneComponent,
  },
  {
    path: "two",
    component: PageTwoComponent,
    data: { revalidate: 5 },
  },
  {
    path: "three",
    component: PageThreeComponent,
    data: { revalidate: 0 },
  }
];
  • The one path is never cached; each visit triggers a fresh server render.

  • The two path: the first request is rendered on the server and cached. The second request serves the cached page and adds the URL to a queue to re-render after 5 seconds. On the third request, if the regeneration completed, the new page is delivered; otherwise, a cached version is provided.

  • The three path: only the first request is rendered and cached; the rest are served from memory. To update the cache, you must manually request the /invalidate API route.

Output

Run npm run dev:ssr to launch the application.

Open the browser's developer tools.

Observe that the Last updated timestamp shifts according to the revalidate value you specified.

Incremental Static Regeneration for Angular — figure 1

Limitations of ISR?
Whenever the source is modified, a fresh build and deployment are required. ISR only responds to backend data updates, which is its intended scope.

That's all! Thank you for sticking with this extensive guide!

If you enjoyed it, drop a thumbs up and star the GitHub repository.
Should the library prove valuable to you or your company, feel free to buy me a coffee 😊.