发布日期

第 37 讲:调试技巧大全:dev / Node inspector / source map / production debug

开发调试、Node inspector、source map 生产调试与性能分析工具箱

第 33 讲教你接住错误并上报,第 36 讲教你看 V8 profiler。本讲是调试器使用手册:dev 模式怎么单步、prod 容器里怎么抓 trace、source map 配置怎么影响 stack 显示、__NEXT_SHOW_IGNORE_LISTED 怎么用、RSC stack 怎么读、Chrome DevTools 与 VSCode 远程 attach 的实战配置。

学习目标

  1. 在 dev 模式 attach Chrome DevTools / VSCode 单步调 server-side code(包括 Server Component)。
  2. 解读 Next.js dev overlay 的 stack 与 source map 信息;用 __NEXT_SHOW_IGNORE_LISTED=true 看完整 framework frame。
  3. 在 prod 容器里抓 heap snapshot / CPU profile,不重启服务。
  4. 看懂 RSC stack 的来源标记,区分 server stack / client stack / hydration stack。
  5. 用 5 类调试手段诊断生产事故:断点 / log / heap snapshot / CPU profile / strace。

一、Dev 模式 server-side 调试

1.1 用 --inspect 启 dev server

# 启动并暴露 inspect 端口(默认 9229)
NODE_OPTIONS='--inspect' pnpm next dev

# 或指定端口
NODE_OPTIONS='--inspect=0.0.0.0:9230' pnpm next dev

打开 Chrome → chrome://inspect → 看到 "next.js" 进程 → 点 "inspect"。

进入 Chrome DevTools 后:

  • Sources 面板:能看到 webpack://(webpack)或 turbopack://(turbopack)开头的虚拟 URL
  • 给 Server Component / Route Handler / Server Action 打断点
  • 触发请求 → 命中断点 → 单步

1.2 --inspect-brk 启动时立即停在第一行

NODE_OPTIONS='--inspect-brk' pnpm next dev

适合调启动期初始化逻辑(instrumentation.ts 的 register)。

