内容较多的页面常常会遇到一个问题:页面中某个模块数据返回得很慢,用户却必须等到整份 HTML 都生成完成以后,才能看到最先应该出现的内容。
例如一个社区首页可能包含导航、帖子流、用户信息、推荐内容和广告模块。用户最想先看到的是帖子流,但只要右侧推荐或者广告接口慢了一点,传统整页渲染模式就可能把所有区域一起拖住。
最近在了解 BigPipe,它解决问题的角度很有意思:不是单纯压缩资源或者减少请求,而是让服务端生成页面和浏览器展示页面这两件事重叠发生。
传统页面为什么会等待
传统动态页面通常按照下面的顺序完成:
浏览器发起请求 -> PHP 获取所有模块数据 -> PHP 拼出完整 HTML -> 浏览器收到完整页面 -> 下载 CSS / JavaScript -> 展示内容
只要页面中有一个模块数据较慢,PHP 就无法完成最终 HTML,浏览器收到第一屏内容的时间也会跟着后移。
问题不一定在于总体耗时特别大,而在于服务端处理期间浏览器几乎没有可展示的内容。对用户来说,这段等待就是空白。
BigPipe 的 Pagelet 思路
Facebook 公开介绍的 BigPipe 方案,会先把页面拆成多个独立区域,称为 Pagelet:
页面 -> navigation pagelet -> content pagelet -> user pagelet -> recommend pagelet -> ad pagelet
这些模块拥有各自的占位容器、HTML 内容以及需要加载的 CSS / JavaScript 资源。
首次响应不等待所有模块都完成,而是先把页面骨架和 BigPipe 前端运行时代码发给浏览器:
<body> <div id="pagelet-navigation"></div> <div id="pagelet-content"></div> <div id="pagelet-user"></div> <div id="pagelet-ad"></div> <script src="/static/bigpipe.js"></script>
浏览器拿到骨架后,已经可以开始解析页面、加载基础样式并准备占位区域。服务端则继续生成各个 Pagelet;某一个模块准备完成,就将它立即输出给浏览器:
<script>BigPipe.onPageletArrive({ id: 'pagelet-content', html: '<div class="thread-list">...</div>', css: ['/static/thread-list.css'], js: ['/static/thread-list.js']});</script>
前端运行时收到数据以后,将内容放入相应容器中。这样帖子流可以先展示出来,其他较慢的模块随后再到达。
它改善的是感知速度
BigPipe 并不意味着所有数据请求都变快了。一个很慢的广告模块仍然可能很慢,一个复杂的推荐计算仍然需要时间。
它改变的是等待关系:
传统模式:模块 A 完成 + 模块 B 完成 + 模块 C 完成 -> 浏览器开始展示BigPipe:页面骨架先展示模块 A 完成 -> 展示 A模块 B 完成 -> 展示 B模块 C 完成 -> 展示 C
如果首屏核心模块较早完成,用户就能更早开始阅读或操作,不必等待页面中的所有附属内容。
这类优化特别适合模块较多、不同模块数据来源不同、首屏优先级明确的页面。
页面拆分不能只按照视觉区域
实现 Pagelet 时,很容易直接按照页面上看见的区块来拆:
左栏一个模块,中栏一个模块,右栏一个模块
但视觉边界不一定等于合理的加载边界。一个 Pagelet 是否适合独立输出,还要看:
- 它的数据请求是否能够独立完成。
- 它是否有明确的占位容器。
- 它依赖的样式和脚本是否容易管理。
- 它是否必须等待另一个模块先完成。
- 它是否属于用户最先需要看到的内容。
例如帖子列表适合优先展示;一个依赖用户资料和登录权限的操作栏,则可能需要等基础用户模块到达后再初始化。
CSS 与 JavaScript 的加载顺序
Pagelet 到达浏览器后,不能简单把 HTML 塞进页面就结束。如果对应 CSS 还没有加载,用户可能会先看到错乱布局,再看到样式突然变化。
因此更合理的顺序通常是:
Pagelet 到达 -> 加载并确认 CSS -> 写入模块 HTML -> 在合适阶段加载并执行 JavaScript
Facebook 对 BigPipe 的公开说明中还提到了 barrier 的概念:优先完成 Pagelet 的展示,再让 JavaScript 初始化阶段继续推进,避免脚本执行抢在主要内容展示之前。
对于实际页面来说,可以根据模块复杂程度调整这一策略,但有一个原则比较明确:用户首先需要稳定可读的内容,其次才是附加交互。
Facebook 与微博中的 BigPipe
BigPipe 最早广为人知,是因为 Facebook 在 2010 年公开介绍了这套方案。Facebook 将动态页面拆成 Pagelet,并说明其实现基于 PHP 和 JavaScript,不要求浏览器或 Web Server 提供特殊能力。
在国内的大型动态页面中,新浪微博也曾公开分享过新版页面中应用 BigPipe 的实践。微博首页与社区内容页有相似特点:页面模块多、内容更新快、主信息流的展示优先级高,因此渐进式输出比整页等待更适合改善用户看到首批内容的时间。
技术方案是否适合一个页面,仍然取决于模块依赖和业务结构,而不是因为某个大站采用过就直接照搬。但这些公开案例说明,大型内容产品在页面体验上会遇到相似的问题:如何让核心内容更早到达用户眼前。
一个简化的客户端接收器
前端可以维护一个轻量运行时,用来接收服务端到达的模块:
var BigPipe = { pagelets: {}, onPageletArrive: function (pagelet) { this.loadCss(pagelet.css, function () { var container = document.getElementById(pagelet.id); container.innerHTML = pagelet.html; BigPipe.loadJs(pagelet.js); }); }, loadCss: function (urls, callback) { // 简化示例:真实实现需要去重与加载完成统计 callback(); }, loadJs: function (urls) { // 简化示例:加载模块初始化脚本 }};
真正用于复杂页面时,还需要处理资源去重、模块依赖、错误降级、到达顺序以及模块之间的事件通信。
小结
BigPipe 的核心并不是一个复杂 API,而是一次渲染思路的改变:
- 将整页拆成可以独立生成与展示的 Pagelet。
- 先输出页面骨架,让浏览器尽快开始工作。
- 模块生成完成后立即流式到达客户端。
- 优先展示用户关心的内容,再补齐后续交互。
传统页面的目标通常是尽快完成整份响应;BigPipe 更关心用户何时看到第一批有价值的内容。对于模块繁多的动态页面,这个差异往往比单纯减少几个静态资源请求更加明显。