Bigpipe 是一种异步渲染方案,能有效提升页面加载速度。它也是Facebook前端渲染的主力,据说为网站提速近一倍。
Bigpipe是怎么做到的?
它的思路:将页面分割成多个部分(pagelet),先向用户输出没有数据的布局(框架),然后将每个部分逐步输出到前端,再最终渲染填充框架,完成整个网页的渲染。这个过程需要前端JavaScript渲染,后端异步输出相互配合。
了解这些,容易联想到的是Node的异步输出能力。
在并发的情况下,数据库IO往往会成为一个瓶颈,如果使用传统的模板渲染(EJS/Jade),恐怕还是个同步的思路:请求DB取数据 -> 串行等待数据到位 -> 渲染。完全没有发挥异步IO的长处。
事实上,Node的http.ServerResponse提供了异步输出的能力: 如果只是response.write数据,没有指示response.end,那么这个响应就没有结束,浏览器会保持这个请求。这意味着,在响应end之前,我们都有机会持续输出响应到客户端。注意,是持续交付,可以理解为一个长连接,并且不阻塞UI线程。
知道这些,就可以模拟Bigpipe了:
场景
有一个页面,需要连到Redis中去取队列数据,取到一条显示一条,全部取完时,页面完成输出。
实验
首先封装一个操作Redis 队列的模块(队列使用 list 的 lpush/rpop 实现):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var redis = require('redis'); var redisUtils = function () { this.client = redis.createClient() }; redisUtils.prototype.push = function (ele) { return this.client.lpush('mylist', ele); }; redisUtils.prototype.pop = function (callback, done) { var that = this; this.popOne('mylist', function (err, reply) { if (reply) { callback(err, reply); that.popOne('mylist', arguments.callee); } else { delete that; done(); } }); }; redisUtils.prototype.disconnect = function () { this.client.quit(); }; redisUtils.prototype.popOne = function (listName, callback) { this.client.rpop(listName, function (err, reply) { callback(err, reply); }); }; module.exports = redisUtils; |
取数据方法是 pop(callback, done),由于使用的node_redis模块,在pop时会有一个Context的切换,闭包引用this 到 that,在完成时 delete掉这个引用以防止泄漏 。其中的 callback 为取每一个出栈元素的回调,它会调用response.write;而 done 就是指示队列已空,取元素完成,它会调用response.end,并关闭 Redis 连接。
页面后端调用就相对简单了,这里只是做演示,所以把数据生成代码也写了进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var express = require('express'); var router = express.Router(); var redisUtils = require('../modules/redis-utils'); var fs = require('fs'); var path = require('path'); var cache = {}; var layout = path.join(process.cwd(), 'views/redis.ejs'); router.get('/', function(req, res) { if (!cache[layout]) { cache[layout] = fs.readFileSync(layout, 'utf-8'); } res.writeHead(200, {'Content-Type': 'text/html'}); res.write(cache[layout]); var redis = new redisUtils(); redis.push('1'); redis.push('2'); redis.push('3'); redis.push('4'); redis.pop(function(err, data){ res.write('<div>' + JSON.stringify(data) + '</div>'); }, function(){ res.end(); redis.disconnect(); }); }); module.exports = router; |
就是一普通的 express 路由模块,为了省事,直接读取一个ejs 模板文件做渲染用,这个模块文件长这样:
1 2 3 4 5 6 7 8 9 10 11 12 |
<!DOCTYPE html> <html> <head> <title>Bigpipe sample</title> <style type="text/css"> div{ border:solid 1px #ccc; width:60px; height:60px; } </style> </head> |
运行效果:
当然,实验中齐刷刷出现四个div的效果,没什么说服力,应用到真实数据规模的场景中,才能体现它的价值。