构建、测量与部署

Turbopack 已是 Next 16 默认打包器;@next/bundle-analyzer 以 --webpack 回退实装(pnpm analyze 可跑,报告在 .next/analyze/);部署侧讲 standalone 自托管、instrumentation + OTel 与 after()

Turbopack:Next 16 的默认打包器

dev 与 build 都默认 Turbopack,不再是实验选项

  • 事实:Next 16 起 next dev 与 next build 默认都跑 Turbopack(Rust 实现,增量编译);webpack 变成显式 opt-out (--webpack 标志)
  • 本站就在用:日常的 pnpm dev / pnpm build 全部是 Turbopack;唯一的例外是下方的 bundle 分析,因为 analyzer 是 webpack 插件
  • 什么时候需要回退 webpack:依赖尚未迁移的 webpack 专属 loader/plugin(如本站实装的 @next/bundle-analyzer)、 或排查「疑似打包器差异」时做对照构建。回退是一次性的命令级选择, 不需要改配置
  • Turbopack 侧的配置(自定义 loader 规则、resolve 别名)写在 next.config.ts 的 turbopack 字段;webpack 函数与 turbopack 字段可以共存,按实际打包器各取所需

bundle 分析实装:pnpm analyze

测量先行:任何「优化包体」的动作之前,先让体积可见

// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";

export default withBundleAnalyzer({
    enabled: process.env.ANALYZE === "true",
    // 构建机/CI 上别弹浏览器,报告写到 .next/analyze/*.html
    openAnalyzer: false,
})(nextConfig);

// package.json —— 关键在 --webpack:
// @next/bundle-analyzer 是 webpack 插件,Next 16 默认的 Turbopack
// 构建下它不产出任何报告(只会打一行警告),所以要显式回退跑一次
{ "scripts": { "analyze": "ANALYZE=true next build --webpack" } }
$ pnpm analyze          # ANALYZE=true next build --webpack
▲ Next.js 16.2.4 (webpack)
- Cache Components enabled

  Creating an optimized production build ...
Webpack Bundle Analyzer saved report to .next/analyze/nodejs.html
Webpack Bundle Analyzer saved report to .next/analyze/edge.html
Webpack Bundle Analyzer saved report to .next/analyze/client.html
✓ Compiled successfully

# 日常构建(不带 --webpack)则跑 Turbopack,快得多但不出 analyzer 报告:
$ pnpm build            # ▲ Next.js 16.2.4 (Turbopack)
  • 报告落在 .next/analyze/ 下三份:client.html(浏览器包, 优化主战场)、nodejs.html(服务端运行时)、edge.html(边缘运行时, 本站 middleware 即在此列)
  • 分析的是 webpack 构建产物,与 Turbopack 产物的分包细节会有出入; Turbopack 原生的分析入口是 next experimental-analyze
  • 看报告的次序:先 client.html 的 First Load 共享 chunk (每页都付的成本),再按路由看独有 chunk; 「谁被谁引进来」比「谁最大」更常给出可执行的结论 —— 与 /engineering/asset-perf 的包体治理一节衔接

部署:output: standalone 自托管

讲解片段;本站刻意未开启 standalone

// next.config.ts —— 演示片段,本站并未开启
const nextConfig: NextConfig = {
    output: "standalone",
};

// 构建产物:.next/standalone/ 是一个自包含目录,
// 只含运行所需的最小 node_modules + server.js:
//   node .next/standalone/server.js
// 注意:public/ 与 .next/static/ 不会自动并入,
// 自托管时要自行拷贝(官方 Dockerfile 模板里就是两行 COPY)
  • 为什么本站不开:本站是教学演示,直接用 next start(Vercel 部署同理)即可;standalone 的收益是 「自包含目录 + 最小 node_modules」,服务于 Docker 镜像体积与 离线部署 —— 演示站没有这两个约束,开了只增加构建产物噪音
  • 什么时候需要它:自托管容器(Kubernetes/Docker)、 私有环境交付、对镜像层大小敏感的场景;官方 with-docker 示例的 Dockerfile 就是围绕 standalone 产物写的

可观测性:instrumentation + OTel,以及 after()

链路追踪回答「慢在哪」,after() 保证「记日志」本身不变慢

// instrumentation.ts —— 包根,与 app/ 同级;Next 启动时自动执行一次
export async function register() {
    // 按运行时条件加载:OTel SDK 依赖 Node API,Edge 运行时跳过
    if (process.env.NEXT_RUNTIME === "nodejs") {
        const { registerOTel } = await import("@vercel/otel");
        registerOTel({ serviceName: "next-playground" });
    }
}

// 之后所有 Server Component 渲染、fetch、Route Handler
// 都会自动产生 trace span;接入 Jaeger/Tempo/Datadog 即可观测
// Route Handler / Server Component / Server Action 里都能用
import { after } from "next/server";

export async function POST(request: Request) {
    const result = await doWork(request);

    // 响应先发回去,日志/指标/通知在响应之后执行:
    // 可观测性代码不再拖慢用户感知的延迟
    after(async () => {
        await logAnalytics(result);
        await flushMetrics();
    });

    return Response.json(result);
}

两者拼起来是完整的请求级观测:register() 在进程启动时挂好 OTel,渲染与取数自动产生 span;after() 把日志与指标上报挪到响应之后, 观测代码不再计入用户感知的延迟。after() 的执行模型 (响应发出后服务端仍保活跑完副作用)在 /data/dynamic-apis 专题有更细的展开。