Skip to content
loke.dev
Several server request paths joining into one shorter route before the page renders

Fix Slow Next.js Server Actions and Data Fetching

Find why a Next.js App Router page or Server Action feels slow, then fix waterfalls, duplicate queries, uncached fetches, and slow post-action refreshes.

Published By Loke3 min read

A slow Next.js page is not always a slow database. Sometimes it is three perfectly normal requests waiting in a line because the component tree made them do that.

Server Actions have another version of the same problem. The action finishes, then the UI waits for a refresh that does more work than the mutation itself.

The short fix

First measure where the time goes. Then run independent reads together, cache data that does not need to be fresh on every request, and invalidate only the paths or tags affected by a mutation.

Find a request waterfall

export default async function Page() {
  const user = await getUser()
  const projects = await getProjects(user.id)
  const activity = await getActivity(user.id)

  return <Dashboard user={user} projects={projects} activity={activity} />
}

Projects and activity both need the user, so the first request is a real dependency. But once you have the ID, the other two reads can run together.

export default async function Page() {
  const user = await getUser()
  const [projects, activity] = await Promise.all([
    getProjects(user.id),
    getActivity(user.id),
  ])

  return <Dashboard user={user} projects={projects} activity={activity} />
}

Do not use Promise.all when one query needs the result of another. Parallel work is useful only for independent work.

Do not make every fetch uncached by accident

Current Next.js docs say fetch requests are not cached by default in the current App Router model. That is often the right choice for private or rapidly changing data, but it is a bad default for a public list that changes once an hour.

Choose the freshness you actually need. Cache public data with use cache or the framework cache APIs used by your version. For private data, keep the request dynamic and spend time on the query and network path instead.

A cache is not a substitute for authorization. Never put user-specific data in a shared cache key.

Server Actions are mutations, not a general data loader

A Server Action is an async server function called over a POST request. It is a good fit for a form or a button that changes data. It is usually not the best place to load all the data for a page.

'use server'

import { revalidatePath } from 'next/cache'

export async function renameProject(formData: FormData) {
  const id = String(formData.get('id'))
  const name = String(formData.get('name'))

  await db.project.update({ where: { id }, data: { name } })
  revalidatePath('/projects')
}

The action changes one thing, then tells Next.js which page is stale. Revalidating the whole site after every small edit creates extra work and makes the next request pay for it.

Watch for hidden database waterfalls

An ORM call inside a loop is still a request per item. Load the related rows in one query, use a join or an IN filter, and select only the columns the page needs. The framework cannot fix an N+1 query for you.

const projectIds = projects.map((project) => project.id)
const activity = await db.activity.findMany({
  where: { projectId: { in: projectIds } },
  select: { projectId: true, action: true, createdAt: true },
})

Measure the user path, not only the function

Add a timer around the page request, the slow query, and the action. Check the browser network panel too. A fast action can still feel slow if the page refresh downloads a large layout and a long list again.

A simple test plan

1. Record a cold page and a repeat page.
2. Log every server request and database query with a duration.
3. Parallelize only independent reads.
4. Decide which reads can be cached and for how long.
5. Revalidate the smallest affected path or tag after a mutation.
6. Test a slow network and a large dataset, not only your laptop.

Does this request depend on per-user request data?
Yes

Read cookies or headers where needed. Treat the route as dynamic.

No

Set explicit cache and revalidation rules around the data that changes.

Not sure

Measure the route and write down the expected freshness before shipping.

Choose between fresh data, cached data, and a revalidated path based on the reader and mutation path.

The fastest fix is often not a new library. It is removing one accidental await, one query inside a loop, or one cache invalidation that is much larger than the change that caused it.

Sources and further reading

  1. Fetching data · Next.js
  2. Updating data with Server Actions · Next.js
  3. Data fetching patterns · Next.js