2026 最佳实践:在 Cloudflare Workers 上部署 Next.js(OpenNext 完整指南)

2026-07 更新:本文已按 2026 年年中的 OpenNext 生态现状全面更新。@opennextjs/cloudflare 现在是 Cloudflare 官方推荐的 Next.js 部署路径(next-on-pages 已经退役),我自己的博客也用这套方案在生产环境跑了大半年。这次更新补充了真实项目的 wrangler.jsonc / open-next.config.ts 配置、ISR 与 R2 增量缓存、Workers CPU 时间限制、一个 set-cookie 毁掉边缘缓存等一整批实战踩坑记录,全部来自线上真实事故,不是文档搬运。

随着 Vibe coding 兴起,我们有越来越多的项目基于 Next.js + OpenNext 构建,部署在 Cloudflare Workers 上。这套方案性能强悍、成本极低,堪称目前全栈开发的“版本答案”。

但是,很多同学(包括我自己)从 Vercel、Docker 或者 VPS 迁移过来时,都会掉进同一批坑里:环境变量怎么不生效? 或者为什么本地好好的,部署上去就是 undefined?甚至,怎么部署又失败了,为啥访问不到数据库?

今天我们就借着手里这个实战项目——也就是你正在看的这个博客——来彻底搞清楚 Cloudflare Workers 环境下部署 Next.js 的门道。顺带说一句,如果你还没迁移、正在犹豫,可以先看我之前写的《把 Next.js 从 Vercel 迁移到 Cloudflare Workers》,那篇讲的是迁移动机和整体步骤;本文更聚焦“迁过来之后怎么配好、怎么别踩坑”。

🗺️ 先对齐 2026 现状:OpenNext 已经是官方答案

先把生态现状说清楚,免得你照着老教程折腾。截至 2026 年中:

  • @opennextjs/cloudflare 是官方推荐路径。老一代的 @cloudflare/next-on-pages 已经退役,官方文档也把新项目全部指向 OpenNext 适配器。如果你搜到的教程还在讲 next-on-pages + Edge Runtime,直接关掉。
  • 跑在 Workers 的 Node.js 运行时上nodejs_compat),不再强迫你把所有路由改成 Edge Runtime。App Router、Route Handlers、Middleware、Server Actions、ISR 都支持。
  • ISR / 增量缓存要自己配:靠 R2 + Durable Objects + D1 组一套“三件套”(下文详解),这是和 Vercel 差异最大的地方。
  • next/image 要换自定义 loader,对接 Cloudflare 的 /cdn-cgi/image/ 边缘裁剪,或者用 IMAGES binding。

本站就是真实案例

你现在看到的这个博客,就是 Next.js 16 + OpenNext 部署在 Workers 上的,版本和关键脚本长这样(直接从 package.json 里抄的):

{
  "scripts": {
    "dev": "next dev -p 3100",
    "build": "next build",
    "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
    "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
  },
  "dependencies": {
    "next": "^16.2.4",
    "@opennextjs/cloudflare": "^1.19.4"
  },
  "devDependencies": {
    "wrangler": "^4.86.0"
  }
}

日常开发直接 next dev,跟在 Vercel 上没区别;部署就是 opennextjs-cloudflare build 把 Next.js 构建产物转换成 Worker,再 deploy 上去。open-next.config.ts 是全部的适配器配置,真实内容就这么几行:

// open-next.config.ts
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
import d1TagCache from '@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache';

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache, // ISR 页面缓存存 R2
  queue: doQueue,                       // revalidate 队列用 Durable Object
  tagCache: d1TagCache,                 // revalidateTag 的索引存 D1
  enableCacheInterception: true,        // 缓存命中时不进 Next.js 服务逻辑,直接回
});

对应的 wrangler.jsonc(删掉了业务绑定和资源 ID,骨架是真实的):

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "main": ".open-next/worker.js",
  "name": "blog-2026",
  "compatibility_date": "2026-01-01",
  "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  // ISR 三件套之一:Durable Object 队列
  "durable_objects": {
    "bindings": [
      { "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"] }
  ],
  // ISR 三件套之二:R2 增量缓存
  "r2_buckets": [
    { "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "site-cache" }
  ],
  // ISR 三件套之三:D1 tag cache
  "d1_databases": [
    { "binding": "NEXT_TAG_CACHE_D1", "database_name": "tag-cache", "database_id": "……" }
  ],
  // 图片转码(后文 Satori 出图那一节会用到)
  "images": { "binding": "IMAGES" },
  "observability": { "enabled": true },
  "vars": {
    "NEXT_PUBLIC_SITE_URL": "https://meathill.com"
  }
}

