Next.js Discord

Discord Forum

Performance optimization on SSR blog page

Answered
Asian paper wasp posted this in #help-forum
Open in Discord
Asian paper waspOP
My site:
https://resume-git-feature-blog-meta-and-share-mwskwong.vercel.app/blog

Context:

I'm POCing on adding like count and view count to my blog pages. Since the counts are dynamic, I cannot use generateStaticParams anymore, so the pages turned from SSG to SSR with data fetching cache. As I expected, the page navigation becomes less responsive due to the page now needs to be generated in runtime. I wonder if is there anything that can be done to improve it.

Tech related info
UI part:
- MUI (which make use of runtime CSS)
- Next MDX Remote + Rehype Prettier Code (with a workaround to make it work with SSR)

How the counts work:
They are stored in Vercel Postgres, and I query the DB using Prisma. This query is not cached since the view count always changes. Both the DB and serverless function region are set to Singapore, where my target audience is Hong Kong. The DB has limited choice in terms of hosting region, in which Singapore is the closest to HK.
Answered by Asian paper wasp
I'm unable to find a solution for this, but at least I can work around it with better UX.

I added a nprogress bar, would you mind taking a look at whether it is prominent enough to indicate the SSR blog pages are loading/transitioning?

DEV build of blog listing page, click on any blog card to observe the behavior: https://resume-git-develop-mwskwong.vercel.app/blog
View full answer

81 Replies

Asian paper waspOP
One thing I'm thinking is since the blog is mostly static, is it possible to have the blog content SSG, but the counts SSR?
I know I can make the counters CSR, but I'm wondering if is there a better option to do so.

I noticed https://leerob.io/blog is pretty good in performance, but I'm not too sure where the bottleneck is on my side
You don't have to ensure the users see the "latest data" right? ISR is pretty enough for that (revalidations)
Currently, you can't configure to cache database queries. (unstable_cache is not stable yet)
Hence, you may add revalidate to segment config
Asian paper waspOP
I'm currently testing where the bottleneck is. Is it data fetching or rendering. So far, the differences with or without fetching counter doesn't impact the performance too much, so it could be the latter
check the delay between printing the log and appearing in the browser
if its long probably because theres await operation in child server component
coz RSC rendering is blazing fast unless you do like >1000 elements lol
also in my end, ur website and leerob's only differs by like what, 100-200 ms interms of request time
so its still pretty fast in my book
urs 469ms
leerob's 300ms
not bad
mine is like 900ms :derp:
@aardani also in my end, ur website and leerob's only differs by like what, 100-200 ms interms of request time
Asian paper waspOP
You sure you are checking /blog/:slug, not /blog?
im checking /blog/[slug]
Asian paper waspOP
Hmm... interesting. Sometimes it slow on my end
404ms
@Asian paper wasp Hmm... interesting. Sometimes it slow on my end
Due to cold start? It is common for serverless applications
@fuma Due to cold start? It is common for serverless applications
Asian paper waspOP
I tested it with https://tools.keycdn.com/performance.
It seems that the TTFB of my site is indeed a lot slower. I tested a few times to make sure it is not due to cold start.

That's 100 - 200ms vs something around 400 - 600ms
Asian paper waspOP
I commented out the counts API call, which is supposed to be fetched on every request. And that doesn't have much impact on the performance. And I also tried to comment out rehype-prettier-code, and that seems to have a slightly higher impact.

I guess the bottleneck is the fact that the page needs to be dynamically rendered on every request, and the rendering process is heavier than I imagined.
I have an RSC using Bright that uses dangerouslySetInnerHTML to format the codes
Sorry correction: Bright doesn't use dangerouslySetInnerHTML but my code-snippet doesn't take that long to render
Asian paper waspOP
I'm unable to find a solution for this, but at least I can work around it with better UX.

I added a nprogress bar, would you mind taking a look at whether it is prominent enough to indicate the SSR blog pages are loading/transitioning?

DEV build of blog listing page, click on any blog card to observe the behavior: https://resume-git-develop-mwskwong.vercel.app/blog
Answer
Asian paper waspOP
Also, how can I close this help post, although this is a workaround, not solution.
yeah but server action doesn't work in ISR (yet)
Asian paper waspOP
Also, view count can look weird with ISR. Let's see how it goes
@fuma You should actually use ISR for this. Time-based revalidation is ideal for this use case
Asian paper waspOP
Just in case, if I'm not mistaken, ISR in app dir is enabled by setting revalidate > 0 in route segment config right?
Yeah
Also remember to remove everything that disables cache
Asian paper waspOP
Will give that a try.

