边缘渲染的 CDN Worker 实践:在离用户最近的地方执行 SSR

发布时间:2026/7/25 4:10:23
边缘渲染的 CDN Worker 实践:在离用户最近的地方执行 SSR 边缘渲染的 CDN Worker 实践在离用户最近的地方执行 SSR一、SSR 服务器在北京深圳用户打开页面要 1.2 秒——一半时间花在网络传输上边缘渲染不是新概念Cloudflare Workers 和 Vercel Edge Functions 已经推了好几年。但实际落地中把 SSR 从中心化服务器搬到 CDN 边缘节点面临三个问题运行时差异Node.js vs WinterCG/Web API、框架限制React 18 RSC 的边缘支持还不太成熟、安全风险用户的认证 token 在 CDN Worker 里处理。边缘渲染适合的页面类型首屏加载速度是核心 KPI如电商详情页、内容相对静态但有个性化组件如用户名和购物车角标、用户分布全球。不适合的大量数据库查询的页面边缘节点没有直连数据库的能力、强状态依赖的页面如实时协作编辑器。核心思路是边缘 SSR 动态部分的水合页面的静态部分在边缘渲染Html Shell、CSS、静态文案动态部分用户信息、个性化推荐用占位符渲染在客户端通过懒加载水合。二、底层机制与原理剖析边缘渲染的三个层次全页缓存如果页面完全不需要个性化在边缘 Worker 中缓存完整的渲染结果。缓存 key 基于 URL 设备类型Desktop/Mobile。缓存时间可以按页面特性设置首页 60 秒、文章页 1 小时。模板缓存 动态拼接页面的 HTML 框架在边缘缓存动态部分用户信息、购物车数量通过 API 调用获取后在 Worker 中用 HTMLRewriter 或字符串替换拼入。完全 SSR对于需要完整 SSR 的页面Worker 执行完整的渲染逻辑。但受限于 Worker 的运行时间限制Cloudflare Workers 最多 30 秒 CPU 时间和内存限制128MB需要精简渲染逻辑。三、生产级代码实现// edge-worker.js /** * CDN Worker 边缘 SSR 实现 * * 运行在 Cloudflare Workers / Vercel Edge 环境 * 设计要点 * 1. 不使用 Node.js APIfs, process, path只用 Web API * 2. 流式渲染——先返回头部 HTML动态内容后追加 * 3. 缓存分层——模板缓存长 数据缓存短 */ // --------------------------------------------------------------------------- // HTML 模板缓存存储在边缘 KV 中 // --------------------------------------------------------------------------- const HTML_TEMPLATES { home: !DOCTYPE html html langzh-CN head meta charsetUTF-8 title{{title}}/title link relstylesheet href/static/main.css !-- 预连接关键域名 -- link relpreconnect hrefhttps://api.example.com /head body header nav a href/首页/a div iduser-badge{{userSkeleton}}/div /nav /header main{{content}}/main script defer src/static/hydrate.js/script /body /html, }; // --------------------------------------------------------------------------- // 请求处理器 // --------------------------------------------------------------------------- async function handleRequest(request, env, ctx) { const url new URL(request.url); const path url.pathname; // 静态资源——直接走 CDN 缓存 if (path.startsWith(/static/) || path.startsWith(/_next/static/)) { return fetch(request); // 让 CDN 原生缓存处理 } // API 请求——代理到源站 if (path.startsWith(/api/)) { return proxyToOrigin(request); } // 页面请求——边缘 SSR return handlePage(request, env, ctx, path); } /** * 页面 SSR 处理 */ async function handlePage(request, env, ctx, path) { // 1. 尝试从 KV 读取缓存的完整 HTML const cacheKey page:${path}:${getDeviceType(request)}; const cached await env.PAGE_CACHE.get(cacheKey); if (cached !shouldBypassCache(request)) { // 带上缓存的 age header return new Response(cached, { headers: { Content-Type: text/html; charsetutf-8, X-Cache: HIT, }, }); } // 2. 并行获取所需数据 const [userData, pageData] await Promise.all([ fetchUserData(request, env), fetchPageData(path, env), ]); // 3. 渲染 HTML const html renderTemplate(home, { title: pageData.title || 首页, userSkeleton: renderUserSection(userData), content: renderContent(pageData), }); // 4. 缓存渲染结果 // 非登录用户缓存 60 秒登录用户不缓存个性化内容 if (!userData.isLoggedIn) { ctx.waitUntil( env.PAGE_CACHE.put(cacheKey, html, { expirationTtl: 60 }) ); } return new Response(html, { headers: { Content-Type: text/html; charsetutf-8, X-Cache: MISS, // 浏览器缓存策略 Cache-Control: userData.isLoggedIn ? private, no-cache : public, s-maxage60, stale-while-revalidate300, }, }); } // --------------------------------------------------------------------------- // 数据获取边缘环境——Web API only // --------------------------------------------------------------------------- async function fetchUserData(request, env) { const cookie request.headers.get(Cookie) || ; const sessionToken extractCookie(cookie, session); if (!sessionToken) { return { isLoggedIn: false }; } try { const resp await fetch(${env.API_ORIGIN}/api/user/me, { headers: { Authorization: Bearer ${sessionToken}, // 传递客户端 IP用于风控 X-Forwarded-For: request.headers.get(CF-Connecting-IP) || , }, // 设置短超时——用户数据 API 必须快 signal: AbortSignal.timeout(2000), }); if (resp.ok) { const user await resp.json(); return { isLoggedIn: true, ...user }; } } catch (e) { // 用户 API 不可用——降级为匿名用户 console.error(User API failed:, e.message); } return { isLoggedIn: false }; } async function fetchPageData(path, env) { try { const resp await fetch(${env.API_ORIGIN}/api/page${path}, { signal: AbortSignal.timeout(3000), }); return resp.ok ? await resp.json() : { title: 页面加载中... }; } catch { return { title: 页面暂时不可用, error: true }; } } // --------------------------------------------------------------------------- // 模板渲染简单的字符串替换 // --------------------------------------------------------------------------- function renderTemplate(templateName, data) { let template HTML_TEMPLATES[templateName]; if (!template) return ; for (const [key, value] of Object.entries(data)) { template template.replace(new RegExp(\\{\\{${key}\\}\\}, g), value || ); } return template; } function renderUserSection(userData) { if (!userData.isLoggedIn) { return a href/login classlogin-btn登录/a; } return div classuser-info>