下面进入正题。先讲环境变量——它依然是新人掉得最惨的坑,然后是这一年在生产环境攒出来的新坑。

❌ 反例教材:看起来很简单

{
  "name": "app-worker",
  "env": {
    "prod": {
      "vars": {
        "NEXT_PUBLIC_API_URL": "https://api.example.com", // ☠️ 以为配在这里就行了?天真!
        "DB_HOST": "postgres.prod.internal"
      },
      "secrets": ["API_SECRET_KEY"]
    }
  }
}

来看看这个典型的错误示范。很多人(比如我)的直觉是:“既然 Next.js 需要这些变量,又有 wrangler.jsonc 这个配置文件,那我就把它们全都配在这里,Wrangler 肯定会帮我处理好的。”

错!大错特错……

两个核心维度

要搞定环境变量,你必须先明确两个核心维度,如果不区分清楚,你的环境变量配置就只能“碰运气”。

构建时 (Build Time) vs 运行时 (Runtime)

构建时,环境变量通过构建环境(CI 或 Cloudflare 的构建设置)配置

  1. 编译打包代码
  2. 预渲染

运行时,环境变量由 vars + secrets 组成,可以本地配置,也可以线上配置

  1. Worker

服务端 (Server-side) vs 客户端 (Client-side)

服务端,可以访问所有变量

  1. Server component
  2. Server action
  3. API

客户端,只能访问构建时就存在的 NEXT_PUBLIC_*

  1. HTML + JS,通常是静态

💣 坑一:构建时 (Build Time) 的缺失

Next.js 的构建机制决定了 NEXT_PUBLIC_ 变量的行为:

  • 所有以 NEXT_PUBLIC_ 开头的变量,在执行 next build 时,会被直接替换成字符串常量并打包进客户端的 JS 文件里。
  • 也就是说,构建时环境变量必须预先定义在“构建 > 变量”里,或者在创建 Worker 时,在“高级设置 > 变量”里设置

当你运行 npm run build 时,此时 Cloudflare Worker 还没启动,Wrangler 的配置也没生效。Next.js 的编译器自然看不到 wrangler.jsonc 里的内容,也看不到 .dev.vars

后果

编译后的客户端代码里,process.env.NEXT_PUBLIC_API_URL 会被编译成 undefined。用户打开浏览器,请求发不出去,控制台一片红。

💣 坑二:构建完毕之后的预渲染

Next.js 自带多种渲染模式:

  1. CSR:客户端渲染,前后端分离后常见的模式
  2. SSR:服务器端渲染,在后端渲染完页面之后把 HTML 直接发给浏览器,有利于用户体验和 SEO
  3. ISR:渐增式渲染,页面渲染后,将 HTML 作为缓存保存起来,一段时间内不需要重复渲染
  4. Pre-rendering:预渲染,把静态页面和高频访问页面在构建时渲染好,运行时就只看缓存,性能最优

于是,构建完成之后,Next.js 会自动帮我们启动第四步,也就是预渲染首页、sitemap 和其他一些明确可以缓存的页面。这个时候,wrangler.jsonc 里的变量也没有生效。

后果

预渲染的执行环境在构建运行时,也要启动服务器,但是没有 wrangler.jsonc 定义的变量……😵‍💫😵‍💫……所以不是 NEXT_PUBLIC_ 的环境变量也要定义在构建时变量里。否则,可能连不上数据库、访问不到 API,导致预渲染失败,继而部署失败。

💣 坑三:运行时 (Runtime) 的混淆

Cloudflare Worker 和传统的 Node.js 服务器不同。

  • Traditional Node(如 Vercel / Docker)里,环境变量通常在启动进程前注入到 process.env
  • Cloudflare Worker 里,环境变量是作为对象绑定 (Bindings) 挂载在 env 参数上的。
  • 虽然 OpenNextnodejs_compat 帮我们做了很多兼容工作,试图把 env 映射回 process.env,但这仅限于服务端运行时

✅ 正确姿势:分而治之

要在 Cloudflare Worker + Next.js 体系下玩转环境变量,你需要把它们拆开处理。

1. 搞定构建时 (Build Time)

目标:确保 next build 能读到 NEXT_PUBLIC_ 变量。
做法

  • 本地开发:老老实实写 .env 文件(如果在 .dev.vars 里写了 NODEJS_ENV=development,那就要写 .env.development)。
  • CI/CD (GitHub Actions / Cloudflare Worker Build)必须在构建时环境变量设置里,把所有 NEXT_PUBLIC_ 变量填一遍。同时,预渲染所需的环境变量也要定义。
