不能直接用res.render()应对高并发,因其同步阻塞式渲染导致TTFB飙升、易触发502/504错误,并伴随Response already committed异常与慢日志;需改用预编译缓存+流式分片输出。

为什么不能直接用 res.render() 应对高并发页面请求
因为 res.render() 是同步阻塞式模板渲染,每次调用都要等数据查完、模板编译完、整个 HTML 字符串生成完才开始写响应。在并发量超过 500 QPS 时,数据库查询 + 模板执行很容易拖慢 TTFB(Time to First Byte)到 800ms 以上,触发 Nginx 的 proxy_read_timeout 或 Tomcat 的 connectionTimeout,直接返回 502 或 504。
常见错误现象包括:Response already committed 异常、浏览器卡在 loading 状态、监控里看到大量 render duration > 2s 的慢日志。
- 模板里嵌了
<#include>或<@macro>,导致上下文反复加载和校验 - 传给模板的数据对象包含未 resolve 的 Promise,引擎卡在等待上
- 没关掉模板引擎的 debug 模式(如 Freemarker 的
template_update_delay设为 0 会频繁扫描文件)
静态化 + 定时预生成:什么时候该用 Freemarker 而不是 EJS
Freemarker 更适合做离线预生成,因为它编译后是纯 Java 方法,无运行时 JS 引擎开销,且支持 Template.process() 直接写入 FileWriter;EJS 默认依赖 Node.js 的 vm 模块执行,每次 renderFile() 都有沙箱创建成本,不适合高频批量生成。
使用场景明确:首页、活动页、商品详情页这类内容更新不频繁(>5 分钟)、但访问量极大(单页日均 PV > 100 万)的页面,必须走预生成。
立即学习“前端免费学习笔记(深入)”;
- 用 Quartz 或 XXL-JOB 触发定时任务,调用
Configuration.getTemplate()+template.process(data, writer)写出index.html - 生成路径要带版本号或时间戳,例如
/static/v20260630/index.html,避免 CDN 缓存击穿 - 不要把生成逻辑塞进 Web 请求链路——哪怕加了缓存,一旦缓存失效,所有并发请求都会撞上生成锁
流式响应真能扛住高并发?三个硬性动作缺一不可
所谓“流式”,不是在 res.write() 后加个 res.flush() 就完事。真正可用的流式响应,必须同时满足:首屏 HTML 在 100ms 内发出、后续区块按数据就绪顺序追加、连接断开时不卡线程。否则只是把阻塞从“全量等”变成“分段等”,反而更难排查。
- 禁用容器缓冲:Tomcat 下必须调
response.setBufferSize(8192)并在首段输出后立刻response.flushBuffer() - HTML 结构得能切:用
<header>、<main>、<footer>隔开,禁止 footer 里引用 main 中的totalCount这类跨区块变量 - 数据获取必须异步:用
CompletableFuture.allOf()(Java)或Promise.allSettled()(Node)并行拉取多个接口,哪个先返回就先写对应<section>
DocumentFragment 不是万能解药:DOM 批量更新的边界在哪
在服务端生成 HTML 字符串的场景下,DocumentFragment 根本不适用——它只存在于浏览器端 DOM 操作中。但如果你在 Node 侧用 JSDOM 做 SSR 后处理(比如插入 SEO meta、动态加 class),那就要小心:JSDOM 的 document.createDocumentFragment() 一样有内存开销,且每次 appendChild() 都触发 layout 计算模拟,比直接拼字符串慢 3~5 倍。
真正该警惕的是:别把服务端模板渲染和客户端 DOM 更新混为一谈。前者目标是快速吐出完整字符串,后者才要考虑 fragment、复用池、虚拟 diff。
容易被忽略的一点:用 innerHTML = 替换节点时,老节点上的事件监听器会被丢弃。如果模板里写了 onclick="track('banner-click')" 这种内联 handler,预生成 HTML 里它还在;但流式响应中用 JS 动态插入,这些 handler 就失效了——得靠事件委托兜底。



















