该用,但必须分层——模板解析结果可缓存,运行时数据合并过程不能缓存;真正应缓存的是template.parse()后的ast或编译函数,而template.execute()必须实时执行,否则导致hydration失效或dom mismatch。

流式HTML生成时,模板缓存该不该用?
该用,但必须分层——模板解析结果可缓存,运行时数据合并过程不能缓存。很多团队误把整个 render(template, data) 结果存进 Redis,导致动态内容(如用户昵称、实时时间)被固化,下次请求直接返回旧 HTML。
真正该缓存的是:template.Parse() 后的 AST 或编译后函数(如 EJS 的 compile() 返回值),这部分不依赖数据,复用率高;而每次请求的 template.Execute() 必须实时执行,否则 hydration 会失效或出现 DOM mismatch。
- Go 的
html/template默认不缓存,需手动调用template.New().ParseFS()后复用同一个*template.Template实例 - Node.js 的 EJS 支持
cache: true选项,但仅缓存编译结果,不是渲染结果 - 若用 Marko,
packages/runtime-class/src/runtime/html/AsyncStream.js内部已做模板函数级缓存,无需额外干预
AsyncStream 与普通模板渲染的缓存差异
普通模板引擎(如 Handlebars、Nunjucks)输出完整字符串,缓存粒度只能是「整页」或「组件片段」;而 AsyncStream 是流式生成器,支持按 chunk 缓存 —— 比如首屏骨架、导航栏、广告位可分别缓存,非关键区块(如评论列表)走实时流。
这带来两个关键约束:缓存键必须包含 stream 分段标识(如 "header:en"、"footer:zh"),且不能跨 stream 生命周期复用 buffer。一旦你用 res.write() 发送了第一个 chunk,后续 chunk 就不能再从缓存读取并拼接——因为 HTTP 流不可逆。
- 错误做法:
cache.get('page') + cache.get('comments')→ 可能破坏流式顺序或触发 double-write - 正确做法:每个 stream segment 独立缓存,由
AsyncStream自动控制 write 时机 - 注意
packages/compiler/src/taglib/loader/cache.js中的缓存 key 构建逻辑,它默认包含 locale 和 device type,但不包含用户 session ID —— 这是故意为之,避免缓存污染
部分 hydration 场景下,缓存必须排除哪些内容?
当只对页面中部分组件做 hydration(比如仅激活搜索框和登录按钮),缓存 HTML 时必须剔除所有含 data-hydrate 属性的节点,或确保这些属性值在缓存前已被剥离。否则客户端 JS 会尝试 hydrate 一个早已被服务端渲染并绑定过事件的 DOM,引发重复绑定或状态错乱。
典型表现是:点击按钮两次才触发、表单提交后清空失败、React/Vue 组件报 Hydration failed 错误。根本原因是缓存层把带 hydration 标记的 HTML 当作静态内容返回了,绕过了服务端的 hydration 预处理逻辑。
- 缓存前应过滤掉
data-hydrate、data-reactroot、data-v-app等框架特定属性 - 若使用 Marko,
packages/runtime-class/src/runtime/html/AsyncStream.js在流式输出时会自动跳过 hydration 相关标记,但自定义缓存中间件需手动处理 - CDN 缓存 HTML 时尤其危险:哪怕服务端没缓存,CDN 可能命中带 hydration 属性的旧响应,导致大面积 hydration 失败
Cache-Control 设置不当会直接废掉流式渲染
Cache-Control: public, max-age=3600 看似合理,但在流式场景下等于宣告放弃实时性。浏览器或 CDN 一旦缓存了首个 chunk(通常是 <div id="root">),后续流式 chunk 就无法更新已缓存的骨架部分,用户看到的永远是旧结构。
<p>正确策略是:骨架部分设 <code>Cache-Control: no-cache,动态区块(如用户头像、未读消息数)必须设 Cache-Control: private, max-age=0,并确保 CDN 对带 Cookie 或 Authorization 的请求不缓存。
- index.html 必须返回
Cache-Control: no-cache, must-revalidate,否则流式传输还没开始,用户就拿到过期骨架 - 若用
etag做条件缓存,注意流式响应无法生成稳定 etag —— 因为每个 chunk 的生成时间不同,建议禁用 - 检查
x-cache响应头:若显示 HIT 且首屏内容明显陈旧,说明缓存策略覆盖了流式通道











