从刚开始Razor/Jade 后端渲染,到angular/react/vue等一大票SPA框架。 于是冒出一大堆网站的主页面都是一个空div外加一个大bundle.js,清新简洁落落大方,沾沾自喜。
直到有人吐槽SEO性能太差,于是各大SPA又加上了server-side rendering【SSR】属性。 大伙玩的不亦乐乎, 新手上路分分钟甩出一个web app。 各大互联网公司也热爱SPA, 比如instagram和WhatsApp就喜欢搞个纯纯的react.js用作首页。 注意,这里有一个大大的But: 优酷和新浪微博这种重型Web却扔在用一种“古老”的技术: BigPipe。
啥是BigPipe? 有没有你的Big bundle.js牛逼? 本文对此进行简单分析。官方定义
- BigPipe是一个重新设计的劢态网页服务体系。
- 将页面分解成一个个Pagelet,然后通过Web服务器和浏览器之间建立管道,进行分段输出(减少请求数)。
- BigPipe不需要改变现有的网络浏览器或服务器。
何为分段传输? 参考聊一聊网页的分段传输与渲染那些事儿
http1.1中引入了一个http首部,Transfer-Encoding:chunked。这个首部标识了实体采用chunked编码传输,chunked编码可以将实体分块儿进行传输,并且chunked编码的每一块内容都会自标识长度。这给了web开发者一个启示,如果需要多个数据,而多个数据均返回较慢的话。可以处理完一块就返回一块,让浏览器尽早的接收到html,可以先行渲染。
特点:
- 后端程序无需等到页面所有 Pagelet 的API都读取执行完,才输出到浏览器,服务器端不浏览器端并行处理,加快了页面显示。
- Pagelet的渲染和输出顺序可以由后端程序控制,及早输出用户关心的模块。
- 模块化渲染,互不影响
为什么使用BigPipe
- 解决速度瓶颈
- 降低延迟时间
源码分析
通读源码并加以分析,我将一些关键的要点进行了提炼和简单的梳理。
下面直接上干货。
抓取新浪微博截取最新的(2019,03,02)新浪微博代码,
通过截图看得出来,初始状态下body内部的div构成非常简单。动态内容主要依靠FM.view贯穿全局渲染。我们重点查看FM模块。
虽然源码被混淆,不过加点注释还是可以读出其中的精髓的。
Pagelet加载流程
- 用JavaScript异步加载css文件
- 当css文件下载完成, 将html插入入页面空DIV
- 启劢JavaScript,绑定事件等
我们把微博分组模块作为范例(pl.nav.group.index),来分析单个pagelet是如何加载的。
FM.view({
"ns":"pl.nav.group.index", // pagelet/component reference name
"domid":"v6_pl_leftnav_group", // DOM ID
"css": ["style/css/module/global/WB_left_nav.css?version=716feb1e4288c3e0"], // Style dependency, css
"js":["home/js/pl/nav/group/index.js?version=f35f25b485d9c6db"], // Script dependency, js
"html": "<div class=\"WB_left_nav WB_left_nav_Atest\" node-type=\"groupList\" fixed-item=\"true\">\n .... <\/div>"}) // body html
});
复制代码
一目了然,FM.view函数就是用来动态载入模块的,内部包括了所有的所有的js,css文件,同时还把HTML的markup引入。 我们依次讲解如何加载各个依赖。
JavaScript依赖
if (!Y(a, d)) {
var k = bd("script"), // bd = document.createElement
l = !1,
m, n;
bh(k, "src", a); // bh = set html attribute and value on element k
bh(k, "charset", "UTF-8");
m = k.onerror = k.onload = k.onreadystatechange = function() {
if (!l && (!k.readyState || /loaded|complete/.test(k.readyState))) {
l = !0;
j(n);
k.onerror = k.onload = k.onreadystatechange = null;
g.removeChild(k);
Z(a)
}
};
n = h(m, 3e4); // h = settimeout, 延迟并绑定onerror和onload函数
g.insertBefore(k, g.firstChild) // insert into
}
复制代码
一个FM.view的返回内容为JavaScript代码,这段代码自动调用pagelet中的内容。
CSS依赖(异步加载,兼容IE)
functionbl(a) {
var b, c;
if (m) { // 老版本ie检测以及处理,懒得分析for (b in bj)
if (bj[b].length < 31) {
c = p(b);
break
}
if (!c) {
b = x();
c = bd("style");
bh(c, "type", "text/css");
bh(c, "id", b);
g.appendChild(c);
bj[b] = []
}(c.styleSheet || c.sheet).addImport(a);
bj[b].push(a)
} else { // 现代浏览器部分
b = x(); //生成script tagvar d = bd("link"); // 设置script元素属性
bh(d, "rel", "stylesheet");
bh(d, "type", "text/css");
bh(d, "href", a);
bh(d, "id", b);
g.appendChild(d) //插入 document.head尾部
}
bi[a] = b // 对已处理的script文件进行标注管理
}
复制代码
HTML元素
functions() {
functionb(b) {
r();
b || a[P] || bH()
}
d ? bv(function() {
bF(d, function(f) {
if (!a[P] && f && e != c) {
bG(f, d);
f.innerHTML = e || ""; // html injection
br(b);
delete a.html
} else b(!0)
})
}) : b(!0)
}
复制代码
状态管理
FM作为前端加载器,控制和执行各个模块的加载以及状态的改变。
由于页面中同时存在多个pagelet, 所以FM要设立一个变量存储各个view的状态。当每个view状态ready或者 重新载入的时候,记录各个view的最新状态。
每个pagelet有一个ID, 这个string是状态管理器中的primary key。
下图是加载一个view的状态变化。
var view_id = view_domid || view_componentRef;
// .....functionloadView() {
if (!a[PL_ABORT]) {
if (view_componentRef) {
fmClear(view_componentRef, view_domid);
fmStart(view_componentRef, view_domid, a)
}
assertViewComplete(view_id, a)
}
updateEvent(PL_JSREADY, a)
}
复制代码
适用场合
当我们用react或者Vue生成一个硕大的bundle文件以后,首屏载入速度必然受到这个单个巨大bundle文件影响。
因此,除了SSR之外,bigpipe也是一种很好的处理方法。
bigpipe实际加载效果
单个bundle实际加载效果 综上,BigPipe并不是适用于所有的场合,类似于Facebook和微博,youku,他主要适用于:- 第一个请求时间较长,后端程序需要读取多个API
- 页面上的劢态内容可以划分在多个区块内显示,且各个区块之间的关系不大(极弱耦合)
- SEO需求较弱