性能优化三部曲之三——Node直出让你的网页秒开

项目: 手Q群成员分布直出

原因:

  • 为家校群业务直出做准备
  • 群成员分布业务是小型业务,而且逻辑相当简单,方便做直出试验田

基本概念:

直出其实并不算是新概念。只不过在Web2.0单页应用流行的年代,一直被人遗忘在身后。其实在Web1.0时代,前后端没有分离的时候,程序员直接用后台程序渲染出模板,这便是直出。而到了今天,当Node大大提高了前端开发者的能力时,前端人员也可以做一些后台的事情,通过Node来实现模板的渲染和数据的吞吐。

框架及技术:

由AlloyTeam开发的,建基于Koa之上的玄武直出框架。该框架的优势在于: (1) 安装与本地开发简单:只需要几行命令安装和几行代码部署本地开发环境。 (2) 完善的文档和规范的开发实践:经过数个项目的试验,文档已经日臻完善,并总结出一套可供大部份业务使用的实践方案。 (3) 部署测试和发布成熟:目前已有兴趣部落、群搜索等数个项目成功发布。 (4) 较好的容错功能: 通过公司的L5负载均衡服务,完美兼容直出与非直出版本,即使直出挂了,也能顺利走非直出版本,让前端可以安心睡大觉。

直出方案:

1. 数据拉取

玄武框架提供一个封装了开源request库的一个同名request框架,并且提供异步拉取数据方案。文档代码如下:

function(req, res, next) {
    var hander = function(error,response,body){
        var data = JSON.parse(body);
        res.body = body;
        next();
    };
    ajax.request(opt,hander);
}

也有不少人认为写异步比较不优雅,因此会使用promise, bluebird等第三方库。我在实践手Q群成员分布的时候,经过对generator的学习之后,探索出一个简易的同步写法,这种写法简单易懂,而且大大减少了对第三方库的依赖。如下面代码:

function*(req, res) {
 var opt = {
    url : 'xxxxxxxxx',
        method: 'POST',
        form: {
                bkn: getBkn(skey),
                gc: gc,
        },
         headers : {
                 'host' : 'web.qun.qq.com',
                 'Referer' : 'web.qun.qq.com'
         }
 };
function requestSync(opt) {
    return function(callback) {
                ajax.request(opt, function(error, response, body) {
                        callback(error, response);
                });
        }
    }
    var content = yield requestSync(opt);
}

只要像上面代码一样进行简单的封装,就可以写出同步的写法。具体代码的分析和理解可以看文章的附录一部份。

2. 模板渲染

除了数据拉取,模板如何渲染也是直出的重要环节。下面有三种方案提供: (1) 在服务器中搭建一个浏览器,渲染好所有东西然后传给前台 这种方案应该是最快的办法,但由于需要在服务器中搭建浏览器进行渲染,因此会消耗服务器大量性能,因此并不可取。 (2) 通过玄武写主要逻辑,然后吐给前台再渲染 这种方案的性能也是非常好的,但由于要将原本代码的部份逻辑放到服务器写,因此需要对后台和前台都做容错,会耗费一些开发时间。 (3)只给前台吐出数据,然后完全由前台渲染 这种方案的改动小,而且容错比较容易实现。例如,我在手Q群成员分布中,在html里加入如下代码:

<script>
        {{'xw-data'}}
</script>

然后在直出入口文件做替换: this.body = tpl.init().replace('{{'xw-data'}}', 'var xw_data = ' + body + ';'); 然后在js文件里对xw_data做判断,如果有数据,直接去渲染,如果没数据,走旧有逻辑。

BigPipe

1. 实现原理

BigPipe是一个重新设计的动态网页服务体系。 将页面分解成一个个Pagelet,然后通过Web 服务器和浏览器之间建立管道,进行分段输出 (减少请求数)。BigPipe不需要改变现有的网络浏览器或服务器(百度百科)。

