Skip to main content

Next.js 工程实践要点

Next.js 的上手成本低,但生产里踩的坑集中在 middleware 边界、图片、产物形态和第三方脚本这四件事上。

一、Middleware 跑在哪、不能做什么​

Middleware 跑在受限的运行时里,不是完整的 Node 环境(Next.js 16 起它改名 proxy.ts 并改跑 Node 运行时,见后文):很多 Node API 不可用,包体积与执行时间也有限制。它能做的事是轻量判断与改写(重定向、A/B 分流、简单的地域判断),不该做的事是查库、做完整鉴权——那些应该放在路由处理或服务端组件里。把重逻辑塞进 Middleware,最常见的后果是冷启动变慢与超时。

// middleware.ts:只做轻量判断与改写
import { NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const zone = req.cookies.get('zone')?.value ?? 'cn'
if (req.nextUrl.pathname.startsWith('/promo')) {
return NextResponse.redirect(new URL(`/${zone}/promo`, req.url))
}
return NextResponse.next()
}

截至 2026 年 10 月,Next.js 16 起该文件改名为 proxy.ts 并运行在 Node.js 运行时,能力边界比 Edge 时期宽,但核心约束没变——它仍是请求前的轻量层,不是业务层,重逻辑要下沉到路由或服务端组件。一个实用判断:Middleware / proxy 适合「看一眼请求就决定去向」,不适合「为了决定去向要先查一堆数据」——后者请放行到路由 handler 或服务端组件,那里才有完整的运行时与数据访问能力。

二、图片优化的收益与代价​

它做的三件事直接是收益:按设备出尺寸、自动转 AVIF/WebP、懒加载。但要注意两点:远程图片域名必须显式配置 remotePatterns,否则会报错或走未优化路径;LCP 图片(首屏最大的那张)要设 priority 关掉懒加载,否则它反而会拖慢首屏。

// next.config 里声明允许的远程图源,否则外部图优化会失败
module.exports = {
images: {
remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }],
},
}

外部图源不配置会出问题;自建图片服务时还要评估优化接口本身的代价,不是开了就免费变快。另外 next/image 的优化发生在服务端或边缘,需要 Node 运行时支持;纯静态导出(output: export)下图片优化不可用,要么用未优化原图,要么把图交给外部优化服务。

三、产物形态与部署平台绑定​

三种形态按场景选:默认产物需要一个 Node 进程来跑;纯静态内容可以导出成静态文件丢到对象存储;容器化部署常用 standalone 输出——它只保留运行必需的文件,镜像显著变小。选择标准不是「哪个更先进」,而是这次部署有没有 Node 运行时、需不需要服务端能力。standalone 输出只是把运行必需文件挑出来,依赖仍要在 builder 阶段装全;它不影响你需要 Node 运行时这一事实——纯静态导出没有 standalone 这回事,两条路别混。

FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
默认构建output: standaloneoutput: export需要 Node 进程,运行时能力完整单文件 + 最小依赖,容器镜像明显更小纯静态,图片优化与动态能力都用不了
图:三种产物形态对应的部署目标——选错形态,后面的优化都白做

四、第三方脚本是性能最大变量​

next/script 的三种策略决定脚本什么时候阻塞页面:beforeInteractive 阻塞首屏(极少用)、afterInteractive 适合统计类(首屏后可加载)、lazyOnload 适合完全非关键的。不要直接在 head 里写 script 标签,那会阻塞渲染且无法被框架调度。

import Script from 'next/script'
// 统计类:首屏后可加载,不阻塞交互
<Script src="https://analytics.example.com/sdk.js" strategy="afterInteractive" />
// 完全非关键:空闲再加载
<Script src="https://widget.example.com/embed.js" strategy="lazyOnload" />

脚本策略是性能变量,选错一个统计脚本就可能吃掉首屏。

五、构建与缓存上的几处​

三条最高频:客户端可见的环境变量在构建期内联进产物,运行时改不了——别以为运行时注入的 NEXT_PUBLIC_ 能动态切换;多层缓存叠加导致「数据不更新」,排查时先定位哪层没失效;构建期取数(在模块顶层 await fetch)会被意外缓存进构建产物,动态数据要放到请求期取。

// 客户端可见变量构建期内联,运行时改无意义
// .env.local: NEXT_PUBLIC_API=https://api.example.com
fetch(process.env.NEXT_PUBLIC_API) // 构建时就被替换成字面量

第四条常被忽略:App Router 下读取请求上下文的 cookies() / headers() 是异步的(React 19 / Next 15 起),在渲染里同步调用会拿到警告甚至报错,记得 await。

六、默认配置不够用​

环境变量是构建期内联进产物的,客户端可见的变量在打包时就固定了,运行时注入改不了它们。

图片优化也不是零配置:外部图源要显式配置允许的域名,否则会报错或静默走未优化路径。

缓存更不是「自动的,不用管」。多层缓存叠加是「数据不更新」类问题的高发区,必须知道每一层怎么失效。

Next.js 的工程问题大多不是 API 不会用,而是「缓存在哪一层」和「配置在什么时机注入」没想清楚。

官方的 Optimizing 章节覆盖了包体积、图片、字体等常规项,落地前扫一遍能省不少试错。