当你访问 BlueSEO,你觉得速度怎么样?关于缓存,我发现了 Cloudflare OpenNext 的秘密

我发现了预渲染(ISR)这个工具,并将Cloudflare OpenNext 适配器对象缓存完整配置了一遍。动态落地页采用预渲染(ISR)首次访问会进行正常静态化,而难点就在首页和列表页,预渲染会出现空白页面,需要多次刷新才能被正常填充,这一定是哪里出了问题。最近我发现了一个新的利器 revalidatePath,测试结果令人满意,让我为你揭晓这一个答案。

December 15, 2025

打从研究 Cloudflare OpenNext 开始,线上测试的重点就是缓存策略,我相信作为先进的全栈开发框架,Next.js 的缓存策略必定是高效率的。但在 Cloudflare OpenNext 适配时,我失败了。在这个过程中,我与各种 AI 大模型进行了深入的交流,包括 Microsoft copilot、Goolge Gemini 、Anthropic Claude、DeepSeek、GPT-4等,可能是上下文没弄好吧,结果令人失望,耗费时间不少于 2 周。最近,我发现了问题所在,你感受一下,当访问 BlueSEO,你觉得速度怎么样?让我为你揭晓这一个答案。

初始折衷方案

最初尝试的过程中,我发现了预渲染(ISR)这个工具,并将Cloudflare OpenNext 适配器缓存部分完整配置了一遍。动态落地页采用预渲染(ISR)首次访问会进行正常静态化,而难点就在首页和列表页,预渲染会出现空白页面,需要多次刷新才能被正常填充,因此折衷采用方案如下:

// 首页和列表
// 例如 src/app/blog/page.tsx
export const dynamic = "force-dynamic";

// 动态落地页
// 例如 src/app/blog/[slug]/page.tsx
export const revalidate = 600;

确实全部都正常了,部署成功后,首页、列表和落地页全部都符合预期,只是首页和列表属于动态加载,比较慢。一直都几个月后,我默认以上配置是最佳实践了,如果再提速,应该跟 CPU 有关了。当我的项目逐渐丰富后,项目文件多了,经常触发 Cloudflare Workers 免费账户的 CPU 10 毫秒限制。开始反问自己,BlueSEO 网站只是初始测试版本,访问量低、数据流程简单,这一定是哪里出了问题。

过程看到希望

一个偶然的机会,AI 告诉我一个情况,你不用导出预计算静态参数 generateStaticParams,因为 Cloudflare Workers 构建阶段不能连接外部资源,比如 D1 数据库、R2 对象等,如果你导出,反而会生成一个空白页面。这让我联想到了最初观察到的现象。经过多次构建观察,进一步确认了 generateStaticParams 确实是无意义的,不知道 Vercel 那边部署是怎么回事,我暂时没有那边的项目。

因此我删除了所有预计算静态参数,测试发现之前出现的问题,比如 src/app/[slug]/page.tsx 初次访问偶尔可以、偶尔不行,这些小毛病也消失了,我确信发现了光明。现在问题给到首页和列表了,如果导出 revalidate 必定会构建出空白页面,当然如果不断刷新至多到 revalidate 过期时 ,页面会被填充。这种体验是不能接受的。

我发现了一个新的利器 revalidatePath,设想如下:当构建出现一个空白页面时,给出一个专用加载骨架,在骨架的上方设置一个刷新按钮,点击按钮调用 Actions revalidatePath 刷新当前路径,测试结果令人满意。

当前最佳实践

posts-refresh.png

  1. 按照以上思路,设计缓存策略如下:
// 首页和列表
// 例如 src/app/blog/page.tsx
// 移除 export const dynamic = "force-dynamic";
// 预渲染(ISR)再生
export const revalidate = 600;

// 动态落地页
// 例如 src/app/blog/[slug]/page.tsx
// 预渲染(ISR)保持不变
export const revalidate = 600;

// 导出预计算静态参数
export async function generateStaticParams() {
  return [] as Array<{ slug: string }>;
}
  1. 针对构建时首页和列表页会直接空白的问题,设计刷新策略如下:
// src/app/(home)/page.tsx

export default async function HomePage() {
  // 获取数据
  data = await getCachedPosts() ?? [];
  // 给出列表还是刷新按钮?
  return {data && data.length > 0
    ? <PostsList data={data} />
    : <PostsRefresh />
  }
}
  1. 因为我确信首页和 Blog 列表是有内容的,所以我不给出没有内容的提示,而是刷新按钮+骨架:
// PostsRefresh 刷新组件

"use client";
import { useTransition } from "react";
import { useRouter, usePathname } from "next/navigation";
import { PathCacheClear } from "@/actions/cache-actions";
import { Button } from "@/components/ui/button";

// 刷新容器(骨架省略)
export function PostsRefresh() {
  return <RefreshButton />;
}

// 刷新按钮
export function RefreshButton() {
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();
  const handleRefresh = () => {
    startTransition(async () => {
      await PathCacheClear(pathname);
      router.refresh();
    });
  };
  return (
    <Button
      size="sm"
      variant="outline"
      className="cursor-pointer"
      disabled={isPending}
      onClick={handleRefresh}
    > Refresh </Button>
  );
} 
  1. 使用了 PathCacheClear Actions ,直接调用服务器刷新当前页面:
// 更新缓存

"use server";
import { revalidatePath } from "next/cache";

// 更新路径缓存
export async function PathCacheClear(
  path: string,
) {
  try {
    revalidatePath(path);
  } catch (error) {
    console.error(`Failed ${path}:`, error);
  }
}

经过上述改造后,当构建完成后的首页和列表页面,第一个访问者通过路径缓存刷新,立即看到最新的列表类信息,首页、列表页、落地页全部实现静态缓存,大大降低了服务器开销,完美满足了用户体验。特别惊喜的是, Cloudflare Workers 免费账户的 CPU 10 毫秒限制再也没有出现了,免费账户的额度完全可以继续跑起来。

那么最后我再问一下,当访问 BlueSEO,你觉得速度怎么样?请给我反馈。