BigPipe实际上也可以算作出直的一种特殊方案,最先是由Facebook提出(2010 Velocity Conference),在国内由新浪微博最先进行大规模实践(2011 Velocity Conference)。 传统的渲染方式,用户所感知的页面延时如下图,从页面生成,网络延迟到页面渲染都是串行进行的。

而BigPipe使得整个用户感知流程变成并行,使页面首屏渲染时间更快。如下图:

BigPipe的渲染方式,首先是在页面头部添加一个全局的加载Pagelet的onPageletArrive函数,然后渲染出HTML各Pagelet的占位标签,等各标签的数据到达的时候,依次调用全局onPageletArrive加载函数进行渲染。如以下代码。

<html>
<head>
<!-- 全局 onPageletArrive function -->
<script>
window.onPageletArrive = function(obj){
// load css
// load js
// load html
};
</script>
</head>
<body>
<!-- Pagelet 占位 -->
<div id="pagelet1"></div>
<div id="pagelet2"></div>
….
<div id="pageletN"></div>
<!-- 后台返回的js代码,用于直接吐出执行全局onPageletArrive -->
<script>
onPageletArrive({                                        
"id" : "pagelet1",
"css" : [ <list of css resources>],
"js" : [ <list of JavaScript resources>] "content" : <html>
"onload": [JavaScript init code]
});
</script>
…
<script>
// onPageletArrive for pageletN
</script>
</body>
</html>

从开发模式来说,BigPipe这种的写法比较适合组件化、模块化的前端开发模式。从网站规模来说,对于大型网站的优化效果会比较明显,毕竟分步吐出内容也是会有延时存在的,对于小型网站来说,有可能直接吐出所有内容会比分步吐出更快。

2. 项目实践

了解完基本的原理之后,我们来谈谈BigPipe在手Q群成员分布项目中用Node的具体实践。

(1) 对页面分模块并设计好占位符 首先要对页面尽可能地划分好不同的模块,亦即Pagelet。读过上一篇优化文章的人都记得,手Q群成员分布可以分为活跃群成员、男女比例、省市分布、年龄四大模块,如下面图片红框标框好的部份。

相应地,我们得出如下的占位符。

<section>
        <!-- 活跃群成员 -->
        <div id="active"></div>
        <!-- 男女比例 -->
        <div id="gender"></div>
        <!-- 成员省份 -->
        <div id="area"></div>
        <!-- age -->
        <div id="age"></div>
</section>

(2) 设计每个Pagelet需要渲染的内容,并对前后台的代码进行分工 根据Facebook的方式,每个Pagelet都有自己独立需要加载的样式,JS文件,模板文件。这对于小型项目来说,大可不必。如手Q群成员分布项目中,我已直接将样式内联,模板文件也存在变量当中。因此,前端全局渲染函数主要负责将内容嵌入占位符当中。因此在head标签内,我设定了一个componentRender的函数,用于负责将后台吐出的内容嵌入占位符。代码大概如下:

function componentRender(target, tplString) {
        // targetObj.innerHTML = tplString
}

而后台的代码,则在拼好模板字符串之后,分步吐出内容,代码大体如下:

this.push("<script>componentRender(\"#active\," + tplString )</script>");
this.push("<script>componentRender(\"#gender\," + tplString )</script>");
this.push("<script>componentRender(\"#area\," + tplString )</script>");
this.push("<script>componentRender(\"#age\," + tplString )</script>");

对于后台的代码,尤其是使用Koa框架,可能会无从入手,大家可以参考Github上的BigPipe Example。大体的写法和解释如下:

// BigPipe需要的模块,用于flush内容到页面
var Readable = require('stream').Readable;
// 生成分片段页面内容函数
var createChunkedView = function(end) {
    function noop() {};
    typeof end === 'function' || (end = noop);
    util.inherits(View, Readable);
    function View(ctx) {
        Readable.call(this);
        ctx.type = 'text/html; charset=utf-8';
        ctx.body = this;
        this.app = ctx;
    }
    View.prototype._read = noop;
    View.prototype.end = end;
    return View;
};
function* end() {
// 页面主逻辑,这里也要以用各种yeild
// this.push(xxx)
// this.push(xxx)
// this.push(null) 最末尾请push空内容,通知框架内容flush结束
}
module.exports = function*() {
        // 原本的koa view函数
        const View = createChunkedView(end);
            yield * new View(this).end();
}

