{"action":"opened","issue":{"url":"https://api.github.com/repos/lcxfs1991/blog/issues/6","labels_url":"https://api.github.com/repos/lcxfs1991/blog/issues/6/labels{/name}","comments_url":"https://api.github.com/repos/lcxfs1991/blog/issues/6/comments","events_url":"https://api.github.com/repos/lcxfs1991/blog/issues/6/events","html_url":"https://github.com/lcxfs1991/blog/issues/6","id":126000796,"number":6,"title":"性能优化三部曲之三——Node直出让你的网页秒开","user":{"login":"lcxfs1991","id":3348398,"avatar_url":"https://avatars.githubusercontent.com/u/3348398?v=3","gravatar_id":"","url":"https://api.github.com/users/lcxfs1991","html_url":"https://github.com/lcxfs1991","followers_url":"https://api.github.com/users/lcxfs1991/followers","following_url":"https://api.github.com/users/lcxfs1991/following{/other_user}","gists_url":"https://api.github.com/users/lcxfs1991/gists{/gist_id}","starred_url":"https://api.github.com/users/lcxfs1991/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lcxfs1991/subscriptions","organizations_url":"https://api.github.com/users/lcxfs1991/orgs","repos_url":"https://api.github.com/users/lcxfs1991/repos","events_url":"https://api.github.com/users/lcxfs1991/events{/privacy}","received_events_url":"https://api.github.com/users/lcxfs1991/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2016-01-11T17:55:59Z","updated_at":"2016-01-11T17:55:59Z","closed_at":null,"body":"## 项目： 手Q群成员分布直出\r\n\r\n## 原因：\r\n- 为家校群业务直出做准备\r\n- 群成员分布业务是小型业务，而且逻辑相当简单，方便做直出试验田\r\n\r\n## 基本概念：\r\n直出其实并不算是新概念。只不过在Web2.0单页应用流行的年代，一直被人遗忘在身后。其实在Web1.0时代，前后端没有分离的时候，程序员直接用后台程序渲染出模板，这便是直出。而到了今天，当Node大大提高了前端开发者的能力时，前端人员也可以做一些后台的事情，通过Node来实现模板的渲染和数据的吞吐。\r\n\r\n## 框架及技术：\r\n由AlloyTeam开发的，建基于Koa之上的玄武直出框架。该框架的优势在于：\r\n(1) 安装与本地开发简单：只需要几行命令安装和几行代码部署本地开发环境。\r\n(2) 完善的文档和规范的开发实践：经过数个项目的试验，文档已经日臻完善，并总结出一套可供大部份业务使用的实践方案。\r\n(3) 部署测试和发布成熟：目前已有兴趣部落、群搜索等数个项目成功发布。\r\n(4) 较好的容错功能： 通过公司的L5负载均衡服务，完美兼容直出与非直出版本，即使直出挂了，也能顺利走非直出版本，让前端可以安心睡大觉。\r\n\r\n## 直出方案：\r\n### 1. 数据拉取\r\n玄武框架提供一个封装了开源request库的一个同名request框架，并且提供异步拉取数据方案。文档代码如下：\r\n```\r\nfunction(req, res, next) {\r\n    var hander = function(error,response,body){\r\n        var data = JSON.parse(body);\r\n        res.body = body;\r\n        next();\r\n    };\r\n    ajax.request(opt,hander);\r\n}\r\n```\r\n\r\n也有不少人认为写异步比较不优雅，因此会使用promise, bluebird等第三方库。我在实践手Q群成员分布的时候，经过对generator的学习之后，探索出一个简易的同步写法，这种写法简单易懂，而且大大减少了对第三方库的依赖。如下面代码：\r\n```\r\nfunction*(req, res) {\r\n var opt = {\r\n    url : 'xxxxxxxxx',\r\n        method: 'POST',\r\n        form: {\r\n                bkn: getBkn(skey),\r\n                gc: gc,\r\n        },\r\n         headers : {\r\n                 'host' : 'web.qun.qq.com',\r\n                 'Referer' : 'web.qun.qq.com'\r\n         }\r\n };\r\nfunction requestSync(opt) {\r\n        return function(callback) {\r\n                ajax.request(opt, function(error, response, body) {\r\n                        callback(error, response);\r\n                });\r\n        }\r\n    }\r\n    var content = yield requestSync(opt);\r\n}\r\n```\r\n只要像上面代码一样进行简单的封装，就可以写出同步的写法。具体代码的分析和理解可以看文章的附录一部份。\r\n\r\n### 2. 模板渲染\r\n除了数据拉取，模板如何渲染也是直出的重要环节。下面有三种方案提供：\r\n（1） 在服务器中搭建一个浏览器，渲染好所有东西然后传给前台\r\n这种方案应该是最快的办法，但由于需要在服务器中搭建浏览器进行渲染，因此会消耗服务器大量性能，因此并不可取。\r\n（2） 通过玄武写主要逻辑，然后吐给前台再渲染\r\n这种方案的性能也是非常好的，但由于要将原本代码的部份逻辑放到服务器写，因此需要对后台和前台都做容错，会耗费一些开发时间。\r\n（3）只给前台吐出数据，然后完全由前台渲染\r\n这种方案的改动小，而且容错比较容易实现。例如，我在手Q群成员分布中，在html里加入如下代码：\r\n```\r\n<script>\r\n        {{'xw-data'}}\r\n</script>\r\n```\r\n然后在直出入口文件做替换：\r\nthis.body = tpl.init().replace('{{\\'xw-data\\'}}', 'var xw_data = ' + body + ';');\r\n然后在js文件里对xw_data做判断，如果有数据，直接去渲染，如果没数据，走旧有逻辑。\r\n\r\n## BigPipe\r\n### 1. 实现原理\r\nBigPipe是一个重新设计的动态网页服务体系。 将页面分解成一个个Pagelet，然后通过Web 服务器和浏览器之间建立管道，进行分段输出 （减少请求数）。BigPipe不需要改变现有的网络浏览器或服务器（百度百科）。\r\n\r\n![1](https://cloud.githubusercontent.com/assets/3348398/12241679/75bd51ca-b8cf-11e5-850a-85fe19f5cce3.png)\r\n\r\nBigPipe实际上也可以算作出直的一种特殊方案，最选是由Facebook提出(2010 Velocity Conference)，在国内由新浪微博最先进行大规模实践(2011 Velocity Conference)。\r\n传统的渲染方式，用户所感知的页面延时如下图，从页面生成，网络延迟到页面渲染都是串行进行的。\r\n\r\n![2](https://cloud.githubusercontent.com/assets/3348398/12241686/7f7deae4-b8cf-11e5-9a42-75ab886ba183.png)\r\n\r\n而BigPipe使得整个用户感知流程变成并行，使页面首屏渲染时间更快。如下图：\r\n\r\n![3](https://cloud.githubusercontent.com/assets/3348398/12241690/84e77e96-b8cf-11e5-949d-b7902ef3fd98.png)\r\n\r\nBigPipe的渲染方式，首先是在页面头部添加一个全局的加载Pagelet的onPageletArrive函数，然后渲染出HTML各Pagelet的占位标签，等各标签的数据到达的时候，依次调用全局onPageletArrive加载函数进行渲染。如以下代码。\r\n```\r\n<html>\r\n<head>\r\n<!-- 全局 onPageletArrive function -->\r\n<script>\r\nwindow.onPageletArrive = function(obj){\r\n// load css\r\n// load js\r\n// load html\r\n};\r\n</script>\r\n</head>\r\n<body>\r\n<!-- Pagelet 占位 -->\r\n<div id=\"pagelet1\"></div>\r\n<div id=\"pagelet2\"></div>\r\n….\r\n<div id=\"pageletN\"></div>\r\n<!-- 后台返回的js代码，用于直接吐出执行全局onPageletArrive -->\r\n<script>\r\nonPageletArrive({                                        \r\n\"id\" : \"pagelet1\",\r\n\"css\" : [ <list of css resources>],\r\n\"js\" : [ <list of JavaScript resources>] \"content\" : <html>\r\n\"onload\": [JavaScript init code]\r\n});\r\n</script>\r\n…\r\n<script>\r\n// onPageletArrive for pageletN\r\n</script>\r\n</body>\r\n</html>\r\n```\r\n\r\n从开发模式来说，BigPipe这种的写法比较适合组件化、模块化的前端开发模式。从网站规模来说，对于大型网站的优化效果会比较明显，毕竟分步吐出内容也是会有延时存在的，对于小型网站来说，有可能直接吐出所有内容会比分步吐出更快。\r\n\r\n### 2. 项目实践\r\n了解完基本的原理之后，我们来谈谈BigPipe在手Q群成员分布项目中用Node的具体实践。\r\n\r\n(1)  对页面分模块并设计好占位符\r\n首先要对页面尽可能地划分好不同的模块，亦即Pagelet。读过上一篇优化文章的人都记得，手Q群成员分布可以分为活跃群成员、男女比例、省市分布、年龄四大模块，如下面图片红框标框好的部份。\r\n\r\n![4](https://cloud.githubusercontent.com/assets/3348398/12241704/9311e312-b8cf-11e5-97d0-6ea5ea2bdcf5.png)\r\n\r\n![5](https://cloud.githubusercontent.com/assets/3348398/12241707/9718fea0-b8cf-11e5-989d-1214a0af25ca.png)\r\n\r\n![6](https://cloud.githubusercontent.com/assets/3348398/12241709/9b139696-b8cf-11e5-8a79-c74ccefbd6e6.png)\r\n\r\n\r\n相应地，我们得出如下的占位符。\r\n```\r\n<section>\r\n        <!-- 活跃群成员 -->\r\n        <div id=\"active\"></div>\r\n        <!-- 男女比例 -->\r\n        <div id=\"gender\"></div>\r\n        <!-- 成员省份 -->\r\n        <div id=\"area\"></div>\r\n        <!-- age -->\r\n        <div id=\"age\"></div>\r\n</section>\r\n```\r\n\r\n(2) 设计每个Pagelet需要渲染的内容，并对前后台的代码进行分工\r\n根据Facebook的方式，每个Pagelet都有自己独立需要加载的样式，JS文件，模板文件。这对于小型项目来说，大可不必。如手Q群成员分布项目中，我已直接将样式内联，模板文件也存在变量当中。因此，前端全局渲染函数主要负责将内容嵌入占位符当中。因此在head标签内，我设定了一个componentRender的函数，用于负责将后台吐出的内容嵌入占位符。代码大概如下:\r\n```\r\nfunction componentRender(target, tplString) {\r\n        // targetObj.innerHTML = tplString\r\n}\r\n```\r\n而后台的代码，则在拼好模板字符串之后，分步吐出内容，代码大体如下：\r\n```\r\nthis.push(\"<script>componentRender(\\\"#active\\,\" + tplString )</script>\");\r\nthis.push(\"<script>componentRender(\\\"#gender\\,\" + tplString )</script>\");\r\nthis.push(\"<script>componentRender(\\\"#area\\,\" + tplString )</script>\");\r\nthis.push(\"<script>componentRender(\\\"#age\\,\" + tplString )</script>\");\r\n```\r\n\r\n对于后台的代码，尤其是使用Koa框架，可能会无从入手，大家可以参考Github上的BigPipe Example。大体的写法和解释如下：\r\n```\r\n// BigPipe需要的模块，用于flush内容到页面\r\nvar Readable = require('stream').Readable;\r\n// 生成分片段页面内容函数\r\nvar createChunkedView = function(end) {\r\n    function noop() {};\r\n    typeof end === 'function' || (end = noop);\r\n    util.inherits(View, Readable);\r\n    function View(ctx) {\r\n        Readable.call(this);\r\n        ctx.type = 'text/html; charset=utf-8';\r\n        ctx.body = this;\r\n        this.app = ctx;\r\n    }\r\n    View.prototype._read = noop;\r\n    View.prototype.end = end;\r\n    return View;\r\n};\r\nfunction* end() {\r\n// 页面主逻辑，这里也要以用各种yeild\r\n// this.push(xxx)\r\n// this.push(xxx)\r\n// this.push(null) 最末尾请push空内容，通知框架内容flush结束\r\n}\r\nmodule.exports = function*() {\r\n        // 原本的koa view函数\r\n        const View = createChunkedView(end);\r\n            yield * new View(this).end();\r\n}\r\n```\r\n\r\n### 3. 成果\r\n首屏时间方面，直出方案比纯前端的方案有大概400ms的优化，提升约28.6%的性能。而BigPipe对比普通直出的优化有大约200ms，提升约16.7%。虽然实践的项目规模较小，模块数量也较少，但BigPipe的优化成果也算是比较令人满意的。\r\n\r\n页面渲染完成时间方面，纯前端的的优化对比优化前有了质的飞跃（在前一篇文章也提到），性能提升33%。普通直出对纯前端的优化提升约200ms, 性能提升约13.3%。而BigPipe优化跟普通直出优化则没有非常明显的优劣，只快了几十毫秒。\r\n\r\n## 总结\r\n纯前端的优化对其实也已经为项目带来比较好的提升。直出对于首屏的渲染也能带来不错的提升。另外，由于玄武框架只是一个简单的页面接入层，并不能直接对数据库进行操作，因此框架方面可以做的事情还有更多。\r\n\r\n系列文章里面所介绍到的纯前端优化方案、直出方案，都属于过去数年的方案。随着直出框架、方案及优秀实践的不断完善，相信应该要成为每个项目的标配。\r\n\r\n文中略略提到的离线包方案，属于腾讯手Q方面独立研发出来的针对手机端优化的方案，对于其它非腾讯业务也有一定借鉴的意义，具体要参考我导师2014年在InfoQ上有关离线包系统的讲解(链接)。至于未来数年，我们可以将希望寄放在websocket，当然还有即将普及的HTTP2.0身上。\r\n\r\n\r\n## 附录一\r\n数据拉取同步方案：\r\n```\r\nfunction*(req, res) {\r\nvar opt = {\r\nurl : 'xxxxxxxxx',\r\n        method: 'POST',\r\n        form: {\r\n                bkn: getBkn(skey),\r\n                gc: gc,\r\n        },\r\n         headers : {\r\n                 'host' : 'web.qun.qq.com',\r\n                 'Referer' : 'web.qun.qq.com'\r\n         }\r\n};\r\nfunction requestSync(opt) {\r\n        return function(callback) {\r\n                ajax.request(opt, function(error, response, body) {\r\n                        callback(error, response);\r\n                });\r\n        }\r\n}\r\nvar content = yield requestSync(opt);\r\n}\r\n```\r\n如何理解?\r\n简单的回调用法：\r\n```\r\nfunction* Hello() {\r\n         yield 1;\r\n         yield 2;\r\n}\r\n\r\nvar hello = Hello() // hello 是一个generator\r\nvar a = hello.next() // a: Object {value: 1, done: false}\r\nvar b = hello.next() // b: Object {value: 2, done: false} \r\nvar c = hello.next() // c: Object {value: undefined, done: true}\r\n```\r\n在next中传入参数：\r\n```\r\nfunction* gen() {\r\n        while(true) {\r\n                var value = yield null;\r\n                   console.log(value);\r\n        }\r\n}\r\nvar g = gen();\r\ng.next(1); \r\n// \"{ value: null, done: false }\"\r\ng.next(2); \r\n// \"{ value: null, done: false }\"\r\n// 2\r\n// 在该示例中，调用 next 方法并传入了参数，请注意，首次调用 next 方法时没有出任何输出, 这是 因为初始状态时生成器通过yield 返回了null.\r\n```\r\n\r\n经典示例一，按顺序执行：\r\n```\r\nfunciton delay(time, cb) {\r\n setTimeout(function() {\r\n   cb && cb()\r\n }, time)\r\n}\r\ndelay(200, function() {\r\n delay(1000, function() {\r\n   delay(500, function() {\r\n     console.log('finish')\r\n   })\r\n })\r\n})\r\nfunction delay(time) {\r\n return function(fn) {\r\n   setTimeout(function() {\r\n     fn()\r\n   }, time)\r\n }\r\n}\r\n\r\nco(function* () {\r\n yield delay(200);\r\n yield delay(1000);\r\n yield delay(500);\r\n})(function() {\r\n console.timeEnd(1) // print 1: 1702.000ms \r\n})\r\nfunction co(GenFunc) {    // GenFunc → function*() { yield ……….}\r\n return function(cb) {   // function() { console.log.timeEnd(1); }\r\n   var gen = GenFunc()\r\n   next()\r\n   function next() {\r\n     if (gen.next) {\r\n       var ret = gen.next(); \r\n      // {done: true/false, value=delay 返回的function}\r\n       if (ret.done) { // 如果结束就执行cb\r\n         cb && cb()\r\n       } else { // 继续next\r\n         ret.value(next)\r\n       }\r\n     }\r\n   }\r\n }\r\n}\r\n```\r\n经典示例二，按顺序执行并且下一个执行依赖于上一个执行的返回值：\r\n```\r\nfunction delay(time) {\r\n        return function(fn) {\r\n                   setTimeout(function() {\r\n                     fn(time) // time为回调参数\r\n                   }, time)\r\n         }\r\n}\r\n\r\nco(function* () {\r\n        var a;\r\n         a = yield delay(200); // a: 200\r\n         a = yield delay(a + 100); // a: 300\r\n         a = yield delay(a + 100); // a: 400\r\n    return a;\r\n})(function(data) {\r\n         console.log(data) // print 400, 最后的值被回调出来\r\n});\r\nfunction co(GenFunc) {\r\n        return function(cb) {\r\n                   var gen = GenFunc();\r\n                   next();\r\n                   function next(args) { // 传入args\r\n                     if (gen.next) {\r\n                               var ret = gen.next(args) // 给next传入args\r\n                               if (ret.done) {\r\n                                         cb && cb(args)\r\n                               } else {\r\n                                 ret.value(next);\r\n                       }\r\n             }\r\n           }\r\n}\r\n```\r\n回到玄武直出代码：\r\n```\r\nmodule.exports = function*(req, res) {\r\n}\r\nco(function*(req, res){\r\n    // some code\r\n        function requestSync(opt) {\r\n        return function(callback) {\r\n                ajax.request(opt, function(error, response, body) {\r\n                        callback(error, response);\r\n                });\r\n        }\r\n}\r\nvar content = yield requestSync(opt); \r\n// 在gen.next()之后，yield会返回值给content\r\n// other code\r\n});\r\n```\r\n参考：\r\nhttp://www.alloyteam.com/2015/03/es6-generator-introduction/\r\nhttp://www.alloyteam.com/2015/04/solve-callback-hell-with-generator/\r\nhttp://www.html-js.com/article/Nodejs-commonly-used-modules-detailed-address-correction-in-Pyramid-Harmony-Generator-yield-ES6-CO-framework-of-learning"}}