最常被遗忘的 Web 性能优化:浏览器缓存
Posted SegmentFault
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最常被遗忘的 Web 性能优化:浏览器缓存相关的知识,希望对你有一定的参考价值。
一提起缓存, Web
开发者们总是在想数据库缓存、页面静态化、使用 Redis
内存缓存。这些方法都有一个共性,就是集中在后台,目的就是加快数据的读取,少用比较容易产生瓶颈的部分。
后台该优化的都优化到了最佳状态,却往往疏忽了一个非常重要的过程,就是数据传输。想着如何快速读取数据,却忘了如何减少请求数据,或者根本不请求数据。所以,今天我们就来聊一聊这个经常被我们遗忘的浏览器缓存。
认识浏览器缓存
当浏览器请求一个网站的时候,会加载各种各样的资源,比如 html
文档、图片、 CSS
和 JS
等文件。对于一些不经常变的内容,浏览器会将他们保存在本地的文件中,下次访问相同网站的时候,直接加载这些资源,加速访问。
这些被浏览器保存的文件就被称为缓存。(不是指 Cookie
或者 Localstorage
)。
那么如何知晓浏览器是读取了缓存还是直接请求服务器?我们就使用 Segmentfault
网站来做个示例(见下图)。
第一次打开该网站后,如果再次刷新页面。会发现浏览器加载的众多资源中,有一部分 size
有具体数值,然而还有一部分请求,比如图片、 css
和 js
等文件并没有显示文件大小,而是显示了 fromdis cache
或者 frommemory cache
字样。这就说明了,该资源直接从内存或者本地硬盘直接读取,而并没有请求服务器。
查看缓存
至于背后的文件,一般存在于: C:\Users\yanying\AppData\Local\Google\Chrome\UserData\Default\Cache
路径中,其中 yanying
是你的 windows
用户名称。
缓存协商
从上面的图片可以看出。一部分请求使用了缓存,而有一部分缓存并没有使用缓存。浏览器如果想判断何时该做什么操作,就必须要有一个判定标准。这里就需要用到缓存协商。简单来说就是 Web
浏览器和服务器之间协定一个法则,什么情况下请求资源,什么情况下不请求。
缓存协商方式和 Cookie
、 User-Agent
一样,通过浏览器 header
进行传输。
缓存协商方式有3种:
Last-Modified
ETag
Expires
Last-modified
定义
Last-Modified
标签代表是文件的最后修改时间,其格式是标准的 GMT
时间。注意: GMT
是标准的格林威治时间,我们国家是 GMT+8
时区。所以,你看到的 Last-Modified
和我们的时间有8个小时差距,不过不影响使用。
一般的动态资源没有所谓的最后修改时间。而静态文件比如 css
文件、图片等文件可以通过 stat()
系统调用获得文件的最后修改时间。
但是,实际网站运行中, Web
服务器(比如 Apache
)会自动获取静态资源的最后修改时间,同时会自动在 HTTP
头文件中添加 Last-Modified
标签。静态资源的相应头文件如下图所示:
包含了 Last-Modified
标签的资源,在下次的请求中,浏览器会带着该时间。当服务器接收到请求后会核对该时间后,文件是否被修改,如果修改了就直接返回数据,没有修改就直接返回 304
状态码,告知浏览器直接使用本地缓存。这样,一次数据传输流量就被免除了,速度稍有加快。
动态资源中使用
动态资源虽然没有相对意义上的最后修改时间,但是我们还是可以直接通过发送 header
头来手动定义 Last-Modified
。这样,通过动态程序判断,也可以达到静态资源节省数据传输流量的作用。
这里使用 php
举个例子:
1、首先创建一个 php
文件,发送一个 Last-Modified
头标签:
<?php
header("Last-Modified:".gmdate("D, d M Y H:i:s")." GMT");
2、使用浏览器请求该文件,我们得到了如下的服务器返回头:
HTTP/1.1 200 OK
Date: Tue, 27 Jun 2017 15:13:02 GMT
Server: Apache/2.4.9 (Win32) PHP/5.5.12
X-Powered-By: PHP/5.5.12
Last-Modified: Tue, 27 Jun 2017 15:13:02 GMT
Content-Length: 0
Keep-Alive: timeout=5, max=97
Connection: Keep-Alive
Content-Type: text/html
观察上面服务器返回的头文件,包含了一个 Last-Modified:Tue,27Jun201715:13:02GMT
,这就是上面动态代码生成的最后修改时间。
3、当我们再次请求该文件的时候,我们看下浏览器发送给服务器的头文件。
GET /php/last.php HTTP/1.1
Host: localhost
Connection: keep-alive
Cache-Control: max-age=0
//...这里省略部分信息
If-Modified-Since: Tue, 27 Jun 2017 15:13:02 GMT
观察一下最后一行,多了一个 If-Modified-Since
标签,他的时间正是服务器刚刚返回的 Last-Modified
的值。这个值就这样又被返回给了服务器。
4、这样就很简单啦。在动态语言端( PHP
)可以直接使用 $_SERVER['HTTP_IF_MODIFIED_SINCE']
即可获取时间值,接着就可以做一些简单的对比工作。如果在这个时间之后数据没有变化则直接返回 304
,告诉浏览器直接使用缓存,而免去数据传输的过程。
而且,最终要的是。这个过程根本无需查找数据库,所以后台程序执行时间非常短,从而大大减少用户等待时间。
这样我们就做到了动态资源也可以实现静态资源的最后修改时间,从而减少数据传输量,达到优化性能要求。
Etag
Last-Modified缺点
Last-Modified
似乎已经做到了部分性能优化效果。但是,总是有些情况下不是很奏效。比如,一个用户修改了一个文件,后来用户觉得修改错误,于是又修改回去。
上面的过程中,文件内容并没有发生变化。但是,文件在系统中的物理最后修改时间却发生了变化。这种情况下,如果浏览器再次请求资源。服务器还是会发送完整数据。从而并未完全达到我们预想的效果。
于是在此之上,我们还可以添加一个 ETag
标签,用来进一步确认文件是否修改。
了解ETag
ETag
类似于 Last-Modified
,也是一个 header
头标签。他的值是一串字符串,用于区分各个文件的版本信息,由于 HTTP
并没有对该值做任何的格式限制,所以可以自定义生成。
ETag
的值不同于 Last-Modified
,他并不会在文件被修改时候就发生变化,而是在文件内容发生变化的时候才会被改变(具体什么时候改变,完全有后台业务逻辑来判断)。对于静态资源, Web
服务器还是会帮我们处理好这个标签,不用考虑太多。
这里我们截取了 Segmentfault
的一张图片的 ETag
,如下图:
下面我们还是来讨论一下动态资源模拟静态资源发送 ETag
标签的过程:
1、这里我们还是新建一个 PHP
文件,其中代码是向浏览器发送一个包含 ETag
的头文件。
<?php
header("ETag : abcd");
2、使用浏览器请求该 PHP
文件:
看下服务器返回的 header
头:
HTTP/1.1 200 OK
Date: Wed, 28 Jun 2017 01:45:40 GMT
Server: Apache/2.4.9 (Win64) PHP/5.5.12
X-Powered-By: PHP/5.5.12
ETag: abcd
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html
里面比正常的返回多了一个 ETag
标签,并且它的值就是我们刚刚设置的 abcd
3、下面我们刷新浏览器,再次请求改页面:
注意观察下浏览器请求的头 header
:
GET /etags.php HTTP/1.1
Host: localhost
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8
Cookie: Phpstorm-65418376=dceeb07b-c7af-45d6-b8be-4079e9424244; Hm_lvt_65dfcf8f1948f7203dd3fb620de01083=1497600508; admin_id=1; admin_token=072517cddaa9c106fe4662ea70a1345c
If-None-Match: abcd
仔细看下最后一行,有一个 If-None-Match
头标签。该标签的值正是我们刚刚接收到的服务器返回的 ETag
的值,这样类似于 Last-Modified
,我们在 PHP
端可以使用 $_SERVER['HTTP_IF_NONE_MATCH']
直接获取我们刚刚的值。
4、获取到该值,我们就可以直接对比文件现有的 ETag
,来决定是直接使用浏览器缓存还是再次发送完整数据。
小结
Etag
和 Last-Modified
非常相似,都是用来判断一个参数,从而决定是否启用缓存。但是 ETag
相对于 Last-Modified
也有他的优势,他可以更加准确的判断文件内容是否被修改,从而在实际操作中实用程度也更高。
有了这两种优化方式,对于节省流量带宽已经起到了非常大的作用。但是总是感觉还是有点儿鸡肋,毕竟每次浏览器还是要来询问一下服务器,文件是否被改变。
如果,我们可以确定,一个文件在半年内不会改变,那么我们可以让浏览器在这半年时间内都不来服务器询问,而直接使用本地缓存。这里就需要使用第三种协商方式 Expires
.
Expires
Expires
这个单词的意思是过期,在这里表示的是过期时间。它的使用方式、格式和 Last-Modified
一样,都是使用浏览器头,也都是标准的 GMT
时间。
但是它的功能却完全不同,包含了 Expires
头标签的文件,就说明浏览器对于该文件缓存具有非常大的控制权。例如,一个文件的 Expires
值是2020年的1月1日,那么就代表,在2020年1月1日之前,浏览器都可以直接使用该文件的本地缓存文件,而不必去服务器再次请求该文件,哪怕服务器文件发生了变化。
所以, Expires
是优化中最理想的情况,因为它根本不会产生请求,所以后端也就无需考虑查询快慢。
下面我们看下 segmentfault
的静态文件的 Expires
:
对于静态资源,大多数服务器是会开启 expires
标记功能。如果遇到没有开启的,则可以使用配置文件开启。
Apache
的 expires
支持设置如下:
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType image/gif "access plus 1 month"
ExpiresByType text/css "now plus 2 day"
ExpiresDefault "now plus 1 day"
</IfModule>
上面的配置中我们设置 image/gif
的格式图片缓存时间为1个月,而 css
文件缓存时间为2天,其他的默认为1天。
另外,对于常用静态资源。如果不在 web
服务器端设置 expires
标签,浏览器也可以智能的标记一个过期时间。比如 gif
图片,浏览器会设置他的过期时间为永不过期。
动态资源中使用Expires
这里我们还是拿 PHP
来举例
1、首先创建一个 PHP
文件,用于发送 Expires
头标签。
这里我们把文件过期时间直接设置为2020年1月1日的0点
<?php
header("Expires:".gmdate("D, d M Y H:i:s",1577808000)." GMT");
2、使用浏览器请求该文件,观察服务器返回的头文件:
HTTP/1.1 200 OK
Date: Wed, 28 Jun 2017 02:24:18 GMT
Server: Apache/2.4.9 (Win64) PHP/5.5.12
X-Powered-By: PHP/5.5.12
Expires: Tue, 31 Dec 2019 16:00:00 GMT
Content-Length: 0
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html
不出意外,我们已经在头文件里面发现 Expires
标签,并且它的值为 Tue,31Dec201916:00:00GMT
(这里不是2020年原因是由于有8个小时时差)。
3、再次使用浏览器访问改页面,发现浏览器的请求数据的路径已经变为了 fromcache
(如下图)。
请求方式与缓存
浏览器有3种请求服务器资源的方式
ctrl+f5
:强制刷新f5
:刷新页面
这3种请求方式对于资源使用缓存的影响各不不同,下面一一的解释:
1、ctrl + f5:强制刷新
2、f5:刷新页面
f5
刷新页面相当于浏览器上面的刷新按钮,是一种比较常用的刷新方式。这种方式下浏览器会使用部分必要的缓存,针对于 Last-Modified
有效,但是 expires
标签就会失去他的作用。
3、地址栏转到方式
cache-control
还有一点点小缺陷
了解了上面所有的缓存协商方式后,我们已经可以高效的优化我们现有的应用。但是还是存在一种可能情况,那就是之前的 Last-Modified
和 expires
都是使用服务器标准时间来标记。
而作为最后的判断者确是浏览器。所以,难免会存在用户电脑时间和服务器时间不一致的情况。
比如我们设定一个资源在未来10分钟内不会过期,而用户电脑比服务器时间快了1个小时(当然这个太少见)。那么我们设置的过期时间对于用户来讲,立即就过期了。那么我们的设置相当于白用功了。
所以为了解决这个可能出现的小缺陷,我们还可以设置一个相对于用户本地时间的缓存过期时间 cache-control
。
作用
cache-control
和之前的 Last-Modified
一样,都是头文件里面的一个标签。只不过他的值是 max-age=<second>
,这里的 <second>
是一个数字,单位为秒。
假设我们设置一个值 cahce-control:max-age=3600
,那么就代表改缓存有效期是用户本地时间加上 3600
秒。这样,缓存的截止时间就和服务器时间没有太大关系了,从而避免了因为时间偏差带来的不良影响。
对于静态文件,如果服务器比如 Apache
开启了 expires
功能,那么也会默认的给头文件添加一个 cache-control
标签。
PHP设置cache-control
对于动态文件,我们可以在程序语言中向浏览器直接输出该标签。我们使用 PHP
做一个演示:
1、创建一个 PHP
文件,向浏览器输出一个包含 cache-control
标签的头:
<?php
header("Cache-Control:max-age=3600");
2、使用浏览器请求该 PHP
文件,获取服务器返回头 header
:
HTTP/1.1 200 OK
Date: Wed, 28 Jun 2017 12:33:16 GMT
Server: Apache/2.4.9 (Win32) PHP/5.5.12
X-Powered-By: PHP/5.5.12
Cache-Control: max-age=3600
Content-Length: 0
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Content-Type: text/html
观察上面的信息,可以发现其中包含 cache-control
标签,其值为我们刚刚设置的 max-age=3600
,那么就代表相对于我本地时间 3600
秒之后缓存过期。(完)
严颖,2017.6.28
个人主页:segmentfault (https://segmentfault.com/u/yanying)
插播一则官方消息
SegmentFault 官方成立了「SF.GG 广州技术交流群」。
群成员:广州地区的程序员,不限技术领域。
群定位:大家可自由在群内讨论、分享技术内容及同城职位推荐,管理猿也会定期分享技术内容。当然,这里也是你结交同城程序猿/媛的地方,成员可自由组织线下约饭、交流。
扫码添加管理员的微信,添加请备注:SF用户名+广州技术交流群,管理员会邀请你入群 lol
以上是关于最常被遗忘的 Web 性能优化:浏览器缓存的主要内容,如果未能解决你的问题,请参考以下文章