3. 成果

首屏时间方面,直出方案比纯前端的方案有大概400ms的优化,提升约28.6%的性能。而BigPipe对比普通直出的优化有大约200ms,提升约16.7%。虽然实践的项目规模较小,模块数量也较少,但BigPipe的优化成果也算是比较令人满意的。

页面渲染完成时间方面,纯前端的的优化对比优化前有了质的飞跃(在前一篇文章也提到),性能提升33%。普通直出对纯前端的优化提升约200ms, 性能提升约13.3%。而BigPipe优化跟普通直出优化则没有非常明显的优劣,只快了几十毫秒。

总结

纯前端的优化对其实也已经为项目带来比较好的提升。直出对于首屏的渲染也能带来不错的提升。另外,由于玄武框架只是一个简单的页面接入层,并不能直接对数据库进行操作,因此框架方面可以做的事情还有更多。

系列文章里面所介绍到的纯前端优化方案、直出方案,都属于过去数年的方案。随着直出框架、方案及优秀实践的不断完善,相信应该要成为每个项目的标配。

文中略略提到的离线包方案,属于腾讯手Q方面独立研发出来的针对手机端优化的方案,对于其它非腾讯业务也有一定借鉴的意义,具体要参考我导师2014年在InfoQ上有关离线包系统的讲解(链接)。至于未来数年,我们可以将希望寄放在websocket,当然还有即将普及的HTTP2.0身上。

附录一

数据拉取同步方案:

function*(req, res) {
var opt = {
url : 'xxxxxxxxx',
        method: 'POST',
        form: {
                bkn: getBkn(skey),
                gc: gc,
        },
         headers : {
                 'host' : 'web.qun.qq.com',
                 'Referer' : 'web.qun.qq.com'
         }
};
function requestSync(opt) {
        return function(callback) {
                ajax.request(opt, function(error, response, body) {
                        callback(error, response);
                });
        }
}
var content = yield requestSync(opt);
}

如何理解? 简单的回调用法:

function* Hello() {
         yield 1;
         yield 2;
}

var hello = Hello() // hello 是一个generator
var a = hello.next() // a: Object {value: 1, done: false}
var b = hello.next() // b: Object {value: 2, done: false} 
var c = hello.next() // c: Object {value: undefined, done: true}

在next中传入参数:

function* gen() {
        while(true) {
                var value = yield null;
                   console.log(value);
        }
}
var g = gen();
g.next(1); 
// "{ value: null, done: false }"
g.next(2); 
// "{ value: null, done: false }"
// 2
// 在该示例中,调用 next 方法并传入了参数,请注意,首次调用 next 方法时没有出任何输出, 这是 因为初始状态时生成器通过yield 返回了null.

经典示例一,按顺序执行:

funciton delay(time, cb) {
 setTimeout(function() {
   cb && cb()
 }, time)
}
delay(200, function() {
 delay(1000, function() {
   delay(500, function() {
     console.log('finish')
   })
 })
})
function delay(time) {
 return function(fn) {
   setTimeout(function() {
     fn()
   }, time)
 }
}

co(function* () {
 yield delay(200);
 yield delay(1000);
 yield delay(500);
})(function() {
 console.timeEnd(1) // print 1: 1702.000ms 
})
function co(GenFunc) {    // GenFunc → function*() { yield ……….}
 return function(cb) {   // function() { console.log.timeEnd(1); }
   var gen = GenFunc()
   next()
   function next() {
     if (gen.next) {
       var ret = gen.next(); 
      // {done: true/false, value=delay 返回的function}
       if (ret.done) { // 如果结束就执行cb
         cb && cb()
       } else { // 继续next
         ret.value(next)
       }
     }
   }
 }
}

