浅谈BigPipe

随着页面元素的越来越多,页面的加载也会越来越慢,性能的问题也就越来越突出,来看看BigPipe能够带给我们什么。

背景

对于一个简单的项目来说,直接前端发送请求,然后后端请求数据返回页面一气呵成并不会有太多的性能问题。但是对于一些复杂的页面,拿电商来说,商品的详情页面分为很多块,可能需要请求不同的后台系统API,比如商品库系统,会员系统,推荐系统等等从而拼接成这样一个页面。对于这样的页面,意味着我们可能发很多请求去请求不同的后台api从而获取页面片段,然而请求的建立是需要消耗时间和性能的,那么我们如何能够短时间加载完呢?

懒加载

当然,一个最容易实现的方法就是懒加载,对于不需要展现的内容先不请求,等到需要展示的时候再请求,这点对于移动端来说比较容易实现,但是对于PC大屏幕来说,需要首屏渲染的东西太多,懒加载几乎没有作用。

nodejs作为中间层

现在的话,流行的一种开发方式是,后端只管业务逻辑、数据操作、api接口实现,而前端利用nodejs作为页面渲染以及路由定制等,这样做有什么好处呢?拿刚才的栗子而言,如果我一个页面需要发送多个请求到后台,这样在建立请求上会消耗大量的时间,但采用nodejs作为中间层的话前端只需要发送一个请求到nodejs,然后nodejs负责调用不同的api接口请求数据,然后组装页面返回给前端渲染。

BigPipe的出现

这样就完美解决问题了么?并没有。nodejs更多的其实是解决前后端分离的问题,让后端能够专注于业务逻辑那些的开发而不用设计页面渲染等方面,通过api接口的设计可以实现一次开发,不同的系统都只需要调用这个api接口就可以了。就刚才所说,nodejs确实解决多请求的问题,但是也同样出现了瓶颈,我们还是先来看看前后端请求的过程:
1、页面发送http请求
2、后端接受http请求,然后请求数据并且封装数据(这里的后端指nodejs)
3、后端返回渲染好的模板
4、前端接收模板,并且渲染成dom树并且去下载CSS和JS文件
5、下载完CSS 文件后,浏览器解析他们并且应用在相应的内容上
6、下载完JS 后,浏览器解析和执行他们
大家会发现这是一个线性的过程,也就是说当前端发送请求去请求页面后,它处于等待时间,这个时候操作全部都在后端,只有等到后端处理完控制权才交还给前端,然后后端在进行等待。由于前后端处理是没有层叠部分的,所以导致时间很长,假如一个查询语句阻塞了数据的获取的话,那么前端获取到页面的时间就会更加长,并且这个过程对于用户来说是不可见的,所以用户也是无法忍受的,所以也迫切需要一种方法进行解决。FaceBook正是遇到了这种问题,2010年初的时候,Facebook的前端性能研究小组开始了他们的优化项目,经过了六个月的努力,成功的将个人空间主页面加载耗时由原来的5 秒减少为现在的2.5秒。这是一个非常了不起的成就,也给用户来带来了很好的体验。在优化项目中,工程师提出了一种新的页面加载技术,称之为Bigpipe。

BigPipe的好处

如果说刚才引入nodejs减少页面请求是化零为整的话,那么BigPipe更像是化整为零,你会说,这不是相当于什么都没做么,且听我慢慢道来。
BigPipe提出分块的概念,即根据页面内容位置的不同,将整个页面分成不同的块儿,称为pagelet。然后根据下面的过程进行传输:
1、Request parsing:服务器解析和检查http request
2、Datafetching:服务器从存储层获取数据
3、Markup generation:服务器生成html 标记
4、Network transport : 网络传输response
5、CSS downloading:浏览器下载CSS
6、DOM tree construction and CSS styling:浏览器生成DOM 树,并且使用CSS
7、JavaScript downloading: 浏览器下载页面引用的JS 文件
8、JavaScript execution: 浏览器执行页面JS代码
你会发现这个跟上面的流程差不多是一样,但这只是一个pagelet的传输过程,这样多个pagelet就可以并行传输了,从而使得客户端和服务端并不需要等待彼此,也就是说当客户端在处理一个pagelet的渲染的同时,服务器端可能正在处理下一个pagelet的封装传输,所以总的传输时间是小于所有的pagelet的传输时间和的。

BigPipe原理

那么具体是怎么实现的呢?当浏览器访问服务器时,服务器接受请求并对其进行检查。如果请求有效,服务器端不做任何的查询,而是立刻返回一个 http request 给浏览器,内容是一段html 代码,包括html 标签和 标签的一部分。标签包括BigPipe 的js文件和css文件,这个js 文件用来解析后面接收的http response,因为后面传输的内容都为js脚本。未封闭的标签中,是显示页面的逻辑结构和pagelet 的占位符的模板,例如:

1
2
3
4
5
6
7
8
<body>
<div id="pagelet1"></div>
<div id="pagelet2"></div>
<div id="pagelet3"></div>
<div id="pagelet4"></div>
<div id="pagelet5"></div>
<div></div>
<div></div>

上述模板使用css-div 描述了页面的结构,不同的div 标签对应不同的pagelet,id 对应了pagelet 的名称。将这个response 返回给浏览器后,服务器开始对每个pagelet 的内容进行查询,加载,生成。当一个pagelet的内容生成好,立刻调用flush()函数,将其返回给客户端,传输的数据是以json 格式的,包括这个pagelet 需要的CSS 和JS,以及html 内容和一些元数据。例如:

1
2
3
4
5
6
7
8
9
10
11
<script type=”text/javascript”>
big_pipe.onPageletArrive(
{
id:"pagelet_composer",
content:"<HTML>",
css:"[..]",
js:"[..]",
……
}
);
</script>

其中”content”表示这个pagelet的内容,是html源码,特殊字符如“”/需要进行转义;”id”表示content要显示的位置,即为对应的pagelet 的id标签;”css”表示需要下载的CSS 资源的路径;”js”表示需要下载的JS 脚本的路径。为了避免文件路径过长,所以在前面需要对css 和js 文件的路径进行转换,转换后为5 位字符串:不同的pagelet 可能会加载同一个css 或js 文件,所以要避免重复下载。
虽然每个pagelet 都有要加载的js文件,但是所有的js文件都是在最后加载,这样有利于加快页面加载速度。客户端,当通过调用“onPageletArrive(json)”函数,第一次影响传输的JS脚本中 的函数解析了传入的json 数据,接着下载需要的CSS,然后把html 内容显示到响应的DIV 标签位置上。多个pagelets 的CSS文件可以同时下载,CSS 下载完成的pagelet 先显示。
在BigPipe 中,js 被给予了比CSS和content更低的优先级。这样,只有当所有的pagelets都显示了,BigPipe 才开始去下载JS文件。所有的JS文件都下载完成后,Pagelets的JS初始化代码开始执行,按照下载完成时间的先后顺序。从用户的角度看来,页面时逐步呈现的。初始的页面显示的更快,可以有效减短用户感觉到的延迟。

BigPipe其他一些问题的讨论

1、并行化处理:服务器端如果可以并行化处理pagelet的装载的话,可以很大程度加快效率,但对于PHP这种单线程的话,要么使用模拟多线程,不然只能串行装载pagelet,但串行已经能满足我们的需求。
2、影响SEO:BigPipe因为是使用json进行页面传输的,所以是会影响SEO,解决方法就是在服务器端首先要根据user-agent判断客户端是否是搜索引擎的爬虫,如果是的话,则转化为原有的模式,而不是动态添加,这样就解决了对搜索引擎的不友好。