Appendix C — Anti‑patterns catalog¶
Thirty things that look like optimizations and aren't. Each entry: what people do, why it seems right, why it isn't, and what to do instead.
Rendering & architecture¶
1. export const dynamic = 'force-dynamic' "to be safe"¶
Why it seems right: guarantees fresh data, avoids stale‑content bugs. Why it isn't: throws away every caching layer. TTFB goes from 20 ms to 900 ms and your CDN HTML hit ratio goes to zero. Instead: identify what actually needs to be dynamic and isolate it in a Suspense boundary (3.6).
2. ssr: false to fix a hydration error¶
Why it seems right: the error goes away. Why it isn't: you removed server rendering for the whole component, hid the content from crawlers and the preload scanner, and added a client round trip. The mismatch is still a bug. Instead: fix the mismatch (4.4).
3. 'use client' at the root because "we need providers"¶
Why it seems right: providers need client state, and they wrap everything.
Why it isn't: every component below becomes client code. This is the single largest bundle
problem in most Next.js commerce apps.
Instead: the children pass‑through pattern, plus route‑group scoping
(3.2).
4. Prerendering the entire catalog¶
Why it seems right: everything is static and fast.
Why it isn't: multi‑hour builds, and 88% of the pages are never visited.
Instead: prebuild the traffic knee (~20K pages), dynamicParams: true for the rest
(3.5).
5. Edge rendering data‑heavy pages¶
Why it seems right: "close to the user" is obviously good. Why it isn't: the compute is now far from the data. Five backend calls at 200 ms each beats one 200 ms user round trip every time. Instead: regional compute near the data; cache aggressively at the edge (8.6).
6. A Suspense boundary around the LCP element¶
Why it seems right: streaming is good, so stream everything. Why it isn't: the LCP image can't be discovered until the boundary's data resolves. You made the metric you're optimizing worse. Instead: LCP element in the first flush; stream what's below it (3.3).
7. A Suspense boundary per list item¶
Why it seems right: maximum granularity, each card appears as soon as it's ready. Why it isn't: 48 boundaries means 48 flushes and a page that visibly assembles itself. Instead: one boundary for the grid (3.3).
Caching¶
8. One global cache tag¶
Why it seems right: simple, and invalidation is guaranteed correct. Why it isn't: every price change on any of 2.4M SKUs purges everything. You have no cache. Instead: a tag hierarchy (3.4).
9. Hard purge on every content change¶
Why it seems right: immediate freshness. Why it isn't: every request for that content hits your origin simultaneously — a stampede at exactly the moment traffic is highest. Instead: soft purge (mark stale, serve stale‑while‑revalidating) (7.2).
10. Caching inventory¶
Why it seems right: it's just another field on the product. Why it isn't: you'll oversell. Customer service cost and reputational damage exceed any latency saving. Instead: never cache inventory; make it a dynamic hole or fail open to "check availability".
11. Caching anything derived from a cookie¶
Why it seems right: the page is mostly the same for everyone. Why it isn't: 🚨 one customer sees another's cart, address, or order history. A reportable breach, not a bug. Instead: dynamic holes, plus an enforced CI isolation test (3.6).
12. Self‑hosting Next.js on N pods with no shared cache handler¶
Why it seems right: ISR works fine locally.
Why it isn't: each pod has its own filesystem cache. Hit ratio is 1/N, and revalidateTag
doesn't propagate.
Instead: a shared Redis cache handler
(3.4).
JavaScript¶
13. Barrel imports¶
Why it seems right: import { Button } from '@company/ui' is clean.
Why it isn't: tree‑shaking fails silently with side effects, CJS, or dynamic re‑exports, and
you ship all 60 components.
Instead: deep imports, optimizePackageImports, and a lint rule
(4.3).
14. next/dynamic on a component that has no interactivity¶
Why it seems right: it's lazy, so it must be faster.
Why it isn't: you still ship the JS, just later. A Server Component ships zero.
Instead: Server Component first, next/dynamic only for interactive things
(4.2).
15. Splitting a 4 KB component¶
Why it seems right: every kilobyte counts. Why it isn't: the extra round trip (50–150 ms on 4G) costs more than the 4 KB (~20 ms). Instead: don't split below ~15 KB unless it's rarely used.
16. Interaction splitting with no preload¶
Why it seems right: the code loads only when needed.
Why it isn't: every click pays a download. That's a direct INP regression.
Instead: preload on mouseenter/focus/touchstart
(4.2).
17. A polyfill bundle for browsers nobody uses¶
Why it seems right: maximum compatibility.
Why it isn't: 40–80 KB for 0.2% of users, paid by 99.8%.
Instead: set browserslist from your real analytics
(4.1).
React¶
18. React.memo on everything¶
Why it seems right: fewer re‑renders must be faster. Why it isn't: if props change every render (inline functions, object literals), you pay the comparison and the render. Instead: stabilize props first, memoize where profiling shows it helps, measure after (5.2).
19. useMemo on trivial expressions¶
Why it seems right: memoization is optimization.
Why it isn't: useMemo(() => a * b, [a, b]) costs more than a * b.
Instead: just compute it. Reserve useMemo for expensive work or referential stability.
20. Deriving state in a useEffect¶
Why it seems right: it's how you keep state in sync. Why it isn't: render → effect → setState → render. Two commits per interaction, and a frame of stale UI. Instead: derive during render (5.1).
21. One context for the whole cart¶
Why it seems right: cart state belongs together.
Why it isn't: opening the drawer changes the context value, re‑rendering all 48 product cards
that only wanted addToCart.
Instead: split by change frequency, or an external store with selectors
(5.3).
22. Virtualizing a 48‑item grid¶
Why it seems right: virtualization is the list optimization. Why it isn't: you broke Ctrl+F, SEO, and scroll restoration to fix a problem that was re‑renders, not item count. Instead: fix the re‑render; virtualize above ~500 items (5.4).
23. Debouncing renders instead of deferring them¶
Why it seems right: fewer renders, less work. Why it isn't: results are always stale by the debounce interval, and the work still blocks when it runs. Instead: debounce the network, defer the render (5.5).
24. Putting the urgent update inside startTransition¶
Why it seems right: transitions make things responsive. Why it isn't: now the checkbox itself lags. You made the interaction worse. Instead: urgent feedback outside, expensive consequences inside.
Images & assets¶
25. sizes="100vw" on a grid¶
Why it seems right: it's the default and it looks fine. Why it isn't: a 4‑column grid downloads 4× the pixels it needs. Silent, invisible, and often the single biggest byte problem on the site. Instead: describe the real rendered width per breakpoint (2.1).
26. priority on six images¶
Why it seems right: the important images should load first. Why it isn't: six high‑priority images compete for bandwidth. Your actual LCP element arrives later than with no priority at all. Instead: exactly one, on the real LCP element.
27. Blur placeholders on every grid tile¶
Why it seems right: it looks polished. Why it isn't: 48 × 1 KB of inline base64 bloats the HTML, delaying the LCP element it was meant to help. Instead: flat background for grids; blur for the hero only.
Third parties & measurement¶
28. The anti‑flicker snippet¶
Why it seems right: prevents the flash of the control variant. Why it isn't: hides the entire page until an SDK loads — up to a 4‑second LCP for everyone, including the control group. It also biases every experiment you run. Instead: edge assignment (8.5).
29. Optimizing the Lighthouse score¶
Why it seems right: it's a number that goes up. Why it isn't: it's a weighted lab composite with ±5–10 points of noise, measured on a machine that isn't your users'. Nobody buys more shoes because it went 62 → 91. Instead: field p75 per page type, and a business metric (1.3).
30. Quoting "0.1 s = +8.4% conversion" as a forecast¶
Why it seems right: it's a real published figure and it makes the business case easy. Why it isn't: applied to a $1B retailer, a 1‑second improvement "earns" $840M — more than the company's margin. Any finance partner notices, and your credibility is gone. Instead: measure your own elasticity, present ranges, discount correlational evidence (1.1).
The meta anti‑patterns¶
Optimizing without measuring first. You'll fix something that wasn't the bottleneck and conclude that performance work doesn't pay.
Measuring without a control. You'll attribute a seasonal traffic shift to your change.
Changing five things at once. You'll keep four that did nothing and one that made it worse.
Shipping the fix and the measurement together. You'll never know whether it worked.
Only reporting wins. The first null result you hide is the last number anyone believes.
Doing architecture before quick wins. You'll spend a quarter to deliver what three days of image work would have, and lose the mandate before you finish.