经典示例二,按顺序执行并且下一个执行依赖于上一个执行的返回值:

function delay(time) {
        return function(fn) {
                   setTimeout(function() {
                     fn(time) // time为回调参数
                   }, time)
         }
}

co(function* () {
        var a;
         a = yield delay(200); // a: 200
         a = yield delay(a + 100); // a: 300
         a = yield delay(a + 100); // a: 400
    return a;
})(function(data) {
         console.log(data) // print 400, 最后的值被回调出来
});
function co(GenFunc) {
        return function(cb) {
                   var gen = GenFunc();
                   next();
                   function next(args) { // 传入args
                     if (gen.next) {
                               var ret = gen.next(args) // 给next传入args
                               if (ret.done) {
                                         cb && cb(args)
                               } else {
                                 ret.value(next);
                       }
             }
           }
}

回到玄武直出代码:

module.exports = function*(req, res) {
}
co(function*(req, res){
    // some code
        function requestSync(opt) {
        return function(callback) {
                ajax.request(opt, function(error, response, body) {
                        callback(error, response);
                });
        }
}
var content = yield requestSync(opt); 
// 在gen.next()之后,yield会返回值给content
// other code
});

参考: http://www.alloyteam.com/2015/03/es6-generator-introduction/ http://www.alloyteam.com/2015/04/solve-callback-hell-with-generator/ http://www.html-js.com/article/Nodejs-commonly-used-modules-detailed-address-correction-in-Pyramid-Harmony-Generator-yield-ES6-CO-framework-of-learning

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python小屋

Python实现局域网内屏幕广播的技术要点分析

为更好地保证教学质量和提高学生的学习积极性,我使用Python开发了一套课堂教学管理系统,具有在线点名、在线答疑、随机提问、在线作业管理、在线自测、在线考试、数...

3637
来自专栏DannyHoo的专栏

定位1

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

832
来自专栏美团技术团队

前端黑科技:美团网页首帧优化实践

自JavaScript诞生以来,前端技术发展非常迅速。移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR 、CSR、预渲染等技术。在美团...

1142
来自专栏java一日一条

前端优化带来的思考,浅谈前端工程化

这段时间对项目做了一次整体的优化,全站有了20%左右的提升(本来载入速度已经1.2S左右了,优化度很低),算一算已经做了四轮的全站性能优化了,回顾几次的优化手段...

523
来自专栏Vue ssr

WebPagetest:新手一眼看懂使用教程

这是一个非常详细且专业的web页面性能分析工具,而且开源的!如果你打不开其官网,或者担心安全问题,你可以自己拿源码搭建这个平台工具。

2044
来自专栏架构师之路

微信钱包中58到家首页为什么这么快

原文标题《前后端分离和模块化-58到家微信首页重构之路》【深度前端干货】 微信钱包内的58到家全新首页已经上线,感兴趣的同学们可以在微信中打开“我的->钱包->...

2817
来自专栏LeoXu的博客

[翻译]使用 Velocity 构建一个稳定安全的Web应用

<p> draft document -- 2003年6月11日 </p> <p> 作为一名web开发者,任何时候当你构建一个Web应用时,有责任确保你的应用程...

862
来自专栏非著名程序员

Android开发框架之KJFrameForAndroid

KJFrameForAndroid 又叫KJLibrary,是一个android的orm 和 ioc 框架。同时封装了android中的Bitmap与Http...

17710
来自专栏GreenLeaves

Web性能优化之雅虎军规

相信互联网已经越来越成为人们生活中不可或缺的一部分。Ajax,flex等等富客户端的应用使得人们越加“幸福”地体验着许多原先只能在C/S实 现的功能。 比如Go...

18310
来自专栏不止是前端

Node:使用Puppeteer完成一次复杂的爬虫

6209

扫码关注云+社区