Improving Next.js Performance Through Rendering and Data Fetching
How I investigate Next.js performance across Server Components, client rendering, data fetching, TanStack Query and large interactive interfaces.
Updated October 22, 2026

A slow Next.js application needs a diagnosis before it needs another memoization hook. The delay can come from a server data request, a large client bundle, browser rendering or an interactive query. Each boundary needs to be measured separately.
I start with one reproducible interaction and measure it. That keeps an optimisation tied to a user visible result rather than a growing collection of memo wrappers.
1. Identify which part is slow
Use the browser’s performance tools and React DevTools Profiler together. React profiling helps identify rendering work; the browser trace also shows scripting, layout, painting, and network activity.
| Symptom | What to inspect first |
|---|---|
| Slow initial load | Bundle size, images, server response, and request waterfalls |
| Slow typing or clicking | Main thread work and component updates |
| Slow navigation | Route bundles, data fetching, and loading boundaries |
| Slow scrolling in a large list | Rendered item count, layout, and painting |
Use a production like build for user facing measurements. Development checks and tooling add overhead, so a development render count alone is not a reliable measure of production performance.
2. Understand what a render means
In the App Router, layouts and pages are Server Components by default. Keep suitable data access on the server and use Client Components for state, effects and browser interactions. A smaller client boundary can reduce the JavaScript shipped for a feature, but server queries and waterfalls still affect the time a user waits.
When state changes, React may render the component and its descendants. Rendering calculates the desired UI; committing applies the necessary changes to the DOM. A component rendering again does not mean every DOM element is recreated.
Start by checking state ownership and unnecessary effects. Avoid copying values into state when they can be derived during render, and keep a frequently changing value close to the components that use it.
If two components need the same selection, their nearest common parent is a reasonable owner. Moving the selection into one child would break that coordination; locality is useful only when it matches the data’s actual consumers.
3. Use memoization where the measurement supports it
memo can let React skip some parent driven renders when props remain equal. It does not prevent updates caused by the component’s own state or subscribed context, and it is a performance optimisation rather than a guarantee.
'use client'
import { memo } from 'react'
const CustomerName = memo(function CustomerName({ name }) {
return <span>{name}</span>
})
Passing the specific primitive value needed by the component often makes the boundary easier to understand than passing a large object that changes frequently.
Be careful with custom comparators
A comparator that checks only user.id is incorrect if the component also displays user.name: the ID can stay the same while the name changes. A custom comparator must account for every prop that affects output or behaviour, including callbacks.
The React memo documentation explains these limitations. When React Compiler is enabled for a project, it can reduce the need for manual component memoization.
4. Cache an expensive calculation when useful
useMemo caches the result of a calculation between renders when its dependencies have not changed. Use it after measuring an expensive calculation or identifying a useful identity boundary.
'use client'
import { useMemo } from 'react'
function ProductList({ products, category }) {
const visible = useMemo(
() => products.filter(product => product.category === category),
[products, category]
)
return (
<ul>
{visible.map(product => <li key={product.id}>{product.name}</li>)}
</ul>
)
}
For a small array, the filter may be cheaper than the complexity of memoization. Likewise, useCallback is useful when a stable function reference helps a memoized consumer or a dependency boundary. Wrapping every callback does not automatically make an application faster.
5. Split code at useful loading boundaries
A large editor or chart can be loaded when the user reaches that feature. In Next.js, next/dynamic can load an interactive component separately from the initial bundle:
'use client'
import dynamic from 'next/dynamic'
const ReportEditor = dynamic(() => import('./ReportEditor'), {
loading: () => <p>Loading the report editor…</p>,
})
export function ReportPage() {
return <ReportEditor />
}
The imported module needs a default component export for this pattern. Next.js already handles route based loading boundaries, so inspect the generated bundles before adding another layer. If a library truly requires browser APIs during module evaluation, evaluate a Client Component wrapper and ssr: false for that specific feature. Avoid disabling server rendering as a blanket performance fix.
6. Give browser data an explicit freshness policy
For interactive dashboards, TanStack Query can coordinate caching, refetching and mutations. Define query keys that include the workspace, filters and page, and choose a staleTime appropriate to the data. Repeated requests caused by unclear cache ownership can be more costly than a small component render.
Avoid fetching the same data independently in a Server Component and again on mount without a reason. Choose server rendering, client fetching, or prefetching with hydration according to the workflow. Authentication changes also need a deliberate cache lifecycle so one user's data is not retained for another session.
The Next.js state and data guide includes a TanStack Query example and explains those boundaries.
7. Reduce the amount of work rendered
For a very large list, pagination or virtualisation can reduce the number of DOM elements. Choose based on the task: pagination provides stable pages, while virtualisation keeps a moving window of rows mounted.
A virtualised list still needs stable item keys, appropriate sizing, and accessible interaction. Test keyboard navigation, variable height content, and focus behaviour. Check the installed library’s documentation because APIs vary by version.
Verify the improvement
Repeat the same interaction with comparable data and environment. Confirm that the change improved the measured problem and did not create stale values, missed updates, or a confusing loading state.
The useful habits are to measure, change one relevant boundary, and measure again. Keep the optimisation when it makes the application easier to use and its behaviour remains clear.