首屏加载优化
概述
页面加载的方式包括以下几种:
- 直接同步加载
一次把服务端的渲染内容加载到浏览器,当页面内容比较少时可以考虑这种方式。 - 滚动同步加载
服务端渲染首屏的内容,其他屏幕的内容放到textarea或者注释中,滚动时再渲染其他屏的内容,此种比较适合。 - 异步加载
服务端渲染主 layout ,加载到客户端,通过 AJAX 获取其他页面内容,然后在客户端渲染。此种和淘宝无线 H5 的方案类似。 - 滚动异步加载
服务端渲染首屏内容,加载到客户端,滚动时再通过 AJAX 获取次屏内容 - 分块加载
服务端支持 chunk 输出,分块将内容传输到客户端,客户端渲染。
对比上述几种方式,1和2并不能加快首屏加载的速度;3和4需要通过ajax获取其余的内容,但是对首屏加载是有益的;5是最优方案,在Node中对应的是Bigpipe。
分块传输
在讨论Bigpipe前,需要了解其技术支撑:分块传输编码。分块传输编码对应http中字段是:Transfer-Encoding
,它是HTTP1.1版本中引入的新技术,目的是在已经建立的tcp连接上持续传递内容。
Persistent Connection
通过持久连接(persistent connection),TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive
。由于浏览器和服务器实现的问题,现在还需设置Connection: keep-alive
去表明当前为长连接,但协议已经不需要了。
客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接,也可以通过Connection: close
明确指明去关闭链接。
Content-Length
一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的,这就是Content-length
字段的作用,声明本次回应的数据长度。
在1.0版中,Content-Length
字段不是必需的,因为浏览器发现服务器关闭了TCP连接,就表明收到的数据包已经全了。
在HTTP1.1协议下,如果在请求时不指定Content-Length
会有什么情况呢?如下程序:
require('net').createServer(function(sock) {
sock.on('data', function(data) {
sock.write('HTTP/1.1 200 OK\r\n');
sock.write('Content-Length: 5\r\n'); // 指定数据包长度 考虑:1. 不添加此行程序 2. 小于实际报文长度 3. 大于实际报文长度
sock.write('\r\n');
sock.write('hello world!');
// sock.destroy();
});
}).listen(9090, '127.0.0.1');
如手动断开连接,那么不论我们怎么设置Content-Length
,请求很快完成,只是浏览器能不能正常获取到内容。
如不手动断开连接,会有以下三种情况:
- 不添加
Content-Length
,客户端会一直等待; -
Content-Length
设置的长度小于或等于实际报文长度,请求顺利完成,但是内容不全; -
Content-Length
设置的长度大于实际报文长度,客户端会一直等待。
TTFB(Time To First Byte)是web性能优化一个很重要的指标,它代表客户端从发送请求到收到第一个字节所花费的时间。TTFB越短, 意味着用户可以越早看到页面内容,体验越好。通过上面的分析可知,为了让客户端顺利收到响应内容,需要一个正确的 Content-Length
值,而为了计算此值需要服务端缓存所有内容,这就和TTFB背道而驰。在 HTTP 报文中,实体一定要在头部之后,顺序不能颠倒,为此我们需要一个新的机制:不依赖头部的长度信息,也能知道实体的边界。
Transfer-Encoding: chunked
分块传输的基本思想是:服务器产生一块数据,就发送一块,采用流模式(stream)取代缓存模式(buffer)。每个非空的数据块之前,会有一个16进制的数值,表示这个块的长度。最后是一个大小为0的块,就表示本次回应的数据发送完了:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
0
下面是采用Nodejs模拟Transfer-Encoding: chunked
的例子:
require('net').createServer(function(sock) {
sock.on('data', function(data) {
sock.write('HTTP/1.1 200 OK\r\n');
sock.write('Transfer-Encoding: chunked\r\n');
sock.write('\r\n');
sock.write('b\r\n');
sock.write('01234567890\r\n');
sock.write('5\r\n');
sock.write('12345\r\n');
sock.write('0\r\n');
sock.write('\r\n');
});
}).listen(9090, '0.0.0.0');
实战
Nodejs的http封装本身是按Transfer-Encoding: chunked
进行传输的,如下面的例子:
var http = require('http');
http.createServer(function (request, response){
response.writeHead(200, {'Content-Type': 'text/html'});
response.write('hello');
response.write(' world ');
response.write('~ ');
setTimeout(function(){
response.write(' 大家好 ');
}, 3000);
// response.end();
}).listen(9090, "127.0.0.1");
我们虽然把response.end()
注释掉,但是客户端还是能得到内容,而且在hello world输出后的3秒能接收到大家好这三字。可以说借助Node比较简单的实现了分块传输。
Express、koa等实现分块传输的思想是类似的,具体实现方式可以参考文章:新版卖家中心 Bigpipe 实践(二)
参考文章
新版卖家中心 Bigpipe 实践(一)
新版卖家中心 Bigpipe 实践(二)
HTTP 协议中的 Transfer-Encoding
HTTP 协议入门