# AI Skills



import { DynamicLink } from "fumadocs-core/dynamic-link";

import { SKILLS_COMMAND } from "@/constants/command";
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";

Vercel Doctor can be installed as a skill for AI coding assistants. Once installed, your AI assistant will automatically suggest running Vercel Doctor after making changes to Next.js projects.

One-line install [#one-line-install]

<DynamicCodeBlock lang="bash" code={SKILLS_COMMAND} />

Or install via curl:

```bash
curl -fsSL https://vercel-doctor.com/install-skill.sh | bash
```

Supported tools [#supported-tools]

The installer auto-detects and installs for all supported tools on your system:

| Tool        | Skill location                                 |
| ----------- | ---------------------------------------------- |
| Claude Code | `~/.claude/skills/vercel-doctor/`              |
| Amp Code    | `~/.config/amp/skills/vercel-doctor/`          |
| Cursor      | `~/.cursor/skills/vercel-doctor/`              |
| OpenCode    | `~/.config/opencode/skills/vercel-doctor/`     |
| Windsurf    | `~/.codeium/windsurf/memories/global_rules.md` |
| Antigravity | `~/.gemini/antigravity/skills/vercel-doctor/`  |
| Gemini CLI  | `~/.gemini/skills/vercel-doctor/`              |
| Codex       | `~/.codex/skills/vercel-doctor/`               |

The installer also adds the skill to your project's `.agents/` directory for project-level context.

What the skill does [#what-the-skill-does]

When activated, the AI assistant will:

1. Know about Vercel Doctor and what it scans for
2. Suggest running `npx -y vercel-doctor@latest . --verbose --diff` after changes
3. Focus on fixing issues that reduce function execution time and invocations first


# CLI Reference



import { DynamicLink } from "fumadocs-core/dynamic-link";

Usage [#usage]

```bash
npx -y vercel-doctor@latest [directory] [options]
```

The `directory` argument defaults to `.` (current directory).

Options [#options]

| Flag               | Description                                           |
| ------------------ | ----------------------------------------------------- |
| `-v, --version`    | Display the current version                           |
| `--no-lint`        | Skip billing lint rules                               |
| `--no-dead-code`   | Skip dead code detection (Knip)                       |
| `--verbose`        | Show file paths and line numbers per rule             |
| `--score`          | Output only the numeric score (useful for CI)         |
| `-y, --yes`        | Skip interactive prompts, scan all workspace projects |
| `--project <name>` | Select specific workspace project(s), comma-separated |
| `--diff [base]`    | Scan only files changed vs the base branch            |
| `--offline`        | Skip anonymous telemetry used for score calculation   |

Examples [#examples]

Scan the current directory [#scan-the-current-directory]

```bash
npx -y vercel-doctor@latest .
```

Scan with verbose output [#scan-with-verbose-output]

```bash
npx -y vercel-doctor@latest . --verbose
```

Shows affected file paths and line numbers for each diagnostic.

Scan only changed files [#scan-only-changed-files]

```bash
npx -y vercel-doctor@latest . --diff
```

Auto-detects the base branch. You can also pin a specific base:

```bash
npx -y vercel-doctor@latest . --diff main
```

Scan a specific workspace project [#scan-a-specific-workspace-project]

```bash
npx -y vercel-doctor@latest . --project web
```

For multiple projects:

```bash
npx -y vercel-doctor@latest . --project web,api
```

Skip dead code detection [#skip-dead-code-detection]

```bash
npx -y vercel-doctor@latest . --no-dead-code
```

CI usage [#ci-usage]

```bash
npx -y vercel-doctor@latest . --yes --score
```

The `--yes` flag skips interactive prompts. Combined with `--score`, it outputs only the numeric score for use in scripts and pipelines.

Programmatic API [#programmatic-api]

Vercel Doctor also exports a programmatic API:

```ts
import { run } from "vercel-doctor/api";
```

Oxlint Plugin [#oxlint-plugin]

The billing lint rules are available as a standalone Oxlint plugin:

```ts
import plugin from "vercel-doctor/oxlint-plugin";
```


# Configuration



import { DynamicLink } from "fumadocs-core/dynamic-link";

Vercel Doctor works out of the box with zero configuration. When you need to customize behavior, you can add a config file.

Config file [#config-file]

Create a `vercel-doctor.config.json` in your project root:

```json
{
  "ignore": {
    "rules": ["vercel-doctor/nextjs-link-prefetch-default"],
    "files": ["src/generated/**"]
  }
}
```

Alternatively, add a `"vercelDoctor"` key to your `package.json`:

```json
{
  "vercelDoctor": {
    "ignore": {
      "rules": ["knip/exports"],
      "files": ["src/generated/**"]
    }
  }
}
```

If both exist, `vercel-doctor.config.json` takes precedence.

Options [#options]

| Key            | Type                | Default | Description                                   |
| -------------- | ------------------- | ------- | --------------------------------------------- |
| `ignore.rules` | `string[]`          | `[]`    | Rule IDs to suppress, in `plugin/rule` format |
| `ignore.files` | `string[]`          | `[]`    | Glob patterns for file paths to skip          |
| `lint`         | `boolean`           | `true`  | Enable or disable billing lint rules          |
| `deadCode`     | `boolean`           | `true`  | Enable or disable dead code detection         |
| `verbose`      | `boolean`           | `false` | Show file paths and line numbers per rule     |
| `diff`         | `boolean \| string` | `false` | Enable diff mode or pin a base branch         |

Ignoring rules [#ignoring-rules]

Use the `plugin/rule` format for rule IDs:

```json
{
  "ignore": {
    "rules": [
      "vercel-doctor/nextjs-image-missing-sizes",
      "vercel-doctor/vercel-large-static-asset",
      "knip/exports"
    ]
  }
}
```

See the <DynamicLink href="/[lang]/docs/rules/function-duration">Rules</DynamicLink> pages for all available rule IDs.

Ignoring files [#ignoring-files]

Use glob patterns to exclude files from scanning:

```json
{
  "ignore": {
    "files": ["src/generated/**", "**/*.stories.tsx", "scripts/**"]
  }
}
```

CLI overrides [#cli-overrides]

CLI flags always take precedence over config file values. For example, `--verbose` on the command line overrides `"verbose": false` in the config file.

Full example [#full-example]

```json
{
  "ignore": {
    "rules": [
      "vercel-doctor/vercel-consider-bun-runtime",
      "vercel-doctor/vercel-avoid-platform-cron"
    ],
    "files": ["src/generated/**", "e2e/**"]
  },
  "lint": true,
  "deadCode": true,
  "verbose": false,
  "diff": "main"
}
```


# What is Vercel Doctor?



import { DynamicLink } from "fumadocs-core/dynamic-link";

import { Step, Steps } from "fumadocs-ui/components/steps";

Vercel Doctor is a specialized health check tool for Next.js projects deployed on Vercel. It scans your codebase for patterns that inflate your bill — function duration, uncached routes, unoptimized images, and more — then gives you a score out of 100 with actionable fixes. Guidance adapts to your detected Next.js major version.

Quick Start [#quick-start]

<Steps>
  <Step>
    Run on your project [#run-on-your-project]

    ```bash
    npx -y vercel-doctor@latest .
    ```

    That's it. No install, no config. It scans your codebase and outputs a score with diagnostics.
  </Step>

  <Step>
    Fix the issues [#fix-the-issues]

    Each diagnostic includes a help message explaining what to change and why. Fix the highest-impact rules first — errors before warnings.
  </Step>

  <Step>
    Re-run to verify [#re-run-to-verify]

    ```bash
    npx -y vercel-doctor@latest . --diff
    ```

    Use `--diff` on feature branches to scan only the files you changed.
  </Step>
</Steps>

What it checks [#what-it-checks]

Vercel Doctor runs two analysis passes in parallel:

1. **Billing lint** — AST-based and pattern-based rules that detect costly Next.js and Vercel patterns
2. **Dead code detection** — finds unused files, exports, and types that bloat your bundle and slow cold starts

Rule categories [#rule-categories]

| Category                                                                                   | Rules | What it catches                                                                            |
| ------------------------------------------------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------ |
| <DynamicLink href="/[lang]/docs/rules/function-duration">Function Duration</DynamicLink>   | 3     | Sequential awaits, blocking logging, non-parallel I/O                                      |
| <DynamicLink href="/[lang]/docs/rules/caching">Caching</DynamicLink>                       | 6     | `force-dynamic`, missing cache policies, `no-store` fetches, side effects in GET handlers  |
| <DynamicLink href="/[lang]/docs/rules/invocations">Invocations</DynamicLink>               | 2     | Client-side fetching in pages, aggressive link prefetching                                 |
| <DynamicLink href="/[lang]/docs/rules/image-optimization">Image Optimization</DynamicLink> | 4     | Global `unoptimized`, missing `sizes`, broad remote patterns, SVG without `unoptimized`    |
| <DynamicLink href="/[lang]/docs/rules/platform">Platform</DynamicLink>                     | 8     | Edge heavy imports, cron jobs, Bun runtime, Fluid Compute, deploy archive, Turbopack cache |
| <DynamicLink href="/[lang]/docs/rules/dead-code">Dead Code</DynamicLink>                   | 4     | Unused files, exports, types, and duplicates                                               |

Features [#features]

* **Zero config** — runs out of the box on any Next.js project
* **Monorepo support** — auto-detects workspaces, lets you pick which projects to scan
* **Diff mode** — scan only changed files on feature branches
* **Reports** — generate markdown reports and AI-compatible fix prompts for Cursor, Claude, and Windsurf
* **Scoring** — 0–100 score with Great / Needs work / Critical labels
* **CI-friendly** — non-interactive mode for automated pipelines
* **Configurable** — ignore rules or files via config file
* **AI skill** — install as a skill for Claude Code, Cursor, Codex, and more
* **Version-aware guidance** — recommendations adjust to your detected Next.js major version


# Reports



import { DynamicLink } from "fumadocs-core/dynamic-link";

Generate detailed reports and AI-compatible fix prompts:

```bash
# Generate a markdown report
npx vercel-doctor . --output markdown --report report.md

# Export AI prompts for fixing issues
npx vercel-doctor . --ai-prompts fixes.json
```

The `--ai-prompts` flag generates ready-to-use prompts for AI coding tools. It exports issues that have a known fix strategy, and for async parallelization findings it also adds a separate migration prompt to codemod `Promise.all` to `better-all` after review and checks. The format is auto-detected from the file extension:

* **`.json`** — Structured format for programmatic use and automation
* **`.md` or `.markdown`** — Human-readable format, easy to copy-paste into AI chats

```bash
# Export as JSON (for scripts, automation)
npx vercel-doctor . --ai-prompts fixes.json

# Export as Markdown (for manual copy-paste into Cursor/Claude/Windsurf)
npx vercel-doctor . --ai-prompts fixes.md
```

Each prompt includes:

* The specific rule violation
* File location, line, and column number
* Before/after code examples
* Step-by-step fix instructions

For async parallelization diagnostics, the output also includes:

* A repository-wide `Promise.all` → `better-all` codemod planning prompt
* A required sequence: review dry-run patch first, then run lint/typecheck/tests, then apply codemod changes


# Scoring



import { DynamicLink } from "fumadocs-core/dynamic-link";

Every scan produces a score from 0 to 100. The score reflects how many unique rules your project triggers, weighted by severity.

Calculation [#calculation]

```
score = max(0, round(100 - penalties))
```

Where penalties are:

| Severity | Penalty per unique rule |
| -------- | ----------------------- |
| Error    | 1.5 points              |
| Warning  | 0.75 points             |

The score is based on **unique rules triggered**, not the total count of diagnostics. If the same rule fires on 10 files, it only counts once.

Labels [#labels]

| Score  | Label      |
| ------ | ---------- |
| 75–100 | Great      |
| 50–74  | Needs work |
| 0–49   | Critical   |

Example [#example]

A project with 2 error rules and 8 warning rules:

```
penalty = (2 × 1.5) + (8 × 0.75) = 3 + 6 = 9
score = max(0, round(100 - 9)) = 91 → Great
```

Share your score [#share-your-score]

After running Vercel Doctor, your score is available on a shareable page with social sharing buttons and an embeddable badge.

Badge [#badge]

Add a badge to your `README.md`:

```markdown
[![Vercel Doctor](https://www.vercel-doctor.com/share/badge?s=91)](https://www.vercel-doctor.com/share?s=91)
```

API [#api]

You can also calculate scores programmatically:

```bash
curl -X POST https://www.vercel-doctor.com/api/score \
  -H "Content-Type: application/json" \
  -d '{"diagnostics": [...]}'
```

Returns:

```json
{
  "score": 91,
  "label": "Great"
}
```

The `/api/estimate-score` endpoint estimates what your score would be after fixing issues, using estimated fix rates of 85% for errors and 80% for warnings.


# Caching



import { DynamicLink } from "fumadocs-core/dynamic-link";

These rules identify patterns that bypass Vercel's CDN and ISR caching, forcing unnecessary server-side rendering and increasing compute costs.

Version-aware behavior [#version-aware-behavior]

Caching diagnostics adapt to the detected Next.js major version:

* **Next.js 15**: warnings emphasize that fetch and GET handlers are uncached by default.
* **Next.js 16+**: warnings prioritize Cache Components guidance (`"use cache"`, cache tags, targeted revalidation).
* **Other/unknown versions**: warnings use framework-agnostic cache policy guidance.

nextjs-no-side-effect-in-get-handler [#nextjs-no-side-effect-in-get-handler]

<Callout type="error">
  Error · 

  `vercel-doctor/nextjs-no-side-effect-in-get-handler`
</Callout>

Detects GET route handlers that contain side effects (mutating cookies, headers, or database calls) or are on mutating route segments like `/logout`, `/signout`, `/delete`, etc.

**Why it matters:** GET requests can be triggered by prefetching and are vulnerable to CSRF. Side effects in GET handlers also prevent caching.

```ts title="Bad"
// app/api/logout/route.ts
export async function GET() {
  cookies().delete("session");
  return Response.json({ ok: true });
}
```

```ts title="Good"
// app/api/logout/route.ts
export async function POST() {
  cookies().delete("session");
  return Response.json({ ok: true });
}
```

Detected mutating route segments: `logout`, `log-out`, `signout`, `sign-out`, `unsubscribe`, `delete`, `remove`, `revoke`, `cancel`, `deactivate`.

***

vercel-no-force-dynamic [#vercel-no-force-dynamic]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-no-force-dynamic`
</Callout>

Detects pages with `export const dynamic = "force-dynamic"`.

**Why it matters:** `force-dynamic` forces server-side rendering on every request, completely bypassing full-page caching.

```ts title="Bad"
export const dynamic = "force-dynamic";

export default function Page() { ... }
```

```ts title="Good"
export const revalidate = 3600;

export default function Page() { ... }
```

***

vercel-no-no-store-fetch [#vercel-no-no-store-fetch]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-no-no-store-fetch`
</Callout>

Detects fetch calls with `cache: "no-store"` or `revalidate: 0`.

**Why it matters:** These options disable caching entirely, increasing uncached bandwidth and compute costs on every request.

```ts title="Bad"
const data = await fetch(url, { cache: "no-store" });
```

```ts title="Good"
const data = await fetch(url, {
  next: { revalidate: 3600 },
});
```

***

vercel-missing-cache-policy [#vercel-missing-cache-policy]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-missing-cache-policy`
</Callout>

Detects GET route handlers without any explicit cache configuration — no `Cache-Control` headers, no `revalidate` export, and no `dynamic` export.

**Why it matters:** Without an explicit cache policy, responses may miss CDN caching opportunities, causing repeated origin hits.

```ts title="Bad"
export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}
```

```ts title="Good"
export const revalidate = 3600;

export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}
```

***

vercel-prefer-get-static-props [#vercel-prefer-get-static-props]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-prefer-get-static-props`
</Callout>

Detects Pages Router routes using `getServerSideProps`.

**Why it matters:** `getServerSideProps` runs on every request. Switching to `getStaticProps` (with optional ISR) caches pages at the CDN and reduces server compute.

***

vercel-get-static-props-consider-isr [#vercel-get-static-props-consider-isr]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-get-static-props-consider-isr`
</Callout>

Detects `getStaticProps` without a `revalidate` return value.

**Why it matters:** Without `revalidate`, all pages are built at deploy time. For large sites this slows builds dramatically. ISR generates pages on-demand and caches them.

```ts title="Good"
export async function getStaticProps() {
  const data = await fetchData();
  return {
    props: { data },
    revalidate: 3600,
  };
}
```


# Dead Code



import { DynamicLink } from "fumadocs-core/dynamic-link";

Dead code detection is powered by [Knip](https://knip.dev). Unused code bloats your bundle, slows cold starts, and increases deployment size — all of which translate to higher Vercel costs.

You can disable dead code detection entirely with `--no-dead-code` or `"deadCode": false` in your config file.

knip/files [#knipfiles]

<Callout type="warn">
  Warning · 

  `knip/files`
</Callout>

Detects files that are not imported by any other file in the project.

**Why it matters:** Unused files are still deployed and parsed during builds. They increase cold start time by adding to the module graph.

**Fix:** Delete the file, or if it's intentionally standalone, add it to your Knip config's entry patterns.

***

knip/exports [#knipexports]

<Callout type="warn">
  Warning · 

  `knip/exports`
</Callout>

Detects exported symbols (functions, variables, classes) that are never imported anywhere.

**Why it matters:** Unused exports add dead code to your bundle. Tree-shaking doesn't always eliminate them, especially in server-side code.

**Fix:** Remove the `export` keyword, or delete the symbol if it's no longer needed.

***

knip/types [#kniptypes]

<Callout type="warn">
  Warning · 

  `knip/types`
</Callout>

Detects exported TypeScript types and interfaces that are never referenced.

**Fix:** Remove unused type exports.

***

knip/duplicates [#knipduplicates]

<Callout type="warn">
  Warning · 

  `knip/duplicates`
</Callout>

Detects the same symbol exported from multiple files.

**Why it matters:** Duplicate exports create ambiguity and can lead to importing from the wrong file, pulling in more code than necessary.

**Fix:** Consolidate duplicates into a single export location.


# Function Duration



import { DynamicLink } from "fumadocs-core/dynamic-link";

These rules catch patterns that inflate the wall-clock time of your serverless functions. Shorter functions mean lower bills — Vercel charges per GB-second of execution time.

async-parallel [#async-parallel]

<Callout type="warn">
  Warning · 

  `vercel-doctor/async-parallel`
</Callout>

Detects 3 or more sequential `await` statements that appear independent — meaning later awaits don't reference variables from earlier ones.

**Why it matters:** Sequential awaits run one after another. If they're independent, running them in parallel with `Promise.all()` can cut function duration significantly.

```ts title="Bad"
const users = await db.user.findMany();
const posts = await db.post.findMany();
const comments = await db.comment.findMany();
```

```ts title="Good"
const [users, posts, comments] = await Promise.all([
  db.user.findMany(),
  db.post.findMany(),
  db.comment.findMany(),
]);
```

***

server-after-nonblocking [#server-after-nonblocking]

<Callout type="warn">
  Warning · 

  `vercel-doctor/server-after-nonblocking`
</Callout>

Detects `console.log()`, `console.info()`, `console.warn()`, `analytics.track()`, `analytics.identify()`, or `analytics.page()` calls in server actions (files with `"use server"`).

**Why it matters:** Logging and analytics calls block the response until they complete. Wrapping them in `after()` lets the response finish immediately while the work continues in the background.

```ts title="Bad"
"use server";

export const updateUser = async (data: FormData) => {
  await db.user.update({ ... });
  console.log("User updated");
  analytics.track("user_updated");
};
```

```ts title="Good"
"use server";

import { after } from "next/server";

export const updateUser = async (data: FormData) => {
  await db.user.update({ ... });
  after(() => {
    console.log("User updated");
    analytics.track("user_updated");
  });
};
```

***

vercel-edge-sequential-await [#vercel-edge-sequential-await]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-edge-sequential-await`
</Callout>

Detects edge runtime files with 2 or more sequential `await` calls without `Promise.all`.

**Why it matters:** Edge functions have strict execution time limits. Sequential I/O wastes precious milliseconds that could be parallelized.

```ts title="Bad"
export const runtime = "edge";

export async function GET() {
  const user = await getUser();
  const settings = await getSettings();
  return Response.json({ user, settings });
}
```

```ts title="Good"
export const runtime = "edge";

export async function GET() {
  const [user, settings] = await Promise.all([getUser(), getSettings()]);
  return Response.json({ user, settings });
}
```


# Image Optimization



import { DynamicLink } from "fumadocs-core/dynamic-link";

These rules identify misconfigurations in Next.js image handling that increase Vercel Image Optimization usage or serve unoptimized images.

vercel-image-global-unoptimized [#vercel-image-global-unoptimized]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-image-global-unoptimized`
</Callout>

Detects `images.unoptimized: true` in `next.config`.

**Why it matters:** This disables Vercel Image Optimization globally. All images are served as-is, without resizing, format conversion, or compression — increasing bandwidth costs and hurting performance.

```js title="Bad"
// next.config.js
module.exports = {
  images: {
    unoptimized: true,
  },
};
```

```js title="Good"
// next.config.js
module.exports = {
  images: {
    remotePatterns: [{ hostname: "cdn.example.com" }],
  },
};
```

***

nextjs-image-missing-sizes [#nextjs-image-missing-sizes]

<Callout type="warn">
  Warning · 

  `vercel-doctor/nextjs-image-missing-sizes`
</Callout>

Detects `<Image>` components with `fill` but no `sizes` attribute.

**Why it matters:** Without `sizes`, the browser downloads the largest available image regardless of viewport width. This wastes bandwidth and increases Image Optimization usage.

```tsx title="Bad"
<Image src="/hero.jpg" alt="Hero" fill />
```

```tsx title="Good"
<Image src="/hero.jpg" alt="Hero" fill sizes="(max-width: 768px) 100vw, 50vw" />
```

***

vercel-image-remote-pattern-too-broad [#vercel-image-remote-pattern-too-broad]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-image-remote-pattern-too-broad`
</Callout>

Detects `images.remotePatterns` entries with overly broad `pathname` patterns (`/**` or missing entirely).

**Why it matters:** Unrestricted remote image paths allow any URL on the hostname to be optimized through your account, potentially driving unexpected optimization usage from user-generated content or third-party embeds.

```js title="Bad"
// next.config.js
module.exports = {
  images: {
    remotePatterns: [{ hostname: "cdn.example.com", pathname: "/**" }],
  },
};
```

```js title="Good"
// next.config.js
module.exports = {
  images: {
    remotePatterns: [{ hostname: "cdn.example.com", pathname: "/avatars/**" }],
  },
};
```

***

vercel-image-svg-without-unoptimized [#vercel-image-svg-without-unoptimized]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-image-svg-without-unoptimized`
</Callout>

Detects `<Image>` components with an SVG `src` that don't have the `unoptimized` prop.

**Why it matters:** SVGs are vector graphics and don't benefit from the Image Optimization pipeline. Running them through it wastes optimization quota without any benefit.

```tsx title="Bad"
<Image src="/logo.svg" alt="Logo" width={100} height={40} />
```

```tsx title="Good"
<Image src="/logo.svg" alt="Logo" width={100} height={40} unoptimized />
```


# Invocations



import { DynamicLink } from "fumadocs-core/dynamic-link";

These rules catch patterns that trigger extra serverless function invocations, increasing your Vercel bill through additional compute cycles.

nextjs-no-client-fetch-for-server-data [#nextjs-no-client-fetch-for-server-data]

<Callout type="warn">
  Warning · 

  `vercel-doctor/nextjs-no-client-fetch-for-server-data`
</Callout>

Detects `useEffect` + `fetch` patterns in page or layout files. This pattern turns what could be a single server render into two round-trips: one to render the page and another API call from the client.

**Why it matters:** Each client-side fetch triggers a separate serverless function invocation. Fetching data server-side in a Server Component eliminates the API round-trip entirely.

```tsx title="Bad"
"use client";

import { useEffect, useState } from "react";

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/data")
      .then((res) => res.json())
      .then(setData);
  }, []);

  return <div>{data?.title}</div>;
}
```

```tsx title="Good"
export default async function Page() {
  const data = await fetchData();
  return <div>{data.title}</div>;
}
```

***

nextjs-link-prefetch-default [#nextjs-link-prefetch-default]

<Callout type="warn">
  Warning · 

  `vercel-doctor/nextjs-link-prefetch-default`
</Callout>

Detects `<Link>` components without `prefetch={false}`. By default, Next.js prefetches every visible link, which can trigger a large number of serverless function invocations on pages with many links.

**Why it matters:** Each prefetch is a server request. On pages with dozens of links, this creates a burst of invocations when the page loads. Disable prefetch globally and add it back only to critical navigation links.

```tsx title="Bad"
<Link href="/dashboard">Dashboard</Link>
<Link href="/settings">Settings</Link>
<Link href="/profile">Profile</Link>
```

```tsx title="Good"
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
<Link href="/settings" prefetch={false}>Settings</Link>
<Link href="/profile" prefetch={true}>Profile</Link>
```


# Platform



import { DynamicLink } from "fumadocs-core/dynamic-link";

These rules detect platform-level configuration issues and suggest optimizations for Vercel deployments.

vercel-edge-heavy-import [#vercel-edge-heavy-import]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-edge-heavy-import`
</Callout>

Detects edge runtime files importing heavy or Node-centric dependencies like `node:fs`, `node:crypto`, `sharp`, or `@aws-sdk/*`.

**Why it matters:** Edge functions have strict size and execution limits. Heavy Node.js dependencies increase cold start time and may fail at runtime.

**Fix:** Move heavy logic to Node.js runtime functions or background jobs, and keep edge handlers lightweight.

***

vercel-sequential-database-await [#vercel-sequential-database-await]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-sequential-database-await`
</Callout>

Detects API routes with 3 or more sequential Prisma or database calls without `Promise.all`.

**Why it matters:** Each sequential database call adds latency to the function execution. Parallelizing independent queries reduces total duration and cost.

```ts title="Bad"
const users = await prisma.user.findMany();
const posts = await prisma.post.findMany();
const tags = await prisma.tag.findMany();
```

```ts title="Good"
const [users, posts, tags] = await Promise.all([
  prisma.user.findMany(),
  prisma.post.findMany(),
  prisma.tag.findMany(),
]);
```

***

vercel-large-static-asset [#vercel-large-static-asset]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-large-static-asset`
</Callout>

Detects static assets (images, fonts, videos, PDFs) of 4 KB or larger served from your app repository.

**Why it matters:** Large static files served from your Vercel deployment consume bandwidth on every request. Moving them to a dedicated CDN or object storage (Cloudflare R2, S3) reduces bandwidth costs.

Reports up to 20 files, sorted by size (largest first).

***

vercel-consider-bun-runtime [#vercel-consider-bun-runtime]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-consider-bun-runtime`
</Callout>

Detects projects not configured for the Bun runtime (no `packageManager: "bun@..."` in `package.json` and no `bun.lock` file).

**Why it matters:** Bun runtime can reduce install and build overhead on Vercel compared to Node.js.

**Fix:** Review [Bun runtime guidance](https://vercel.com/docs/functions/runtimes/bun) and switch if your project is compatible.

***

vercel-avoid-platform-cron [#vercel-avoid-platform-cron]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-avoid-platform-cron`
</Callout>

Detects `crons` configured in `vercel.json`.

**Why it matters:** Vercel cron jobs run as serverless functions, billed per execution. Scheduled workloads with predictable patterns can often run cheaper using GitHub Actions or Cloudflare Workers Cron Triggers.

***

vercel-consider-fluid-compute [#vercel-consider-fluid-compute]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-consider-fluid-compute`
</Callout>

Detects projects with 3 or more API/server routes.

**Why it matters:** Fluid Compute improves concurrency and reduces execution overhead for workloads with variable latency or bursty traffic. It's worth evaluating for projects with multiple server routes.

***

vercel-suggest-turbopack-build-cache [#vercel-suggest-turbopack-build-cache]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-suggest-turbopack-build-cache`
</Callout>

Detects `next.config` files with `experimental` settings but without `turbopackFileSystemCacheForBuild`.

**Why it matters:** Next.js 16+ supports Turbopack build cache, which can significantly reduce build times. This check is version-aware and only applies to Next.js 16+ projects.

```js title="Good"
// next.config.js
module.exports = {
  experimental: {
    turbopackFileSystemCacheForBuild: true,
  },
};
```

***

vercel-suggest-deploy-archive [#vercel-suggest-deploy-archive]

<Callout type="warn">
  Warning · 

  `vercel-doctor/vercel-suggest-deploy-archive`
</Callout>

Detects projects with 5,000 or more files.

**Why it matters:** Large projects can hit API rate limits during deployment. Using archive mode uploads a single tarball instead of individual files, cutting deployment time by roughly 50%.

```bash
vercel deploy --archive=tgz
```
