Back to blogs

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

October 1, 2025
7 min read
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:

const user = await fetchUser();
const tx = await fetchTransactions(user.id);
const notifs = await fetchNotifications(user.id);


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:

const [user, tx, notifs] = await Promise.all([
fetchUser(),
fetchTransactions(),
fetchNotifications()
]);


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:

const res = await fetch(API_URL, { cache: 'force-cache' });


For data that changes but not too often, I use revalidation:

const res = await fetch(API_URL, { next: { revalidate: 60 } });


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:

useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);


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:

export default async function Page() {
const res = await fetch(API_URL, { cache: 'force-cache' });
const data = await res.json();

return <MyUI data={data} />;
}

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:

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), { ssr: false });


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:

export const dynamic = 'force-static';

export async function GET() {
const res = await fetch(API_URL);
return Response.json(await res.json());
}


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:

<Suspense fallback={<LoadingSkeleton />}>
<UserProfile />
</Suspense>


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:

  1. All data was fetched on the client with useEffect.
  2. Calls were sequential.
  3. No caching was in place.
  4. A heavy chart library loaded on the first render.
  5. No loading placeholders existed.

It felt painfully slow, taking nearly 3 seconds to be usable.


I refactored step by step:

  1. Moved fetching into a Server Component with Promise.all.
  2. Added cache: 'force-cache' and revalidation to control caching.
  3. Wrapped expensive DB calls in unstable_cache.
  4. Lazy-loaded the chart library with dynamic imports.
  5. 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:

  1. Fetching data sequentially instead of in parallel.
  2. Requesting too much data.
  3. Forgetting to cache or revalidate.
  4. Fetching everything client-side instead of using Server Components.
  5. Loading huge bundles at once.
  6. Adding proxy layers that create latency.
  7. 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.

nextjs api performancenextjs 15 api callsslow api calls reactfix slow api requests nextjsoptimize api calls in reactnextjs 15 app router cachinghow to improve api speed nextjsreact api performance optimizationnextjs server components api fetchreduce latency in nextjs appbest practices for nextjs 15 apispeeding up api calls in react nextjsoptimize fetch in nextjs 15nextjs caching and revalidation

Recent Blogs

View All