# 必须在构建环境存在的变量
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=xyz123

# 预渲染时所需的变量,比如首页需要读取数据库
DATABASE_URL=mysql:xxxx

2. 搞定运行时 (Runtime)

目标:让服务端代码 (API Routes, Server Components) 能读到 Vars 和 Secrets。
做法:使用 wrangler.jsonc(或 wrangler.toml)配置。

这就是 wrangler 发挥作用的地方。这里的变量只会在 Worker 跑在边缘节点上时存在,并且只有服务端代码能读到

{
  "env": {
    "prod": {
      // ✅ 这里的变量是给 Server 用的
      "vars": {
        // 其实这里甚至不需要配 NEXT_PUBLIC_...,除非你在 Server 端也用了它
        // 但为了统一管理,通常也会保留一份
        "NEXT_PUBLIC_API_URL": "https://api.example.com",
        "INTERNAL_CONFIG": "some-value"
      },
      // ✅ 敏感信息放 secrets,不要明文写在 vars 里
      "secrets": ["DATABASE_PASSWORD", "OPENAI_API_KEY"]
    }
  }
}

通常来说,不能暴露给前端的环境变量都会定义在这里。典型的就是数据库连接串——顺带一提,如果你的后端连的是 Postgres / Supabase 这类传统数据库,别让 Worker 每次请求都裸连数据库,中间套一层连接池是必须的,这块我单独写过一篇《Workers + Supabase + Hyperdrive 最佳实践》,讲连接串怎么配、Hyperdrive 怎么绑定。

3. 本地开发必备:.dev.vars.env(.development)

你可能会问:“wrangler.jsonc 里只能写明文的 vars,那 secrets 怎么办?总不能把 API Key 提交到 GitHub 吧?”

