当你访问 BlueSEO,你觉得速度怎么样?关于缓存,我发现了 Cloudflare OpenNext 的秘密
我发现了预渲染(ISR)这个工具,并将Cloudflare OpenNext 适配器对象缓存完整配置了一遍。动态落地页采用预渲染(ISR)首次访问会进行正常静态化,而难点就在首页和列表页,预渲染会出现空白页面,需要多次刷新才能被正常填充,这一定是哪里出了问题。最近我发现了一个新的利器 revalidatePath,测试结果令人满意,让我为你揭晓这一个答案。
打从研究 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 刷新当前路径,测试结果令人满意。
当前最佳实践

- 按照以上思路,设计缓存策略如下:
// 首页和列表
// 例如 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 }>;
}
- 针对构建时首页和列表页会直接空白的问题,设计刷新策略如下:
// src/app/(home)/page.tsx
export default async function HomePage() {
// 获取数据
data = await getCachedPosts() ?? [];
// 给出列表还是刷新按钮?
return {data && data.length > 0
? <PostsList data={data} />
: <PostsRefresh />
}
}
- 因为我确信首页和 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>
);
}
- 使用了 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,你觉得速度怎么样?请给我反馈。