One think I don't really understand is that how come Leerob's blog, which is also using SSR instead of ISR or SSG, is having decent performance, unless there are some scaling features gated behind the paywall on Vercel.
Maybe they are using some sort of redis cache to fasten up the r/w operation
Asian paper waspOP
OK, I think I found the bottleneck. It is really the rendering part. Now the question is, whose fault is that? MUI? next-mdx-remote? Or rehype-pretty-code?
from initial request to the part just before render()
Asian paper waspOP
Just a few hundred milliseconds.
Also tested the TTFB with and without MDXRemote (which also renders a bunch of MUI components and rehype-pretty-code) and the differences are significant
Asian paper waspOP
I do map quite a few HTML elements to MUI components, so that might be the cause. I've also tried to remove rehype-pretty-code from the rendering process (while keeping the import statements) and that around 30% improvement in TTFB
I'm now trying and see what will happen if I just render MD to plain HTML, but re-enable rehype-pretty-code.
its insane how long ur blog takes to load even if its just a short length :thinq:
i like how the "heart" dissapears when you refresh it
Asian paper waspOP
That's what happens when I want to implement a like feature without asking the user to log in (yes, I can do that with OAuth, but that's a different story). The alternative is local storage. But I'm not there yet
Asian paper waspOP
For the record, differences in TTFB between having MD --> MUI component mapping + rehype-pretty-code vs just MD to plain HTML
And the same blog in SSG...
Sigh... I might go the SSG + fetching view and like counts on client side path
Why don't use ISR? It's better than client-side data fetching
coz he said view counts that are ISR'd are "weird"
@fuma Why don't use ISR? It's better than client-side data fetching
Asian paper waspOP
coz he said view counts that are ISR'd are "weird"
Yeah, that's my concern. But ISR is still one of the candidates. I haven't decided yet actually. But I noticed a few article sites are using SSG + client-side fetching ways to do stuff like this.

What do you think? And I would to know why you think ISR is better.
imo views that are regularly updated like every 10 minutes are fine. its not a crucial part of the applciation.
Imagine if Youtube's view count are "real time" :this_is_fine:
How does it come to be weird? I don't think updating the view counts in real-time is a good choice considering you won't need that actually
Asian paper waspOP
Yeah, you guys are right. YouTube also has a stale view count.
How long do you think the revalidation time should be? 3 hr?
Twice a day? So that you can check once at day an once at night haha
10 minutes ig?
i was thinking if you have like 200 articles and it refreshes every 10 minutes it would take up your quota
unless maybe thats not an issue
Asian paper waspOP
Just make sure I understand how caching works, let's say the blog page is now making use of generateStaticParams + revalidate, this essentially makes the blog pages ISR right?

I'm thinking whether I should revalidate the page if some one clicks the like button. Let's say I've assigned a tag to the like count data fetch, when I revalidate this tag, it should trigger the page revalidation and rebuild the page, right?
generateStaticParams
or
revalidate
or
'force-static'

will make it ISR, yes
the latter approach is good too. I have a /articles/[slug]/revalidate path to trigger ODR
Asian paper waspOP
Wait I only need generateStaticParams? You mean I can make use of unstable_cache + revalidate for getLikeCount and it will also trigger page rebuild?
yeah IIRC (havent test out) if data cache inside a static route needs revalidating it will rebuild the entire page
but i 80% beileve thats what the docs said
build cache/route cache is dependent on data cache too
So ultimately the duration of the revalidation time of a Route (on which that it uses build cache)
is
the lowest number of revalidation that any fetch inside the route uses.
Asian paper waspOP
@aardani @fuma Thanks for the suggestion on ISR, this does seem a way better UX.
https://resume-git-feature-isr-blog-mwskwong.vercel.app/blog

I think is better to keep the nprogress bar though. When I visit the page while it is revalidating, it falls back to SSR and the page is dead slow.
Asian paper waspOP
I mean, that's how ISR works right? It serves stale data when it is working in background
yep
You can learn about this in the caching section of docs, it has clearly explained how it works
@fuma You can learn about this in the caching section of docs, it has clearly explained how it works
Asian paper waspOP
I just did it again. That's a lot of caches and is more complicated than I initially anticipated.

You you mind confirming my understanding of the following:

In a client component, if I invoke a mutation, and it calls revalidateTag after it is done, it will invalidate the corresponding data cache, and in turn, invalidate the full route cache of the corresponding route as well. But this is not enough, we also need to invalidate the router cache. That's why we should call router.refresh() after the mutation is successful in the client component.

Is this correct?
Im not sure about calling the revalidate function in client components, mayhbe others can chime in
but your explanation does make sense
@aardani Im not sure about calling the revalidate function in client components, mayhbe others can chime in
Asian paper waspOP
Just to clarify, the mutation is done by calling a route handler, which updates the DB and call revalidateTag after it's done
ah yeah so revalidateTag is stil being called in the server right? in that case yeah it has to refresh it
but im not sure if its the accurate behavior when used in server action. (theres an entire article about it, and i haven't read much into it)
Asian paper waspOP
What I understand is I don't need to call router.refresh() when using server actions
Yeah revalidate is a server side only function, while router cache is only available on client side