这时候就需要 .dev.vars 文件出场了。

  • 作用:专门用于本地开发 (npm run dev) 时的 Secrets 注入。
  • 格式:和 .env 一样,KEY=VALUE
  • 注意千万不要提交到 git!(记得加 .gitignore
# 本地开发用的 Secrets
OPENAI_API_KEY=sk-proj-123456
DATABASE_PASSWORD=secret-password

当你运行 wrangler devnext dev(通过 OpenNext 适配器)时,Wrangler 会自动读取这个文件,并把它挂载到 env 上。这样你就不用在 wrangler.jsonc 里写假的 secrets。线上的 secrets 则用 wrangler secret put KEY 上传,或在 Dashboard 里添加。

如果某些环境变量只有前端会用,那么就可以用 .env 来存放,跟其他框架的开发体验一致。

4. 极致体验:类型安全 (Type Safety)

在 TypeScript 项目里,最爽的莫过于输入 env. 之后,编辑器自动提示所有的变量名。Cloudflare 提供了官方支持来实现这一点。

步骤:运行命令生成类型定义(我把它挂在 pnpm cf-typegen 脚本上):

npx wrangler types --env-interface CloudflareEnv env.d.ts

这个命令会扫描你的 wrangler.jsonc.dev.vars,自动生成类型定义文件,里面定义了 CloudflareEnv 接口——vars、secrets、KV、R2、D1、DO 绑定全都有:

interface CloudflareEnv {
  KV_Company: KVNamespace;
  DB: D1Database;
  NEXT_PUBLIC_API_URL: string;
  OPENAI_API_KEY: string;
}

然后在代码里通过 getCloudflareContext() 使用这个类型:

import { getCloudflareContext } from '@opennextjs/cloudflare';

export async function GET(request: Request) {
  const { env } = getCloudflareContext();
  // 这里的 env 就是 CloudflareEnv 类型,有自动补全!
  console.log(env.NEXT_PUBLIC_API_URL);
}

注意一个细节:在静态生成 / 预渲染的上下文里(比如 generateStaticParams、构建期跑的 generateMetadata),要用异步形式 await getCloudflareContext({ async: true }),否则会直接抛错。我的经验是:不确定调用点会不会进静态化路径的,一律写 async 形式,省心。

5. 代码怎么写?

在 Next.js + OpenNext 环境下,你可以像平时一样写代码,但心里要有数:

// ✅ 场景 A:客户端组件 (Client Component)
// 这个值是在 Build Time 被“烧录”进来的。
// 如果构建时没给 env,这就是 undefined,不管你 wrangler 里配没配。
console.log(process.env.NEXT_PUBLIC_API_URL);

// ✅ 场景 B:服务端组件 (Server Component / API Route)
// 这个值是在 Runtime 动态获取的。
// OpenNext 会帮我们从 Worker 的 env 注入到 process.env
export async function GET() {
  // 这里能读到 wrangler secrets
  const apiKey = process.env.OPENAI_API_KEY;

  if (!apiKey) {
    throw new Error('Missing API Key! Check your wrangler secrets!');
  }

  return Response.json({ status: 'ok' });
}

// 但是,考虑到类型安全、通常我们还要用 bindings,更推荐 getCloudflareContext()
export async function GET() {
  const { env } = getCloudflareContext();
  const apiKey = env.OPENAI_API_KEY;
  await env.KV.get(key);
  return Response.json({ status: 'ok' });
}

6. 高级技巧:环境隔离与 Secrets 管理

除了以上几点,还有几个非常重要的“潜规则”,也是新手经常踩坑的地方。

🔒 规则一:Secrets 不会被覆盖 (Secrets Persistence)

很多同学担心部署时 wrangler.jsonc 里没有写 Secrets 的值(出于安全原因),会不会把线上已经配置好的 Secrets 给覆盖成空?
答案是:不会。wrangler deploy 只更新代码和 vars。Secrets 是存储在 Cloudflare 的加密保险箱里的,只要你不显式地去删除或更新它,它就一直都在。

🧬 规则二:Env 不会继承 (No Inheritance)

这是一个反直觉的设计:Environment 配置之间是互不继承的
如果你在 wrangler.jsonc 的最外层写了一堆通用配置,然后在 [env.production] 里只写了差异部分……
恭喜你,你的 Production 环境会丢失所有最外层的配置!

正确做法:在每个 Env(devstagingprod)里,老老实实把所有变量重新写一遍。虽然看起来冗余,但能保证配置的确定性,避免缺乏隐式继承带来的诡异 Bug。类似的还有各种 bindings,很难用,但是没办法。

🛡️ 规则三:如果没有指定环境,dev 环境不会默认生效

如果你在 wrangler.jsonc 配置了多个环境,比如 dev、staging、prod。但是启动开发环境的时候没有指定环境,那么 dev 环境不会默认生效。需要在 next.config.ts 里指定环境:

initOpenNextCloudflareForDev({
  environment: 'dev',
  configPath: './wrangler.jsonc',
});

💣 2026 追加:这一年在生产环境踩出来的新坑

环境变量只是入门关。这个博客上线之后,我又陆续踩了一批更隐蔽的坑,每一个都消耗过我半天以上的排查时间。下面按“杀伤力”排序。

坑四:ISR 不是配个 revalidate 就完事——R2 增量缓存三件套

在 Vercel 上,ISR 是“开箱即用”的:写个 revalidate,平台自动帮你存缓存、跑重新验证。到了 Workers 上,这套基础设施要你自己搭——OpenNext 只提供接口,存储和队列得用 Cloudflare 自家的资源拼起来:

  • R2 增量缓存NEXT_INC_CACHE_R2_BUCKET):ISR 渲染出来的页面 HTML / RSC payload 存这里。不配的话,revalidate 等于摆设,每次请求都全量渲染。
  • Durable Object 队列NEXT_CACHE_DO_QUEUE):负责 stale 页面的后台重新验证,保证同一页面不会被并发重复渲染。注意要在 migrations 里声明 DO 的 class。
  • D1 tag cacheNEXT_TAG_CACHE_D1):支撑 revalidateTag / revalidatePath。如果你只用时间驱动的 revalidate,可以不配;用了按需失效就必须有。

配置就是前文贴过的 open-next.config.ts + wrangler.jsonc,照抄就能用。另外强烈建议把 enableCacheInterception: true 打开:缓存命中时直接在 Worker 入口把响应发回去,不再进入 Next.js 的路由和渲染逻辑,省 CPU 时间也降时延。

这里还有一个必须知道的“预期现象”:每次 deploy 都会更换 build id,整个 ISR 缓存随之整体失效。部署之后的一小段时间里,所有页面都要重新回源渲染,你会看到一波源站请求 / 数据库负载尖峰。这不是 bug,但你要保证上游扛得住——我的做法是给上游 API(本站是 WordPress 的 wp-json)再加一层 Cloudflare 边缘缓存,让部署尖峰打在边缘缓存上而不是数据库上。

