Skip to content
Go back

使用 BigPipe 让页面内容逐块呈现

内容较多的页面常常会遇到一个问题:页面中某个模块数据返回得很慢,用户却必须等到整份 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,而是一次渲染思路的改变:

传统页面的目标通常是尽快完成整份响应;BigPipe 更关心用户何时看到第一批有价值的内容。对于模块繁多的动态页面,这个差异往往比单纯减少几个静态资源请求更加明显。

参考资料


Share this post on: