/slug route hitting the cache less than 15% within 12 hours

I have a website that has over 600 articles. While investigating some huge usage spikes with a member of the Vercel team we noticed that articles were hitting the cache 8% of the time within 24 hours. Now originally I did have ppr enabled but I was informed that was causing the HUGE spike in fast origin transfer. I have since turned that off but that shouldn’t be the reason why /[slug] routes aren’t hitting the cache more than 15% of the time within 12 hours.


Now I do not have generateStaticParams() as doing that for all pages increases my build time from 2 minutes to over 8 minutes. Now I understand why because it has to fetch the data for everything page and render it. I also use react-tweet with redis to cache calls to X/Twitter which causes any page using that unable to be rendered statically.

What is the typical move here? Is it returning a generateStaticParams() with an empty array which will generate pages on-demand? Or is it just picking an arbitrary number of articles to render, say the last 50? What happens to the rest of the 550+ articles when they get visited if I go that route? Do they get generated on demand or are they all dynamic?

To give some more context, currently when running pnpm build for the site, this is the output for /[slug]

web:build: ├ ƒ /[slug]                                              4.83 kB         254 kB

When I do generateStaticParams() for every single article I get

web:build: ├ ● /[slug]                                                                    4.83 kB         254 kB
web:build: ├   ├ /no-11-furman-rolls-past-no-12-elon-in-first-round-of-fcs-playoffs-31-6
web:build: ├   ├ /multiple-factors-push-delaware-state-hawaii-out-of-linear-tv-limelight
web:build: ├   ├ /potato-bowl-bid-would-bring-nmsu-back-to-its-big-west-roots
web:build: ├   └ [+602 more paths]

What are the pros and cons to having things SSF vs dynamic? I use Sanity to power the site along with their SanityLive component to have live updates so I’m not sure if going SSG would impact that ability or not.

Hi @jamesrsingleton, thanks for the detailed write up.

I’d recommend following this guide Guides: ISR | Next.js because the first example is precisely your use case. Try experimenting with different amount for revalidate to see which one fits best with your use case. Higher the number, better the cache hit.

I think rendering the top 50 most visited pages during build should be a good middle ground and then rest of the pages are rendered on-demand by Next.js using dynamicParams set to true.

I haven’t used SanityLive but if it’s a client component then it shouldn’t have issues for the stale data.

Let me know if this works for you.

I don’t know if setting export const revalidate = 3600 or some number there would be ideal as ideally, articles don’t need to be revalidated after they are published unless it’s republished due to an update in the article. So putting a time there would just mean everything gets revalidated eventually even if it doesn’t need to.

Rendering the Top 50 most visited pages during build would probably work, I would need a way to figure that out as Sanity does not give that data nor is there a way from Next.js. Another alternative could be doing the latest 50.

I believe this is the SanityLive component and I would assume it’s a client component since it uses hooks. They recommend that it’s rendered at the root layout.tsx.

Also I updated from building all 600+ pages to building only the last 50 and set dynamicParams = true and the build time only decreased from 8 minutes to 5 minutes. Not the biggest drop but I guess it’s something.

I also wanted to add that after adding generateStaticParams() and export const dynamicParams = true to my /[slug] route that my home page has been aggressively cached and no longer gets updated from SanityLive. However, the routes built via generateStaticParams() are getting updated.

It looks like it’s being prerendered?

Also, I thought setting export const dynamicParams = true would then cache it if it’s been hit once and was not originally in the generateStaticParams() but I think that thought was wrong. How can I make sure that those are also eventually cached?

I noticed a URL outside the last 50 got visited so I decided to visit it to see if it would be cached and looks like it’s still running function invocations

Is that expected?

Hi @jamesrsingleton, thanks for writing in detail about your testing. Let’s keep the SanityLive feature aside for the moment and first clear out the ISR part.

So, I recreated a simple example repo where I created 4 /blog routes with different generation and caching modes. I was able to get pretty good cache hits as you can see the image below. It will definitely be lesser for a real world scenario.

In my experience, pages rendered during build time are always cached and didn’t lead to function invocation. Whereas the pages built on demand are also generated only once and then served from the Edge Cache. ff in the path means only first fifty pages are rendered during build.

Only time I saw function invocation was when using revalidate (ISR) and cache expired. You can see the 304 STALE response and others where the cache is hit first and then function invocation happens.

I hope this helps.