顺带说一句,R2 在这套架构里是个多面手:除了当 ISR 缓存,我还用它存放生成好的 OG 图和用户上传的文件。R2 没有出口流量费,跟 Workers 又是零配置互通,具体上传怎么做可以看《用 Cloudflare R2 上传文件实战》。

坑五:Workers 的 CPU 时间不是无限的——Satori 出图翻车记

Workers 按 CPU 时间计费和限额(墙钟时间里等待 I/O 不算),日常渲染页面绰绰有余。但一旦你干重活——比如用 next/og(底层是 Satori)动态生成社交分享图——就会立刻撞到天花板。我在做文章 OG 图的时候连踩三个坑:

  1. Satori 不支持 WebP。WordPress 的特色图片十有八九是 WebP,直接喂进去就报错。必须先转码成 PNG/JPEG。
  2. global_fetch_strictly_public 之下,Worker 不能回打同 zone 的 URL。所以想借 /cdn-cgi/image/ 转码是走不通的——那个子请求会被打回 Worker 自己,拿到的根本不是图片。正解是用 IMAGES binding 在 Worker 内部直接转码。
  3. ImageResponse 永远输出 PNG,没有参数能改。带照片的 1200×630 合成图 PNG 普遍在 1MB 上下,超过 WhatsApp 约 300KB 的红线,分享卡片直接降级甚至不显示。

最终方案:输入侧用 IMAGES binding 把封面图裁成 1200×630 的 PNG 再喂给 Satori(顺便省 base64 体积和解码内存),输出侧把 ImageResponse 的 PNG 再过一次 IMAGES 转成 JPEG:

// 输入侧:把 WebP 封面在 Worker 内转成 PNG,裁到 OG 画布尺寸
const cover = await env.IMAGES.input(upstream.body)
  .transform({ width: 1200, height: 630, fit: 'cover' })
  .output({ format: 'image/png' });

// 输出侧:把 ImageResponse 的 PNG 再转成 JPEG@82
const transformed = await env.IMAGES.input(pngStream).output({
  format: 'image/jpeg',
  quality: 82,
});
return new Response(transformed.image(), {
  headers: { 'Content-Type': 'image/jpeg' },
});
// 实测体积从 ~1MB 降到 150–280KB,稳过各家社交平台的限制

另外,Satori 渲染是纯 CPU 密集操作,千万不要每次请求都现渲。我的做法是渲染结果写进 R2,命中缓存就直接回图,一篇文章一辈子只渲染一次(发布钩子里主动刷新)。CPU 限额的具体数字随套餐不同,以官方定价页为准,但“重计算的结果必须缓存”这个原则在 Workers 上是铁律。

坑六:一个 set-cookie,毁掉整个边缘缓存

这个坑最隐蔽,杀伤力却最大。我用 next-intl 做双语,它默认会写一个 NEXT_LOCALE cookie 记住用户语言。看起来人畜无害?问题在于:只要响应里带了 set-cookie,这个响应就会被按 no-store 处理——边缘缓存直接失效,浏览器的 bfcache(后退/前进秒开)也被禁用。

结果就是:明明配好了 ISR 和边缘缓存,页面却每次都回源渲染,Core Web Vitals 也上不去。排查了半天,罪魁祸首竟然是一个我根本没用到的 cookie。

修复很简单——语言完全由 URL 驱动,把探测和 cookie 都关掉(这是本站 src/i18n/routing.ts 的真实配置):

// src/i18n/routing.ts
export const routing = defineRouting({
  locales: ['en', 'zh'],
  defaultLocale: 'zh',
  localePrefix: 'as-needed',

  // URL 即权威:不依赖 cookie / Accept-Language 自动改语言
  localeDetection: false,

  // 不写 NEXT_LOCALE cookie:set-cookie 会让响应变 no-store
  // —— 边缘缓存失效 + bfcache 被禁
  localeCookie: false,
});

推而广之:在 Workers + 边缘缓存的架构下,审计你的每一个 set-cookie。A/B 测试 SDK、统计脚本、i18n 库都可能悄悄写 cookie,把你的缓存命中率打成零。

坑七:export const runtime = 'edge' 会让路由直接消失

反直觉预警:在“边缘计算平台”上部署,反而不能用 Next.js 的 Edge Runtime。@opennextjs/cloudflare 只打包 Node runtime 的路由,凡是标了 export const runtime = 'edge' 的 route,根本不会进入构建产物——本地一切正常,线上直接裸 500。

