Notes from optimizing a blog page.
The loading UI on a computer science lecture page was showing for far too long relative to the content, so I decided to optimize it.
The lecture page looked like this. Which part do you think was causing the slow load?

I assumed the most complex component — the logic gate simulator at the bottom center — was the culprit. Everything else was either plain text or components with minimal JavaScript.
So I started optimizing that component. The original implementation looked like this:
// flow.tsx
'use client';
// ...tons of imports
export function Flow(props) {
...
}
First, I extracted it into a separate file with dynamic import and Suspense so its loading wouldn’t block the entire page:
// dynamic-flow.tsx
"use client";
const Flow = dynamic(() => import("./flow"));
export function DynamicFlow(props) {
return (
<Suspense>
<Flow {...props} />
</Suspense>
);
}
But this would cause CLS once loaded, so I added a fallback matching the original component’s height:
// dynamic-flow.tsx
"use client";
const Flow = dynamic(() => import("./flow"));
export function DynamicFlow(props) {
return (
<Suspense fallback={<Fallback height={props.height} />}>
<Flow {...props} />
</Suspense>
);
}
Sadly, there was no improvement in page load time.
Only then did I get suspicious and try removing the simulator component entirely. The page loaded just as slowly. This meant that even reducing the simulator’s load time to zero wouldn’t make the page feel any faster.
What could possibly be slow on such a simple page… While puzzling over this, I spotted an alarming log in Next.js:
GET /cs 200 in 1400ms
│ GET https://api.resend.com/audiences/b8cb47fc-.. 200 in 1358ms (cache skip)
│ │ Cache skipped reason: (auto no cache)
I was using Resend to fetch the newsletter subscriber count, and that API request was taking over 1 second. In other words, this was the culprit:
Join 366 others.
In a SPA, such network requests typically happen in useEffect after the
initial render, so they wouldn’t block the page. But unfortunately I was using
Server Components.
export default async function Page() {
const data = await getSubscriberCount();
...
}
The browser had to show a loading fallback for the entire page until
getSubscriberCount resolved on the server.
Once I identified the cause, the fix was easy. Just like the simulator component, I extracted the subscriber count into its own component and wrapped it in Suspense.
Amdahl’s Law comes up when learning about multiprocessors in computer architecture. Here’s the definition from Wikipedia:
Amdahl’s law is used to find the maximum expected improvement to an overall system when only part of the system is improved.
Try the interactive example below. Notice that when the proportion of the improvable part is small, even enormous effort yields little overall improvement:
When 40% of the system is sped up by 2.0x, the overall speedup is 1.3x. Even with infinite speedup, the overall improvement cannot exceed 1.7x.
If you replace “computer system” with “rendering process” in the Wikipedia definition, it perfectly describes what happened. Just like optimizing the simulator — which made up a tiny fraction of the total work — barely improved overall performance.
Find the cause before optimizing. Avoid adding complexity for nothing. Don’t blindly apply optimization tips from the internet. Look at the process that produces the result, not just the result itself.