在互联网应用中WEB页面的打开速度,在用户体验中占用极其重要的地位,根据HubSpot所做研究的显示:如果Yahoo将页面加载时间减少0.4秒,流量可能会增加9%;页面慢1秒可能会使亚马逊每年损失16亿美元的销售额;Bing搜索的2秒延迟将导致每位访客的收入损失4.3%,点击量减少3.75%,查询量下降1.8%。根据上面的数据可以看出,Web页面的加载时间对网站具有重要的意义。
Web页面一般包括两种资源:静态资源与动态资源。静态资源主要包括静态html、图片、js、css、视频等资源,这些资源基本不会频繁发生变化,对所有用户也基本是一致的。动态资源主要为针对每个用户动态显示的数据信息,例如:用户信息、登录情况、用户数据、账户余额、账户明细等。动态资源的处理时间主要依赖后台应用、数据库的处理性能,可通过提高服务器性能、优化业务处理逻辑来解决,一般需要针对具体的问题来具体分析。本文将主要讨论静态资源的加速技术。
在一段时间内打开一个网站浏览其中的多个页面,其中会有大量相同的静态资源请求,由于静态资源变化频率较低,可以缓存之前已经下载的资源,以备后续的访问请求。
我们打开浏览器访问一个页面,在开发者模式下可以看到如下信息:
打开这个页面一共发送了303个请求,传输了21.6M的数据,总的耗时为11.48秒,整体来看这是一个很慢的页面(由于用了异步请求,所以在用户体验上还没有那么糟糕)。
再次使用F5刷新页面,可以看到如下信息:
这次是300个请求,但是传输的数据量只有369K,整体耗时为6.5秒,加载仅有2.5秒,这次在体验上就非常好,需要页面显示的内容很快就显示出来了,这些就是资源缓存的效果。在开发者模式的Network项下,可以看到大量耗时为0ms,size为(memory cache)的请求,标识这些请求,并没有真正的发送到服务器,而是直接在浏览器的内存缓存中读取,因此他的响应时间是0ms,非常的快!
另外还存在一些size为(disk cache)的请求,这些请求也没有真正发送到服务器,而是在浏览器在磁盘的缓存中读取的,由于磁盘的速度比不上内存,因此他会有个几十到几百的一个处理时间,对于什么情况下将请求缓存到内存,什么情况下将请求缓存到硬盘,则是由浏览器自己的内部来决定的。在一般情况下,较小的资源会缓存到内存,较大的资源会缓存到磁盘。
还存在一些返回码为304大小为几十到一百个字节的请求。这些请求的资源在本地具有缓存,但是缓存内容已过期,浏览器不确认是否需要更新,因此向服务器发起请求。服务器在确认资源没有过期后将返回不带报文体的为返回码304的报文。这种情况下会减少文件资源的传输,降低带宽的利用,但是由于仍然需要向服务器发起一次请求,导致处理时间在几十毫秒到几秒不等。
那浏览器是如何确定哪些资源是可以缓存,缓存后的资源什么时候过期,在什么情况需要更新本地缓存资源呢?这些都是通过HTTP 协议报文头来决定的。缓存的方式可以分为两种类型:本地缓存、协商缓存。本地缓存是直接利用本地缓存的资源,不会再向服务器再次发送请求。协商缓存是根据本地浏览器最后一次下载资源的时间、标签等信息,向服务器发送下载请求,如果服务器认为本地资源已经是最新的了,则仅仅返回http响应头,响应代码是304,浏览器将直接使用本地资源,否则服务器将按正常的响应返回最新的资源,响应代码是200。这两种缓存不并不是互斥的,而是相辅相成的,浏览器会优先使用本地缓存,如果本地缓存已过期将通过协商缓存确认缓存内容的有效性并更新缓存。
可缓存的资源一般是静态资源,但是浏览器并不是依据资源类型来判断是否可以进行缓存的,浏览器是根据响应码来判断是否可缓存,只有成功的响应才会被缓存,成功的响应码包括:200(OK)、203( Non-Authoritative Information)、206(Partial Content)、300(Multiple Choices)、301(Moved Permanently)、410(Unauthorized),另外可缓存一般是GET请求,POST请求一般不会做缓存。
对于资源的是否可以缓存、缓存的方式、时间,是通过HTTP报文头来决定的。涉及的HTTP 报文头属性包括:Expires、Pragma、Cache-Contrl、Last-Modified/If-Modified-Since、ETag/If-None-Match。
Expires:指定一个绝对时间,在这个时间之后缓存中的资源将会过期。
例如:
Expires: Tue, 04 May 2022 22:00:00 GMT
标识资源文件将会在2022年4月5日星期二 22点过期,过期之前浏览器将直接使用内存或硬盘中的缓存,过期后浏览器将再次发起请求获取新的资源。
Pragma:是一个通用定义,目前只存在一个标准的定义 no-cache,使用方式如下:Pramgma:no-cache
用在请求头中,表示强制要求缓存服务器将请求提交到源服务器,这个一般用在代理服务器上,当我们使用Ctrl+F5刷新浏览器时,在请求头上会自动带上这个属性;
用在响应头中,要求浏览器对缓存的信息在使用时需要服务器评估缓存的有效性,也就是需要将文件的属性信息(文件修改时间、ETag)发送到服务器,如果文件资源有更新则正常返回,否则返回304告知浏览器缓存是有效的。因此这里的no-cahce并不是不缓存,而是在使用前需要验证。如果打算禁止浏览器缓存,则需要使用HTTP 1.1协议中新增的Cache-Control。
Pragma和Expires是HTTP 1.0中的标准,在HTTP 1.1中新增了Cache-Control,其功能要更加丰富,常用到的功能定义包括:Public:响应可以被任何对象缓存,即使是通常不可以缓存的内容;
Private:响应只允许被单个用户缓存,不能作为共享缓存(通常指的是代理服务器);
no-cache:同Pramgma: no-cache;
no-store:这个是我们通常理解的浏览器不要缓存数据,这时任何请求和响应都不会被缓存,新的请求都不会在缓存中读取,需要重新向服务器发起请求;
max-age:指定资源在缓存中的最大时间周期(单位为秒),超过这个时间周期缓存将失效。
Cache-Control: public, max-age=864000标识这个资源可以被任意缓存,缓存周期为10天;
Cache-Control: no-store标识这个资源不能被浏览器缓存,因为对他的访问每次都可能发生变化。
Cache-Control: privete, no-cache, max-age=0标识这个资源可以被缓存,但是会立即过期,下次访问时需要向服务器发起查询确认。
通过Cache-Control根据资源的变化情况,可以灵活定义浏览器哪些数据可以缓冲、缓存的时长。但是被缓存的资源一般都是较少发生变化的,大多数情况下过来缓存期后,服务器上的资源也不会发生变化,这个时候也不需要在服务器重新下载全部资源。这个时候就用到了协商缓存。根据HTTP 1.0协议要求,对响应头里面需要包含:Last-Modified,标识当前资源文件的最后修改时间,使用格式为:Last-Modified: Tue, 04 May 2022 22:00:00 GMT
浏览器在缓存资源时,同时会存储这个时间,当本地缓存过期后再次发起请求,需要在请求头中包含If-Modified-Since,格式为:Last-Modified-Since: Tue, 04 May 2022 22:00:00 GMT
服务端在接收到这个请求后,将会用这个时间同本地文件修改时间进行比较,如果本地文件有更新,将正常返回完成的报文;如果本地的文件的更新时间没有发生变化,则返回响应码为304的响应头,告诉浏览器服务器文件未发生变化,节省整个资源文件的下载时间和流量。
在使用Last-Modified/If-Modified-Since时,需要注意的几个问题是:
1)时间是精确到秒级的,如果在一秒只能发生多次变化,这种方式是无法识别的,因此不适合变化频次较高的资源,需要保障变化的间隔时间大于1秒;
2)在负载均衡的多服务器上发布时,需要保证各个服务器上的文件修改时间是一致的,这种情况一般需要通过tar包释放的方式发布;如果直接向各个服务器直接copy文件,极有可能各个服务器的时间不一致,导致资源文件的更新判断错误,从而出现重复下载的情况。
HTTP 1.1 协议中新增的ETag/If-None-Match则是通过另外一个方式来确定服务器端的文件是否发生变化。Etag(Entigy Tag)是根据文件的属性生成的一个ID,这些属性可以包括:inode、修改时间、文件大小、等文件属性信息,不同的WEB服务器可以实现自己的ID生成策略,通过这个Etag来代替单一的文件修改时间来标识文件是否发生变化。这样可以解决Last-Modified无法解决的一些问题:
1)有些文件可能会周期性的被发布,但是他的内部不一定发了变化,仅仅是由于全量发布更新了修改时间,这个时候我们不希望客户认为文件发生了更新而重新下载;
2)某些文件修改比较频繁,修改时间间隔小于1秒,这种修改Last-Modified/If-Modified-Since无法识别。
同时ETag对于集群部署的服务也存在同Last-Modified一样的问题,相同的文件在不同的服务器上可能生成的Etag是不同的,因为除了文件的大小外修改时间、Inode可能在不同的服务器上都不一样,这时可以自定义ETag的产生方式,保证同一文件在不同服务器上ETag的一致。
浏览器和服务器之间可以通过缓存技术加速静态资源的访问,但是由于当前随着客户端界面的丰富和多样化,需要下载、处理的静态资源会原来多,存在同时需要下载大量资源文件的情况,这时也会导致客户端资源加载缓慢和用户体验的下降。
通过上图可以看到,在下载js文件时每个文件的用时达到1~2秒(这个是在内网环境,因此下载速度慢绝对不是网络环境导致的).
查看在1~2秒的详细用时,可以看到整体用时1.76秒,Stalled时间为1.67秒,真正下载的时间0.09秒,绝大部分时间都消耗在Stalled上面,根据Chrome的说明出现Stalled状态可能是由于以下原因:
有更高级别的请求;
对同一个请求源服务器已经打开了6个链接;
浏览器正在分批磁盘缓存。
也就是说浏览器为了避免对后台服务器带来特别大的冲击,最多同时只能打开6个TCP链接,在这6个链接里面,所有的资源文件只能顺序下载,其他多余的资源下载请求只能等待排队。
因此解决办法只有减少下载请求数量的次数,具体的方法就是对需要下载资源文件进行合并。Js、css文件的合并方式可以为将同类型、同功能、同稳定性的资源合并到一个文件。对于存在大量小图片资源的需求,可以将大量的小图片合并为一个大图片一次性下载、缓存,在使用时通过程序只截取需要的部分,需要注意的是这种优化虽然可以加快资源下载速度,但是可能会增加程序设计的复杂度和浏览器的CPU消耗,这两方面的优略需要根据实际情况进行取舍。
前面介绍的浏览器缓存技术,主要是通过浏览器缓存已经访问过的文件资源,对下次的访问起到加速的作用。但是在浏览器第一次访问网站时还是需要下载大量的资源,如果服务器位于广州而用户位于新疆,那整个资源的下载过程还是比较慢的,因为仅仅一次网络传输的时间将达到数十毫秒,一次网页访问会包括成百上千次的网络传输,仅网络传输时间就会达到秒级。
这时候就需要用到CDN技术,一种分布式的网络资源缓存技术,它既能加速网站静态资源的下载,又能极大节省互联网带宽。CDN的全称是Content Delivery Network,即内容分发网络,其主要功能是将一个站点上的各种静态资源,分发到分布在全国各地的节点上,使用户可以就近访问所需资源,从而缩短用户下载静态资源的延时,提高用户访问网站的响应速度以及网站的可用性。
CDN会在全国乃至全球部署缓存服务器,并提供多种网络运营商接入方式。当客户端根据域名发起请求时,DNS服务器会根据客户端发起请求的IP地址解析出理论上距离最近、访问速度最快的CDN网络中的缓存服务器IP地址。
CDN缓存服务器缓存的资源内容的更新策略主要包括主动更新和被动更新。主动更新就是在源服务器发生内容更新后,主动通知CDN服务器进行资源更新,CDN将扫描源站服务器下指定目录的所有资源,更新本地的缓存服务器。被动更新就是在客户端在访问CDN服务器时,如果CDN缓存服务器发现在本地缓存中不存在,则会向源站服务器发起回源请求,CDN缓存服务器在获取最新资源后再保留到本地缓存服务器。
上面介绍的WEB服务器静态资源加速技术,主要从技术层通过缓存的方式加速静态资源的下载,但是对越来越丰富多样的页面展示来说,需要下载的资源也越来越多,缓存技术也不能完全达到满意的效果,这时就需要结合延迟加载技术。页面采用分批、逐步渲染的过程,先将一部分内容显示出来,后台同时异步下载其他非必要资源,使得浏览器先给用户展示出部分信息,而不至于无聊的等待,从而能给用户带来更好的使用体验。另外,还有许多其他的用户体验优化方法,需要相互结合使用来提高系统的可用性、易用性。