:thinking: so I must not be doing something correctly in my [slug]/page.tsx. Do I need to add the export const revalidate = 600; as well in order for the pages to be cached once they are generated? Also that 600 would only be 10 minutes whereas your comment says to Revalidate every year. If I put export const revalidate = 31,557,600 that should cache it for a year UNLESS SanityLive comes in and revalidates the path/tag I think.

I think the page setup looks good. All pre-rendered pages are cached and it seems like you are building all the pages in this setup so they all should be cached.

I don’t think you need revalidate for caching. In my example repo, I had 4 combinations:

  • pre-render all: always served from ISR cache
  • pre-render all with revalidate: always served from ISR cache but revalidated in background after 10 minutes
  • pre-render first 50: first 50 are served from ISR cache, rest are generated on demand and then served from cache
  • pre-render first 50 with revalidate: first 50 are served from ISR cache, rest are generated on demand and then served from cache but revalidated in background after 10 minutes

For your use case we should be doing #1 or #3 set up.

You can use revalidate to explicitly cache the content in Edge cache for an year if you like but I don’t see the need.

Then I think something isn’t working as expected.

So I went to our logs and found this route that was visited

I then visited it again two minutes after the first visit

And it does not seem to be hitting the cache? It looks like everything is being hit as if it’s the first time. This article is from 2024 so it isn’t going to be in the latest 50 articles. However, I would expect it to be cached?

And the route seems to be getting hit MULTIPLE times for some reason, and I doubt people are visiting a year old article. Looking at my analytics I am the only one who has visited it this week.

Furthermore, when looking at the logs in live I am not seeing other visits show up. For example, I hit the home page and that’s not showing up at all.

So with 8 visits to this page in a span of 6 minutes, none of them are cached

Here is another case where I just published this article and shared it to multiple social media platforms. The route was hit 29 times and every single one of them is a function invocation instead of prerendered.

Hi @jamesrsingleton, thanks for sharing these additional information.

Home page and other static routes are not shown in logs (after the first visit to that deployment) because they’re served from Edge Cache. I’m discussing this with the team because I also noticed this behavior and had some feedback.

Now coming back to the other routes:

  • what happens when you visit any of the latest 50 pages: are they cached or not?
  • what happens when you use the revalidate = 31_557_600?

Update:

Upon more research I found that Sanity docs
suggest to keep the stega: false for static generations.

Can you try turning this off?

So I can try turning it off. However, after doing a new build I am seeing prerender from ISR Cache. I will turn stega off though.

Also, I do see the home page from time to time show up in the logs


So setting stega: false does not seem to solve the non caching pages outside the latest 50


But can confirm that the latest 50 are still prerender

1 Like

@jamesrsingleton I see. It’s weird. I think it could be due to the revalidate option of sanityFetch, which defaults to 60. Can you try setting that number to a very high value, like one year?

Can you share the code on which you are testing this?

@anshumanb I think that is just an example usage of using client to then create your own sanityFetch. However, I decided to take a look at some examples that Sanity has template-nextjs-personal-website/app/(personal)/[slug]/page.tsx at main · sanity-io/template-nextjs-personal-website · GitHub and even using that the pages do not get cached via ISR after the first visit.

For example, I went to this url https://www.redshirtsports.xyz/the-case-for-marist-joining-the-northeast-conference-in-football a couple of times and each time it’s a function invocation and not served via ISR Cache.

1 Like

So I even added export const revalidate = 604800 to cache for a week because I thought that would then cache pages that were not in the generateStaticParams() after they were visited. However, I am not seeing that.

Here is me visiting a page

And then some time later, I go and visit the page but it’s not in the ISR Cache

Hi @jamesrsingleton, I see. Can please share your public repo or a minimal reproducible example. That will let us all work together from the same code to figure out what’s going wrong.

So, a sample project with sanity setup (throwaway project) like yours. This way I can test and validate myself and then share with you. Otherwise the feedback loop is too long.

So I ended up having to add export const dynamic = 'force-static' to my /[slug] page to get things to then statically render after being visited. However, it does say from Edge Cache vs ISR Cache and I honestly don’t know the difference.

1 Like

Hi @jamesrsingleton, thanks for updating here what worked for you. Edge cache is used for static assets, since you’ve set force-static there will be no ISR in place. In terms of caching they both are same ie you will get a cached response.

I’m trying to recreate this behaviour.

1 Like

Got it. I know I need to recreate a repo, I have just been busy trying to ship some updates to this website :sob:

1 Like