252 lines
13 KiB
Markdown
252 lines
13 KiB
Markdown
<!-- THIS FILE IS GENERATED by scripts/build-docs.mjs. Do not edit by hand. -->
|
|
<!-- To change scanner descriptions, edit lib/scanners/*.mjs metadata exports. -->
|
|
<!-- To change gate thresholds, edit lib/gates/*.mjs metadata exports. -->
|
|
|
|
# Scanner patterns
|
|
|
|
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.
|
|
|
|
## Patterns
|
|
|
|
### `cache-components-suspense-dedupe` — 'use cache' with multiple Suspense boundaries on the same data
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**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-cache`
|
|
- `https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents`
|
|
- `https://nextjs.org/docs/app/guides/migrating-to-cache-components`
|
|
|
|
---
|
|
|
|
### `edge-heavy-import` — Heavy / node-only import inside edge-runtime file
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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-runtime`
|
|
- `https://vercel.com/docs/fluid-compute`
|
|
|
|
---
|
|
|
|
### `force-dynamic` — export const dynamic = 'force-dynamic'
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** 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-config`
|
|
|
|
---
|
|
|
|
### `headers-in-page` — Dynamic API call forcing dynamic rendering
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** 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-config`
|
|
- `https://nextjs.org/docs/app/building-your-application/caching`
|
|
|
|
---
|
|
|
|
### `large-static-asset` — Large file in public/
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: bandwidth
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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-usage`
|
|
- `https://vercel.com/docs/image-optimization`
|
|
|
|
---
|
|
|
|
### `max-age-without-s-maxage` — Cache-Control: max-age without s-maxage
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: edge-requests
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** 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-cache`
|
|
- `https://vercel.com/docs/caching/cache-control-headers`
|
|
|
|
---
|
|
|
|
### `middleware-broad-matcher` — Middleware matcher missing or too broad
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: edge-requests
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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/middleware`
|
|
|
|
---
|
|
|
|
### `missing-cache-headers` — Cacheable route or fetch with no caching (Cache-Control absent or no-store)
|
|
|
|
- **Severity**: medium
|
|
- **Billing dimension**: edge-requests
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** 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=<seconds>, stale-while-revalidate=<window>. For fetch(): drop cache:"no-store" (use { next: { revalidate: <seconds> } } in Next.js) so the response is cached by the framework + CDN.
|
|
|
|
**Citations:**
|
|
- `https://vercel.com/docs/caching/cdn-cache`
|
|
- `https://vercel.com/docs/caching/cache-control-headers`
|
|
- `https://nextjs.org/docs/app/building-your-application/caching`
|
|
|
|
---
|
|
|
|
### `prisma-include-tree-bloat` — Deep Prisma include tree (3+ levels)
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** Nested .include({ x: { include: { y: { include: { z: ... } } } } }) makes Prisma issue a single huge join that scales O(N*M*K). 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-fetching`
|
|
|
|
---
|
|
|
|
### `region-pin-in-config` — Function region pinned in config
|
|
|
|
- **Severity**: low
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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/region`
|
|
- `https://vercel.com/docs/functions/configuring-functions/region`
|
|
|
|
---
|
|
|
|
### `source-maps-production` — Source maps enabled in production
|
|
|
|
- **Severity**: low
|
|
- **Billing dimension**: edge-requests
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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-devtool`
|
|
|
|
---
|
|
|
|
### `sveltekit-prerender-missing` — SvelteKit page without explicit prerender / ISR config
|
|
|
|
- **Severity**: low
|
|
- **Billing dimension**: function-duration
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** 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-options`
|
|
- `https://kit.svelte.dev/docs/adapter-vercel`
|
|
- `https://vercel.com/docs/incremental-static-regeneration`
|
|
|
|
---
|
|
|
|
### `turbo-force-bypass` — Turborepo cache bypass on a monorepo
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: build
|
|
- **Traffic-independent**: yes (cold-path findings survive the doctrine drop)
|
|
|
|
**Description.** 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/monorepos`
|
|
- `https://vercel.com/docs/builds`
|
|
- `https://turborepo.dev/docs/crafting-your-repository/caching`
|
|
|
|
---
|
|
|
|
### `unoptimized-image` — Image optimization gap (raw <img>, global flag, missing sizes, or SVG mis-routed)
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: image-optimization
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** Four shapes of image-cost waste: raw <img> tags bypass the framework Image component; `images.unoptimized: true` disables Vercel image optimization globally; <Image fill> without `sizes` forces serving the largest source variant; <Image src=".svg"> without `unoptimized` routes vector data through the raster pipeline.
|
|
|
|
**Fix.** For raw <img>: switch to next/image, enhanced-img (SvelteKit), <Image /> (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/image`
|
|
- `https://vercel.com/docs/image-optimization`
|
|
|
|
---
|
|
|
|
### `use-cache-date-stamp` — new Date() / Date.now() / Math.random() inside a 'use cache' file
|
|
|
|
- **Severity**: high
|
|
- **Billing dimension**: isr
|
|
- **Traffic-independent**: no (cold-path findings get dropped)
|
|
|
|
**Description.** `'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-cache`
|
|
- `https://nextjs.org/docs/app/api-reference/functions/cacheLife`
|
|
|
|
---
|