模板缓存与分块输出无关,缓存的是同步编译函数,分块需手动调用res.write()逐段写入;必须禁用res.send()、改用res.write()+res.end()并设置setNoDelay(true),且每块数据边界、缓存键上下文、运行时变量须严格隔离。

服务端模板缓存本身不负责分块输出,分块输出是响应流控制行为,和缓存无关;但二者必须协同——缓存的是编译函数,分块输出靠的是实时执行该函数并逐段写入 res。
模板缓存和分块输出不是一回事
很多人误以为“缓存模板”就能自动支持流式渲染。实际上:template.compile() 返回的是一个同步函数,它本身不产生流;res.write() 是否分块、何时 flush,完全由你调用时机决定,和模板是否被缓存毫无关系。
- 缓存对象是
function(data) { return htmlString },它只做「输入数据 → 输出字符串」这一步 - 分块输出需要你手动把大页面拆成多个逻辑段(如
<header>、<main>、<footer>),每段独立获取数据、调用缓存的模板函数、再res.write() - Express 默认禁用流式写入,
res.send()会锁死整个响应体;必须改用res.write()+res.end(),且提前设置res.socket.setNoDelay(true)
为什么不能等全部数据就绪再 res.end()
首屏渲染延迟(TTFB + 首字节到达时间)直接取决于你第一次调用 res.write() 的时机。如果整个页面依赖 5 个 API 调用,而你坚持等全部返回再开始写响应,用户就要干等最慢的那个接口。
- 典型错误:在
async function handler(req, res)里用await Promise.all([...])拿全量数据,再一次性res.send(html) - 正确做法:先
res.write('<header>...</header>'),然后并发 fetch 主体数据,返回即写<main>,最后写<footer> - 注意
res.flushHeaders()不是必需的,但若 header 很大或含动态Set-Cookie,可显式触发发送
res.write() 分块时容易踩的坑
HTTP 响应流不可逆,一旦 res.write() 发出,就不能回退、不能重写、不能插入中间内容。很多问题源于对流生命周期的理解偏差。
立即学习“前端免费学习笔记(深入)”;
- 不要跨请求复用同一个
AsyncStream实例——每个请求必须新建,否则 buffer 混淆、chunk 错序 - 缓存键若含
streamSegment: 'header',就不能在后续res.write()中再读取这个 key 的值拼接——流已发出,缓存只能用于下一次请求 - 若模板片段含
<script>window.NONCE = 'xxx'</script>,这个NONCE必须在每次请求实时生成并注入,绝不能进缓存体;否则所有用户拿到同一个值,CSRF 防御失效 - 使用
res.socket.destroy()或超时中断时,要确保没正在res.write(),否则抛write after end错误
真正难的不是怎么分块,而是每一块的数据边界是否清晰、缓存键是否带齐上下文、运行时变量是否彻底剥离——这些地方一错,缓存就变成定时炸弹,而不是加速器。



















