# React and Next.js mistakes that pass tests and slow pages

> Sequential awaits, oversized client boundaries, eager heavy imports, images without size, unstable props and missing Suspense: how to spot and measure each.

Published 2026-10-30 · https://aiskills402.com/blog/react-nextjs-performance-mistakes

Performance bugs in React code rarely announce themselves. The page renders the right content, every unit test is green, and the type checker is quiet; the cost shows up only as seconds a visitor waits or kilobytes a phone has to download. Six patterns account for a large share of this in App Router projects: awaits that run one after another, a client-side marker placed too high in the tree, heavy libraries loaded before anyone needs them, images with no reserved space, props that look the same but are new on every render, and slow data with no streaming boundary. This guide explains what each one costs, how to recognise it in code, and how to confirm a fix with a measurement and not a feeling.

## Sequential awaits: the request waterfall

The Next.js documentation draws a line between data that depends on earlier data and data that does not. When one request needs the result of another, such as an id from the first call, the order is forced. When requests are independent, the docs warn that several `async`/`await` lines in one component can still run sequentially, so the second call is held up until the first has finished ([Next.js, fetching data](https://nextjs.org/docs/app/getting-started/fetching-data)). The page then waits for the sum of the calls instead of the longest one.

The fix the documentation shows is to start all the calls first and wait for them together with `Promise.all`, and it adds that if one request fails there the whole operation fails, with `Promise.allSettled` as the alternative when partial results are acceptable. One distinction must stay straight: a call fed by the output of an earlier one has to remain behind it. Reporting that one as a waterfall is a false alarm. The same page also describes preloading, which starts a request before some other blocking work so that both proceed together.

## A client marker placed too high

In the App Router, files are Server Components by default. Adding `"use client"` to a file draws a boundary, and the documentation states that everything the file imports, plus the components it renders directly, goes into the client bundle ([Next.js, Server and Client Components](https://nextjs.org/docs/app/getting-started/server-and-client-components)). Put the marker on a whole page because one widget needs state, and every import of that page, including libraries that only format text on the server, now ships to the browser and has to be hydrated.

The recommendation in the same document is to mark the files that define interactive pieces and keep the large parts as Server Components. Its example is a layout that is mostly static and holds one interactive search box: only the search box becomes a client component. A server component can also be passed to a client component as `children`, so a modal can keep its server-rendered content. The marker on a small button or a search field is correct, because the whole file is interactive. The suspicious case is a marker sitting on a route page, a layout or some large wrapper.

## Heavy libraries imported at the top

A library that charts data, builds a document, or parses a large format costs download and parse time whether or not it is used. The lazy-loading guide says deferring client components and imported libraries decreases the JavaScript needed for a route, and gives two tools: `next/dynamic`, and `React.lazy` with Suspense ([Next.js, lazy loading](https://nextjs.org/docs/app/guides/lazy-loading)). For a library that only a handler uses, the guide shows a plain `import()` inside the handler, so the module is requested after the user acts, in its example after typing in a search box.

The telltale sign is a static import near the first lines of a client file, with its only use inside an `onClick` or inside a branch that begins false. A tiny helper needed on first paint does not qualify, nor does a module already loaded lazily. The trade-off is real: the first click now waits for a download, so a loading state is worth adding, and a feature almost everyone uses is better left in the main bundle.

## Images without reserved space

An image with no declared size loads at an unknown height, and the text below it jumps when it arrives. The Next.js image documentation explains that `width` and `height` give the browser the aspect ratio so it can reserve space and avoid layout shift, and that they do not set the rendered size, which CSS controls. For a remote or dynamic source the documentation says both props are required, with `fill` as the choice when the ratio is unknown ([Next.js, Image component](https://nextjs.org/docs/app/api-reference/components/image)). The visible symptom is Cumulative Layout Shift, one of the three Core Web Vitals, for which web.dev gives a good threshold of 0.1 or less ([web.dev, Core Web Vitals](https://web.dev/articles/vitals)).

## Unstable keys and unstable props

Two small habits cause extra work on every render. The first is the key on a list item. The React documentation says a key lets React match items between renders even when positions change, warns that an index as the key often causes subtle bugs, and says that a key made with `Math.random()` never matches, so the components and DOM are recreated every time and input inside the items is lost ([react.dev, rendering lists](https://react.dev/learn/rendering-lists)). A stable id from the data is the cure.

The second is a new object, array or function written inline while a component renders and then handed to a `memo` child. React compares props by reference, and the memo reference page points out that two empty objects are not equal, so a freshly built object defeats the optimisation and the child re-renders anyway. It recommends `useMemo` or `useCallback` in the parent, or moving the value outside the component ([react.dev, memo](https://react.dev/reference/react/memo)). The same page notes that React Compiler applies the equivalent of memoisation automatically, so if the compiler is on, hand-written memo work is often unnecessary, and a review that cannot see the compiler setting should not guess. The same reasoning covers an effect whose dependency list holds a value that changes every render.

## Missing Suspense boundaries

With server data that is not cached, the documentation says a slow request blocks the whole route from rendering until all the data has arrived. Streaming breaks the page into chunks that are sent as they are ready, either with a `loading.js` file for the entire route or with a `<Suspense>` boundary around a single component, where everything outside the boundary is sent at once ([Next.js, fetching data](https://nextjs.org/docs/app/getting-started/fetching-data)). Place the boundary around the slow part, not around the page, so static content such as the heading shows up first. The same documentation warns that a layout which reads uncached data does not fall back to a `loading.js` in its own segment, so the boundary belongs closer to the slow access.

## A check that can fail: measure before and after

Reading code finds suspects; only a measurement shows whether a change helped. Pick one page and record its numbers before touching anything. In Lighthouse or the Chrome performance panel, note Largest Contentful Paint, Cumulative Layout Shift and Interaction to Next Paint, and open the Network panel to see the order and overlap of the requests. web.dev's guide to the Core Web Vitals sets the good thresholds at 2.5 seconds for LCP, 200 milliseconds for INP and 0.1 for CLS, judged at the 75th percentile of page loads ([web.dev, Core Web Vitals](https://web.dev/articles/vitals)), so a single lab run is a clue and not a verdict.

Then change one thing and run it again. For a waterfall, the Network panel should show the calls starting together, not one after another. For a client boundary, the amount of JavaScript transferred on first load should fall. For an eager import, the library's file should no longer be requested until the click. For images, layout shift should go to near zero. A check that stays unchanged after the fix either measured the wrong page or the fix did nothing, and both are worth knowing. Test the opposite as well: put the mistake back and confirm the number gets worse, otherwise the metric may be insensitive to your change. If the layout shift persists after the image fix, the cause may lie in the CSS itself; other guides on catching such faults before release are collected under [building](/blog/topics/building).

## When this does not apply

These patterns concern React with the Next.js App Router. A pure client-side single-page app has no server components to move work into, a page behind a login that is rarely used may not repay the effort, and a build with React Compiler changes the memoisation advice. Documentation changes between versions, and the passages cited here were read on one date, so check the version you run. The thresholds are field targets at a percentile and a lab run can differ from them. No test of ours stands behind this list: it is drawn from the official documentation and from commonly reported patterns, not from a measured ranking of which one costs the most.

## One tool for the first read

Our [react-performance-review](https://aiskills402.com/skills/react-performance-review) skill reads React or Next.js code you paste and reports five kinds of wasted time, giving for each the location, the reason and a suggested rewrite. They are: awaits chained although nothing links them; an effect that stores something computable during render; a client mark covering a whole route when only a leaf is interactive; a charting or document library pulled in eagerly yet used after a click; and fresh inline values given to a memoised child or an effect's dependency list. Dependent calls and other harmless patterns are left unreported, and it states no timings, because only a real run of the page can produce those.
