Brick Next架构设计与前端性能优化实践
又踩坑了,Brick Next 的 SSR 数据脱水失败
今天上线前最后测一把,发现页面首屏数据全空,Loading 转了半天才出来内容。本地开发环境好好的,一上预发就抽风。排查半小时,定位到是 Brick Next 在 SSR 模式下,客户端 hydration 时拿不到服务端传下来的初始状态,也就是所谓的“脱水失败”。
说白了就是:服务端渲染的时候数据是有的,HTML 返回了正确的内容,但前端接管的时候 Redux 或状态管理里没接上,于是重新发起请求,导致闪屏 + 多余请求。
这里我踩了个坑:以为 Brick Next 的数据脱水是开箱即用的,结果文档里提了一嘴要手动配置 window.__INITIAL_STATE__,但我一开始图省事,直接用了社区的一个 starter 项目,里面 state 注入方式是错的 —— 它用的是 JSON.stringify 后拼字符串写进 script 标签,但没有做 XSS 转义,导致某些特殊字符(比如换行、双引号)破坏了 JS 语法,页面直接报错,__INITIAL_STATE__ 变成 undefined。
折腾了半天才发现问题出在模板注入
先说我怎么排查的:
- 打开浏览器,看源码,搜
__INITIAL_STATE__,找到了那一行 JS 注入代码 - 复制那串 JSON 出来跑
JSON.parse,报错:Uncaught SyntaxError: Unexpected token - 一看,有个字段值是:
"desc": "用户说:"这体验不行啊""—— 这个反斜杠没被正确转义,直接拼进去就成了语法错误
所以问题本质不是 Brick Next 不支持,而是我们自己把数据塞进 HTML 的方式太粗糙了。
后来试了下几种方案:
- 方案一:自己写 escape 函数,替换掉
"、、<、>等字符 —— 麻烦,容易漏 - 方案二:用
serialize-javascript这个库,专门干这事的 - 方案三:改用
<script type="application/json" id="state">放纯 JSON,然后 JS 里读取并解析
我一开始想用方案三,看起来更干净。但问题是,application/json 里的内容不能包含未转义的 HTML 实体,而且还要处理 CSP 安全策略,有些公司不允许内联 script 执行 eval 类操作。虽然不 eval,但 document.getElementById('state').textContent 也算“动态执行”,可能被拦。
最后还是选了方案二,引入 serialize-javascript,简单粗暴,亲测有效。
核心代码就这几行
服务端渲染模板注入部分(Node.js / Koa 示例):
import serialize from 'serialize-javascript';
// 假设你已经拿到了数据
const initialState = {
user: { id: 1, name: '张三' },
posts: [],
desc: '用户说:"这体验不行啊"'
};
// 错误写法 ❌
// const badScript = <script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script>;
// 正确写法 ✅
const safeScript = <script>window.__INITIAL_STATE__ = ${serialize(initialState)};</script>;
// 最终返回的 HTML 字符串中插入
res.send(
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root">${renderedApp}</div>
${safeScript}
</body>
</html>
);
注意:serialize-javascript 会自动帮你处理:
- 引号转义
- 避免
被截断 - 防止
__proto__等原型污染 - 可选是否加 IIFE 包裹(这里不需要)
然后在客户端入口文件里,记得读这个变量:
// client-entry.js
const preloadedState = window.__INITIAL_STATE__;
// 如果你在用 Redux
const store = createStore(rootReducer, preloadedState, middleware);
// 清理全局变量,防污染
delete window.__INITIAL_STATE__;
这样 hydration 就能正常接上了,首屏不会闪烁,API 也不会重复请求。
这里注意我踩过好几次坑
第一个坑:别忘了删 window.__INITIAL_STATE__。虽然不影响功能,但留着是个安全隐患,别人可以在控制台看到完整初始状态,尤其如果里面有敏感信息(比如未脱敏的用户数据),就不合适了。
第二个坑:如果你用了模块热更新(HMR),开发环境下也可能读到上一次残留的 __INITIAL_STATE__。建议在开发环境也走 mock 数据 + 动态加载,而不是每次刷新都依赖服务端注入。或者干脆只在 SSR 模式下启用该逻辑。
第三个坑:Redux 的 reducer 结构必须和服务端生成的一致。我之前本地测试改了 reducer 名字叫 postList,服务端还是 posts,导致数据没挂载上去,debugger 跟了半天才发现是 key 对不上。
为什么不用 react-helmet-async 或其他状态容器?
有人可能会说,用 Context + 自定义 Provider 不就好了?理论上可以,但 Brick Next 默认还是推荐集中式的初始状态注入,尤其是涉及多页面、多路由、SEO 场景时,分散在各个 context 里反而难管理。
而且 Brick Next 内部对 window.__INITIAL_STATE__ 有做类型校验和 merge 策略,你要是自己搞一套 hydration 机制,后期升级容易出兼容问题。
所以结论是:老老实实用官方模式,只是别再手写 JSON.stringify 往里塞了。
改完后还有一两个小问题,但无大碍
现在首屏数据稳定了,但发现 Chrome DevTools 里偶尔提示 Failed to load resource: net::ERR_FAILED,查了是某个字体资源 404,跟当前问题无关。估计是 build 时 asset publicPath 没配对,但这不属于本文范畴,先放一放。
另外,在低版本 Android 浏览器上,serialize-javascript 输出的 ES6 字符(如 Unicode 字符)可能会解析失败。解决方案是在 babel 编译时确保最终产出是 ES5,或者使用 isJSONSafe: true 选项:
serialize(initialState, { isJSONSafe: true })
这个选项会强制把所有非 ASCII 字符转成 uXXXX 形式,兼容性更好。
以上是我踩坑后的总结
SSR 数据脱水这事,看着简单,真出问题很难快速定位。尤其是当你依赖第三方 starter 或 boilerplate 时,很容易被带沟里。记住一句话:只要往 HTML 里插 JS 数据,就必须用安全序列化工具,别信自己的正则。
Brick Next 其实没错,错的是我们自己偷懒没看全文档。官方文档里其实写了推荐使用 serialize-javascript,但我当时跳着看,只看了“怎么定义 __INITIAL_STATE__”,没看“如何安全注入”这一节,血的教训。
以上是我个人对 Brick Next SSR 状态注入的完整踩坑记录,有更优的实现方式欢迎评论区交流。比如你有没有试过用 structuredClone + Blob URL 的方式?我还没敢在线上用,怕兼容性翻车。

暂无评论