首屏优化之BigPipe

5,301 阅读5分钟

从刚开始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需求较弱

转自新浪微博(Weibo.com)前端构建详解之BigPipe

如何在Koa集成Bigpipe首屏渲染服务

聊一聊网页的分段传输与渲染那些事儿