首屏加载优化

概述

页面加载的方式包括以下几种:

  1. 直接同步加载
    一次把服务端的渲染内容加载到浏览器,当页面内容比较少时可以考虑这种方式。
  2. 滚动同步加载
    服务端渲染首屏的内容,其他屏幕的内容放到textarea或者注释中,滚动时再渲染其他屏的内容,此种比较适合。
  3. 异步加载
    服务端渲染主 layout ,加载到客户端,通过 AJAX 获取其他页面内容,然后在客户端渲染。此种和淘宝无线 H5 的方案类似。
  4. 滚动异步加载
    服务端渲染首屏内容,加载到客户端,滚动时再通过 AJAX 获取次屏内容
  5. 分块加载
    服务端支持 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,请求很快完成,只是浏览器能不能正常获取到内容。
如不手动断开连接,会有以下三种情况:

  1. 不添加Content-Length,客户端会一直等待;
  2. Content-Length设置的长度小于或等于实际报文长度,请求顺利完成,但是内容不全;
  3. 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 协议入门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,010评论 19 139
  • 本文转自:直播中的首屏加载优化 | www.samirchen.com 直播中的首屏加载时间指的是进入直播间时从播...
    SamirChen阅读 1,324评论 0 4
  • 用vue-cli打包创建的webapp工程,index.html中不包含dom内容结构,都是在js中,如下图 仅仅...
    fangdown阅读 1,513评论 2 3
  • https://segmentfault.com/a/1190000010042512
    程序员小布的养生之道阅读 803评论 0 47
  • 今天最后一次换药,把账结了,竟然有2000多,汗哪~ 娃跟哥哥玩了一天,俩娃作了个底朝天~今天的重头戏是~过油~从...
    圈_圈_阅读 188评论 0 0