Subchapter 7.15
references/scanner-patterns.mdMarkdown13 KBView on GitHub
AST/grep-style scanners run in parallel with metric-driven investigation. They find known anti-patterns. Findings on cold-path or unmappable files are dropped unless the scanner declares trafficIndependent: true.
Total scanners: 15.
Description. Default 'use cache' does not dedupe identical calls across separate <Suspense> boundaries on the same render. Each boundary re-invokes the cached function, multiplying function-duration cost and inflating ISR write churn when the output is large.
Fix. Hoist the promise to the page level (const dataPromise = fetchData() at the top, passed down to each Suspense child) OR move the shared fetch into a 'use cache: remote' data-access layer so cross-request and cross-boundary dedupe applies.
Citations:
https://nextjs.org/docs/app/api-reference/directives/use-cachehttps://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponentshttps://nextjs.org/docs/app/guides/migrating-to-cache-componentsDescription. Edge runtime is a constrained sandbox with no node: builtins and a much smaller cold-start budget than Node functions. Heavy SDKs (sharp, @aws-sdk/*, @prisma/client, pg, puppeteer) either fail at deploy or inflate cold-start latency. Move the import to a Node runtime function, or replace with an edge-compatible alternative (e.g., neon-driver instead of pg).
Fix. Either (a) drop the export const runtime = 'edge' so the route runs on Node (default in 2026), or (b) replace the heavy import with an edge-compatible alternative. For DB: use @neondatabase/serverless or @planetscale/database instead of pg/mysql2. For image: do the work in a Node route handler. For auth signing: use jose (Web Crypto) instead of jsonwebtoken.
Citations:
https://vercel.com/docs/functions/runtimes/edge-runtimehttps://vercel.com/docs/fluid-computeDescription. force-dynamic disables static + ISR rendering. The route runs the function on every request. Sometimes necessary (cookies, headers, real-time data), often a habit that costs function-duration and edge-requests at scale.
Fix. Audit the route. If dynamic behavior comes from cookies()/headers()/searchParams, force-dynamic may be redundant — Next infers dynamic automatically. Consider revalidate / ‘use cache’ / generateStaticParams if any portion can be pre-rendered.
Citations:
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-configDescription. headers(), cookies(), and draftMode() are dynamic APIs. Reading them in a page/layout makes the entire segment dynamic — no ISR, no static generation, and a function invocation on every request.
Fix. Move the dynamic API call into a child Server Component that lives inside a Suspense boundary. The parent can stay static; only the leaf re-renders dynamically.
Citations:
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-confighttps://nextjs.org/docs/app/building-your-application/cachingDescription. Static assets in public/ over 500 KB ship as-is from the CDN. Whether the cost is meaningful depends on traffic, but the candidate is binary — the file is either needed at that size or it can be optimized (compressed image, video transcode, or moved off the critical path).
Fix. Verify the asset is reachable on the customer-facing hot path. Then choose: (a) compress (convert PNG → AVIF/WebP; transcode MP4 to lower bitrate); (b) host externally (Vercel Blob, S3, or a media CDN with per-asset signed URLs); (c) lazy-load (defer to client-side fetch instead of bundling into initial HTML).
Citations:
https://vercel.com/docs/manage-cdn-usagehttps://vercel.com/docs/image-optimizationDescription. max-age caches in the browser; s-maxage caches at the CDN. Without s-maxage, every uncached visitor request invokes the function. Adding s-maxage often cuts function invocations by 80%+ on read-heavy routes.
Fix. Add s-maxage to the Cache-Control header. Example: Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=86400. Pair with explicit cache-bust strategy if content can change.
Citations:
https://vercel.com/docs/caching/cdn-cachehttps://vercel.com/docs/caching/cache-control-headersDescription. middleware.ts without a config.matcher (or matcher: [“/(.*)”]) runs on every request including _next/static, _next/image, favicon.ico, and image asset fetches. Edge-request cost scales accordingly.
Fix. Scope the matcher to actual application paths. Example: matcher: [“/((?!_next/static|_next/image|favicon.ico|.\.(?:svg|png|jpg|jpeg|gif|webp)$).)”]
Citations:
https://nextjs.org/docs/app/building-your-application/routing/middlewareDescription. Two antipatterns: (a) GET handlers without explicit Cache-Control headers serve uncached; (b) fetch() calls with cache:”no-store” or next:{revalidate:0} opt out of caching even on cacheable upstream data. For non-auth routes / fetches, both are leaving cache hits on the floor.
Fix. For GET handlers: return a Response with Cache-Control: public, s-maxage=, stale-while-revalidate=. For fetch(): drop cache:”no-store” (use { next: { revalidate: } } in Next.js) so the response is cached by the framework + CDN.
Citations:
https://vercel.com/docs/caching/cdn-cachehttps://vercel.com/docs/caching/cache-control-headershttps://nextjs.org/docs/app/building-your-application/cachingDescription. Nested .include({ x: { include: { y: { include: { z: … } } } } }) makes Prisma issue a single huge join that scales O(NMK). Function duration explodes, memory spikes, often causes timeouts.
Fix. Replace with explicit .findMany() calls or scoped .include() of only what the consumer reads. Consider Prisma.select() to project specific fields. For lists, batch with DataLoader patterns.
Citations:
vercel-react-best-practices:server-parallel-fetchingDescription. vercel.json regions or per-route preferredRegion is set. If the pinned region is far from the dominant user geo (or far from a data source) p95 TTFB suffers. This scanner provides the configured-region signal so the region-misconfig gate can recommend an audit.
Fix. Audit the pinned region against traffic geography (Speed Insights or Web Analytics by country) and data-source location. Consider multi-region if data lives in a fixed location and users are global; consider relocating if users are concentrated in one geography.
Citations:
https://vercel.com/docs/functions/configuring-functions/regionhttps://vercel.com/docs/functions/configuring-functions/regionDescription. productionBrowserSourceMaps: true ships .map files in the production bundle, increasing transfer size 30-100% per visitor. Useful for error reporting via Sentry; not useful for users.
Fix. Keep source maps generation but exclude them from the public bundle. Upload to your error tracker via build-time CI step; do not serve them with the deployment.
Citations:
https://nextjs.org/docs/messages/improper-devtoolDescription. SvelteKit page or +page.server.ts is missing an explicit prerender, ssr, or adapter config.isr declaration. Default is per-request function execution — investigate whether the route could be prerendered or ISR-cached.
Fix. If the page is static (no per-user / per-request data), add export const prerender = true in +page.ts or +page.server.ts. If the data refreshes on a schedule, prefer adapter-vercel’s ISR option via export const config = { isr: { expiration: 60 } }.
Citations:
https://kit.svelte.dev/docs/page-optionshttps://kit.svelte.dev/docs/adapter-vercelhttps://vercel.com/docs/incremental-static-regenerationDescription. Turborepo’s per-task cache can be bypassed by an explicit force flag, a cache: false config, or missing build-skip configuration. Every commit can rebuild unchanged work; Build Minutes climb with project count.
Fix. Remove TURBO_FORCE=true from build env/scripts unless intentional. Set tasks.build.cache: true in turbo.json (or remove the override), and include generated outputs in Turbo’s cache contract. Prefer Vercel’s skip-unaffected monorepo behavior when available; use ignoreCommand only when that setting cannot cover the project.
Citations:
https://vercel.com/docs/monoreposhttps://vercel.com/docs/buildshttps://turborepo.dev/docs/crafting-your-repository/cachingDescription. Four shapes of image-cost waste: raw tags bypass the framework Image component; images.unoptimized: true disables Vercel image optimization globally; without sizes forces serving the largest source variant; without unoptimized routes vector data through the raster pipeline.
Fix. For raw : switch to next/image, enhanced-img (SvelteKit), (Astro), or NuxtImg. For global unoptimized:true: remove the flag unless the project is hosted outside Vercel. For fill without sizes: add sizes="(max-width: 768px) 100vw, 50vw" or whatever matches your layout. For SVG: add unoptimized so the raw SVG ships instead of rastering it.
Citations:
https://nextjs.org/docs/app/api-reference/components/imagehttps://vercel.com/docs/image-optimizationDescription. 'use cache' memoizes by argument identity AND prerender output. A timestamp baked into the cached output (new Date().getFullYear() in a footer, Date.now() in a payload field) forces a fresh ISR write on every regeneration even when the underlying data is unchanged. Random values have the same failure mode.
Fix. Replace module-scope new Date() with a build-time constant (const buildYear = new Date().getFullYear()) or move per-request timestamps into a client component inside useEffect. Do not pass dates as arguments to 'use cache' functions — they invalidate the cache every call.
Citations:
https://nextjs.org/docs/app/api-reference/directives/use-cachehttps://nextjs.org/docs/app/api-reference/functions/cacheLife