我的 RSS feed 路由就是这么坏的:从 Vercel 迁过来时留着一行 runtime = 'edge'(在 Vercel 上这是优化),结果 feed 悄无声息地 500 了很久才被发现。结论:迁移时全局搜索 runtime = 'edge',全部删掉,让所有路由都走默认的 Node runtime——反正整个 Worker 本来就跑在边缘。

坑八:Workers 是纯 ESM 运行时,选依赖先看构建产物

最后一个坑关于依赖选型。Workers 运行时没有全局 require,如果某个包的 ESM 构建产物里内部还藏着 CommonJS 的 require() 调用,Worker 会在启动或首次调用时直接抛 ReferenceError: require is not defined。我在做 HTML 转 Markdown 时用 turndown 就翻了车——它的 .mjs 产物里用 require() 引 DOM 实现,本地 Node 下毫无异常,一上 Workers 就崩,最后换成纯 ESM 的 node-html-parser 才解决。

经验:给 Workers 项目挑依赖,别只看 npm 周下载量,先确认它在 ESM-only、无 DOM 的环境下能跑;拿不准的就 opennextjs-cloudflare build + preview 在本地 workerd 里实际验证一遍——这也是我把 pnpm preview 留在日常流程里的原因。

🏆 最佳实践总结

说了这么多,最后送大家一份 Cloudflare Workers 上部署 Next.js 的完全清单(2026 版):

  1. 选对适配器:新项目直接上 @opennextjs/cloudflare,不要再碰 next-on-pages 的老教程。
  2. Build Time 分离:凡是 NEXT_PUBLIC_ 开头的变量,在构建时环境变量里配置,构建时注入代码,有助于 Tree-shaking。
  3. Runtime 显式声明:服务端用的变量,全部写在 wrangler.jsoncvars 里。
  4. Build Time 预渲染:预渲染要用的变量,两边都写,写两遍。
  5. Secrets 隐式管理:敏感信息必须用 wrangler secret put 上传,或在 Worker 设置面板里添加;本地开发用 .dev.vars.env 在线上运行时不生效。
  6. 环境完全隔离:为 devprod 准备两套完全独立的资源 ID(KV、R2、D1),防止数据污染;所有 Env 的配置必须全量复制,不要依赖继承。
  7. 类型安全:用 wrangler types 生成 CloudflareEnv,代码里统一 getCloudflareContext()env;静态化路径记得 { async: true }
  8. ISR 三件套:R2 增量缓存 + DO 队列 + D1 tag cache 一次配齐,并打开 enableCacheInterception;心里记住“deploy 会清空 ISR 缓存”这个预期现象。
  9. 重计算必缓存:Satori 出图这类 CPU 密集任务,结果写 R2,绝不每次现算。
  10. 守住边缘缓存:审计所有 set-cookie;删光 runtime = 'edge';给上游 API 加边缘缓存兜住部署尖峰。
  11. 依赖选型看 ESM:引入新包之前,确认它在 ESM-only、无 DOM 的 Workers 环境能跑,用 preview 实测。

遵循这套最佳实践,你的 Cloudflare Workers 项目就能稳如磐石,既享受 Serverless 的低成本,又拥有企业级的稳定性。这套方案我已经在生产环境验证了大半年,上面每一条都有真实事故背书。

希望这篇文章能帮你少踩几个坑。如果你也在 Workers 上跑 Next.js,欢迎交流你踩到的新坑!

常见问题(FAQ)

Next.js 能直接部署到 Cloudflare Workers 吗?

需要通过 @opennextjs/cloudflare 适配器把 Next.js 构建产物转换为 Workers 可运行的格式,再用 wrangler 部署;App Router、ISR、Route Handlers 都受支持,本站(Next.js 16 + @opennextjs/cloudflare 1.19)就是生产实例。

OpenNext 和 next-on-pages 有什么区别?

next-on-pages 面向 Cloudflare Pages 且依赖 Edge Runtime,目前已退役;@opennextjs/cloudflare 面向 Workers,支持 Node.js 运行时与更完整的 Next.js 特性,是官方推荐路径。

部署后 ISR / 缓存不生效怎么办?

确认已配置 R2 增量缓存与对应绑定(配合 DO 队列、D1 tag cache),并检查 revalidate 设置;另外注意响应里不要带 set-cookie(会变 no-store),且每次 deploy 会更换 build id、ISR 缓存整体失效属预期现象。

延伸阅读

相关文章

觉得文章有帮助?

如果我的分享对你有所启发,欢迎通过赞助来支持我持续创作。

❤️ 赞助我

评论