Why Your API Calls Are Slow and How to Fix Them in Nextjs 15

When I first started working with React and then Next.js, I expected my pages to load instantly. But as projects grew, I noticed something frustrating — even when my backend was fine, the frontend felt slow. Users were waiting on spinners, or worse, blank screens.
At first, I thought it was just the backend response time. But once I dug deeper, I realized most of the delays were caused by how I was fetching and rendering data. Next.js 15’s App Router changes caching and fetching defaults quite a bit, so I had to adjust how I think about API performance. In this post, I’ll walk through the mistakes I made, what slowed my apps down, and the fixes that actually worked.
Measuring Before Fixing
One mistake I made in the beginning was jumping straight into fixes without knowing what was wrong. Now, before doing anything, I open Chrome DevTools and check the Network tab to see how long the API actually takes. Sometimes the backend is fine, but my frontend is chaining requests.
I also use the Performance panel to see hydration time and rendering delays. In Next.js, logging server-side fetch times also helps. Once I know where the slowdown is — backend, frontend fetching, or rendering — I start fixing.
Sequential Requests Slow Everything Down
In my early builds, I fetched data sequentially. For example, I needed user info, transactions, and notifications. My code looked like this:
The problem? Each call waited for the previous one. If each took 200ms, the total could easily cross 600–700ms.
The first big improvement came when I switched to parallel fetching:
This reduced the total wait to just the slowest call. In one dashboard, that single change made the page feel twice as fast.
Overfetching Too Much Data
Another issue I ran into was pulling far more data than I actually needed. For example, fetching entire user profiles with dozens of fields when all I needed was a name and avatar. This bloated the response size and slowed down both the network and the frontend parsing.
To fix this, I started being intentional about queries. If the API supported field filters, I added ?fields=name,avatar
. For lists, I added pagination and limit=10
. In one case, switching from full profile objects to just the essentials cut the payload from 200kb to under 20kb.
The lesson I learned: send only what the UI really needs.
No Caching or Revalidation
When I checked my logs, I noticed something shocking: the same endpoint was being hit multiple times for the same user within seconds. That’s wasted bandwidth and server load.
On the client side, I began using SWR or React Query, which cache responses and revalidate in the background. This meant users instantly saw cached data on repeat visits, with updates fetched quietly in the background.
On the server side, Next.js 15 introduced new caching behavior. By default, fetch
is no longer cached — it behaves like cache: 'no-store'
. This was different from older versions of Next.js. To fix this, I explicitly set caching when I knew data wasn’t changing on every request:
For data that changes but not too often, I use revalidation:
This caches the response and revalidates every 60 seconds.
I also started wrapping heavy DB calls in unstable_cache
so they wouldn’t rerun on every request. These small caching tweaks reduced redundant calls and made pages load faster without changing the UI.
Fetching in the Wrong Place
At first, I put almost all fetches inside useEffect
in client components:
This meant the user loaded the page, saw nothing, and then waited again for data. It felt sluggish, even if the backend was fast.
In Next.js 15, I shifted most fetching to Server Components. For example, in app/page.tsx
:
Now the server fetches the data before sending HTML to the client. The user sees meaningful content immediately. If I need dynamic or user-specific data, I wrap that part of the UI in <Suspense>
with a fallback, so at least the rest of the page is ready.
Bundle Size and Hydration Delays
Even when API calls were fast, sometimes the app still felt slow because hydration and rendering blocked interactivity. This usually happened because my JavaScript bundles were too large.
The fix was to split code and lazy-load nonessential parts. In Next.js, dynamic imports helped:
This kept initial loads lightweight. I only pulled in heavy charts after the main UI was interactive. After making these changes, the “frozen screen” effect during load almost disappeared.
API Routes Adding Overhead
Another hidden delay came from how I structured APIs. Sometimes I used Next.js API route handlers (app/api/...
) as proxies to my backend. While convenient, they introduced extra latency, especially with serverless cold starts.
In Next.js 15, route handlers also default to no caching. If I wanted them cached, I had to explicitly declare it:
This forced caching for stable routes. For truly dynamic data, I kept them uncached but optimized the backend instead. The key lesson: avoid unnecessary proxy hops when possible.
Lack of UI Feedback
Sometimes even when requests were quick, the app felt broken because users stared at blank space. In one of my apps, users thought the API was slow, but the truth was I didn’t add a loading state.
Now I always provide feedback. With React Suspense in Next.js 15, it’s straightforward:
Even if the data takes a second, users see placeholders immediately. It improves perceived speed, which is just as important as actual speed.
Real Example: Fixing My Dashboard Page
One of my projects had a dashboard that showed user info, transactions, and notifications. Initially:
- All data was fetched on the client with
useEffect
. - Calls were sequential.
- No caching was in place.
- A heavy chart library loaded on the first render.
- No loading placeholders existed.
It felt painfully slow, taking nearly 3 seconds to be usable.
I refactored step by step:
- Moved fetching into a Server Component with
Promise.all
. - Added
cache: 'force-cache'
and revalidation to control caching. - Wrapped expensive DB calls in
unstable_cache
. - Lazy-loaded the chart library with dynamic imports.
- Added skeleton placeholders with Suspense.
After that, users saw meaningful content in under 700ms, with charts loading smoothly afterward. The difference was night and day.
What I Learned
If your API calls feel slow in Next.js 15 or React, it’s often not the backend alone. Slowness usually comes from:
- Fetching data sequentially instead of in parallel.
- Requesting too much data.
- Forgetting to cache or revalidate.
- Fetching everything client-side instead of using Server Components.
- Loading huge bundles at once.
- Adding proxy layers that create latency.
- Ignoring loading states and user feedback.
The fixes aren’t glamorous, but they work. Measure first, then adjust caching, fetching, and rendering strategies. Always think about both actual speed and perceived speed.
Since moving to Next.js 15 App Router, being explicit with caching (force-cache
, revalidate
, unstable_cache
) and taking advantage of Server Components have been the biggest improvements in my projects.
Whenever I face “slow APIs” now, I go through this checklist instead of panicking. Almost always, the bottleneck is in one of these spots — and fixing it makes the app feel snappy again.