1.3 VSCode 配置 launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side (Node)",
      "type": "node",
      "request": "launch",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": ["dev"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "skipFiles": ["<node_internals>/**"]
    },
    {
      "name": "Next.js: debug client-side (Chrome)",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    },
    {
      "name": "Next.js: attach to running",
      "type": "node",
      "request": "attach",
      "port": 9229,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

3 种配置:

  • launch (Node):VSCode 自己启动 pnpm dev
  • launch (Chrome):调客户端
  • attach:连到已经启动的 --inspect 进程

1.4 多 worker 进程 inspect

Next.js dev 默认 fork 出 router-server + render-worker。单个 9229 端口只接到 1 个。如果断点没命中:

# 给所有子进程开 inspect
NODE_OPTIONS='--inspect=0.0.0.0:9229' pnpm next dev
# Node 自动给子进程递增端口:9230, 9231...

或在 chrome://inspect 里 "Open dedicated DevTools for Node" → 自动发现所有 inspect 进程。

二、Production 模式调试

dev 关掉了 minify、保留 stack、加了 source map。prod 完全不同。

2.1 prod 容器里临时打开 inspect

K8s pod 不能改启动参数?用 kill -USR1

# 进容器
kubectl exec -it my-pod -- /bin/sh

# 给主进程 PID 1 发 USR1 信号
kill -USR1 1

# Node 会监听 127.0.0.1:9229,但需要端口转发到本地
# 在本地另开终端
kubectl port-forward my-pod 9229:9229

# 然后 chrome://inspect 连 localhost:9229

⚠️ 安全:inspect 端口暴露 = 任意代码执行。只 attach 到本地端口转发,不要 0.0.0.0 暴露

2.2 抓 heap snapshot 不停服

// 在某个 admin-only route handler
import { writeHeapSnapshot } from "v8";

export async function POST(req: NextRequest) {
  await requireAdmin(req);
  const path = `/tmp/heap-${Date.now()}.heapsnapshot`;
  writeHeapSnapshot(path);
  return Response.json({ path });
}

调用后从容器拷出来:

kubectl cp my-pod:/tmp/heap-xxx.heapsnapshot ./

在 Chrome DevTools → Memory → Load → 选这个 snapshot。

对比 2 个时间点的 snapshot 找内存泄漏(Comparison view)。

2.3 抓 CPU profile

类似:

import { Session } from "node:inspector/promises";

export async function POST(req: NextRequest) {
  await requireAdmin(req);
  const session = new Session();
  session.connect();
  await session.post("Profiler.enable");
  await session.post("Profiler.start");

  // 等 N 秒(或让 admin 触发 stop)
  await new Promise((r) => setTimeout(r, 30_000));

  const { profile } = (await session.post("Profiler.stop")) as any;
  const fs = await import("node:fs/promises");
  const path = `/tmp/cpu-${Date.now()}.cpuprofile`;
  await fs.writeFile(path, JSON.stringify(profile));
  session.disconnect();
  return Response.json({ path });
}

DevTools → Performance → Load 这个 .cpuprofile。

2.4 现成工具 0x / clinic

npx 0x -- node packages/next/dist/bin/next start
# 跑负载
# Ctrl+C
# 自动生成 火焰图 HTML

或 clinic(第 36 讲讲过)。

三、Source map 配置

3.1 next build 自动产 source map

prod build 默认:

  • client bundle _next/static/chunks/*.js 有对应 .js.map
  • server bundle 在 .next/server/ 里也有 .map

但默认 productionBrowserSourceMaps: false → client map 不发到 CDN。打开:

// next.config.js
module.exports = {
  productionBrowserSourceMaps: true,
};

⚠️ 公开 source map 意味着源码暴露。生产环境通常只上传到 Sentry / 自家服务,浏览器不公开访问。

3.2 Sentry 自动上传

pnpm add @sentry/nextjs
npx @sentry/wizard

它会在 next.config.js 里包一层 plugin,build 时把 source map 上传到 Sentry,client bundle 删掉本地 map。错误聚合时 Sentry 自动反查源码位置。

3.3 RSC 错误的 source map

Server Component 的 stack 在第 33 讲讲过:digest 化、隐藏内部细节。dev 模式有完整 stack,prod 没有。

如果你想在 prod 看完整 server stack(自查、不暴露给用户):

// instrumentation.ts → onRequestError
export const onRequestError = (err, req, ctx) => {
  console.error("Full server stack:", err.stack); // 进 server log(不进 response)
  Sentry.captureException(err, { contexts: { request: req, next: ctx } });
};

stack 不会发到客户端,但能进你的 log 系统。

四、__NEXT_SHOW_IGNORE_LISTED

packages/next/src/server/patch-error-inspect.ts
const showIgnoreListed = process.env.__NEXT_SHOW_IGNORE_LISTED === 'true'

Next.js 默认会把 framework 内部 frame 收起:

Error: oops
    at MyPage (app/foo/page.tsx:5:13)
    at <ignore-listed frames>      ← 折叠
    at handleRequest (router-server.ts:120:5)

__NEXT_SHOW_IGNORE_LISTED=true 后:

Error: oops
    at MyPage (app/foo/page.tsx:5:13)
    at renderToStringStream (app-render.tsx:823:11)
    at ServerRouteRenderer.handle (route-modules/...:45:9)
    at handleRequest (router-server.ts:120:5)
    ...

适用场景:

  • 调试自己改的 Next.js 源码
  • 想看具体走哪个 route module
  • bug report 给 Next.js 团队

同类 env

Env效果
__NEXT_SHOW_IGNORE_LISTED=true不折叠 framework frame
NEXT_PRIVATE_DEBUG_CACHE=trueIncrementalCache 打 debug log
DEBUG=next:*next.js 自带的 debug logger
NEXT_TELEMETRY_DEBUG=1telemetry 不发送但 console
NEXT_PRIVATE_PRINT_HANG_WARNING=true渲染挂起时打 stack

五、Dev overlay 与 turbopack tracing

dev overlay = 浏览器右下角红色错误提示。它能展开看:

  • 完整 stack(已 source-mapped)
  • "Edit in editor" 跳到 VSCode 行
  • 修复后自动消失(Fast Refresh)

如果 overlay 看不到错误(白屏),可能 root layout 崩了 → 应该走 global-error 但 dev 模式更可能就 console.error 完事。

turbopack 特有

turbopack 在 dev 模式有更细的 module tracing:

NEXT_TURBOPACK_TRACING=trace pnpm next dev

会在 .next/trace.log 写一份 turbopack 的所有 task 调用。配合 turbo-trace-viewer 看 task graph:

pnpm dlx turbo-trace-viewer .next/trace.log

排查 "为什么这个文件没有热更新"、"为什么这个 chunk 没拆开" 之类的问题。

六、Client 错误调试

6.1 错误来自 Client Component

dev:overlay 显示。prod:根据 error boundary 展示。

6.2 hydration mismatch

Error: Hydration failed because the initial UI does not match what was rendered on the server.

定位流程:

  1. 看 dev overlay 的 diff 提示:哪段 server HTML 与 client 不匹配
  2. 常见原因:
    • Date.now() / Math.random() 在 render 期直接调用
    • window.localStorage 在 server 端为 undefined
    • 用了 useEffect 但又同时在初始 render 改 DOM
  3. 修复:
    • 时间戳类用 useEffect + useState 推迟到 client
    • 或用 suppressHydrationWarning(仅作 last resort)

6.3 客户端 source map

DevTools → Sources → 找 webpack:// 或 turbopack:// → 文件结构对应你的 src/。打断点不需要额外配置。

七、5 类调试手段对比

手段场景工具
断点复现一次能命中的 bugChrome DevTools / VSCode
log偶发、需要在多个时刻看状态pino + Datadog
heap snapshot内存增长 / 泄漏DevTools Memory
CPU profileCPU 100% / 慢 endpointDevTools Performance / 0x
strace / dtraceI/O 异常、syscall 卡Linux strace、macOS dtrace

strace 实战

# 哪个系统调用慢
strace -ttt -T -p $(pidof node) 2>&1 | head -100

# 文件 I/O 跟踪
strace -e trace=openat,read,write -p $(pidof node)

可以看到比如 read(7, ..., 65536) = 0 (timeout) 这种证据。

八、生产事故"3 看"模板

发生事故时:

  1. 看监控(第 34 讲):哪个 page 哪个 metric 异常?时间起点是什么?
  2. 看 trace(第 34 讲):哪个 span 慢/错?是上游、还是本地代码?
  3. 看 log(第 34 讲 + 第 33 讲 onRequestError):那一时刻具体的 error 是什么?

3 看完后,进入针对性调试:

  • 是 hot path 慢?→ CPU profile
  • 是内存爆?→ heap snapshot
  • 是上游慢?→ 找上游团队
  • 是逻辑 bug?→ dev 复现 + 断点

九、debugger; 语句

老办法但有效:

// app/foo/page.tsx
export default async function Foo() {
  const data = await fetchData()
  debugger;   // 进 inspect 时停在这里
  return <Stuff data={data} />
}

只对 --inspect 启动的进程有效,prod 没 inspect 时是 noop。

十、配套 fixture

fixtures/lecture-37/ 提供:

  • /leak:故意泄漏 closure → 多次访问 heap 增长
  • /cpu:CPU hot loop → 抓 profile
  • /hydration-bad vs /hydration-good:hydration mismatch 演示
  • app/api/debug/heap-snapshot/route.ts:触发 heap snapshot 写盘
  • app/api/debug/cpu-profile/route.ts:触发 30s CPU profile

启动:

cd learning/nextjs-40-lectures/fixtures/lecture-37
pnpm install && pnpm build && NODE_OPTIONS='--inspect=0.0.0.0:9229' pnpm start

# 1. attach Chrome
# 2. curl http://localhost:3037/leak (×100)
# 3. curl -X POST http://localhost:3037/api/debug/heap-snapshot
# 4. /tmp/heap-*.heapsnapshot → Chrome Memory → Load

十一、本讲小结

  1. dev 用 --inspect + Chrome / VSCode 单步;attach 现成进程也行。
  2. prod 容器用 kill -USR1 临时开 inspect;本地端口转发不要暴露。
  3. heap snapshot / CPU profile 都可以不停服抓。
  4. __NEXT_SHOW_IGNORE_LISTED=true 看完整 framework stack。
  5. source map 自动产但 prod 默认不公开;接 Sentry 自动上传。
  6. 事故"3 看":监控 → trace → log,再针对性调试。

下讲预告

第 38 讲《5 个真实生产案例复盘》。我会综合前面所有讲过的知识,剖析 5 个典型生产故障,从症状到 root cause 到修复 PR,演示完整事故复盘流程。