Most slow App Router apps fail for a handful of repeated reasons.
| Mistake | Hurts | Fix |
|---|---|---|
| 'use client' on a page | LCP | Move it to the interactive leaf |
| Heavy library on the client | LCP | Render it on the server |
| <img> without dimensions | CLS | next/image or aspect-ratio |
| Font swapped in late | CLS | next/font, self-hosted |
| Work on every keystroke | INP | useDeferredValue, useMemo |
| cookies() in the root layout | TTFB | Read it where it is needed |
The first two are one mistake at different scales. A 'use client' directive high in the tree drags every component below it into the browser bundle, and a library imported inside that boundary ships whether or not it renders. Putting 'use client' on the smallest component that owns state is the most effective habit in an App Router codebase.
The layout shift on /slow measured 0.09 for one image: a hero without dimensions reserves no space, so everything below it jumps when the bytes arrive. next/image writes the aspect ratio into the markup, which is why /fast scores 0. Late fonts shift text the same way.
INP is discovered last, because it needs a real interaction to exist at all. On /slow the filter recomputes 4,000 rows inside the onChange handler and INP reads 184-296 ms across runs; with useDeferredValue and a memoized computation it reads 24-32 ms. The React 7,897 Compiler (The React Compiler) removes some of this work, but cannot make an O(n) loop cheap. Finally, do not fetch in a Client Component's useEffect when a Server Component could fetch during the render: that costs a round trip after hydration, and often shows up as a good FCP with an awful LCP.