前言
对于页面中静态资源(html/js/css/img/webfont),理想中的效果:
-
页面以最快的速度获取到全部必须静态资源,渲染飞快;
-
服务器上静态资源未更新时再次访问不请求服务器;
-
服务器上静态资源更新时请求服务器最新资源,加载又飞快。
静态资源加载速度引出了咱们今天的主题,由于最直接的方式就是将静态资源进行缓存。页面渲染速度创建在资源加载速度之上,但不一样资源类型的加载顺序和时机也会对其产生影响,因此也留给了咱们更多的优化空间。
固然除了速度,缓存还有另外2大功效,减小用户请求的带宽和减小服务器压力。
常见缓存类型
对于前端而言,这多是咱们最容易忽略的缓存类型,缘由在于大部分设置都在服务器运维层面上进行,不属于前端开发的维护范围。但静态资源的内容更新时机其实前端是最清楚的,若是能在理解浏览器缓存策略的基础上合理配置效果最佳。
浏览器缓存策略通常经过资源的Response Header来定义,html文件在很早以前的规范里也能够经过Meta标签的http-equiv来定义。
可在w3c的官方文档中查看全部HTTP Response Header字段的定义,跟缓存相关的主要有上图中被圈出来的几个:前端
-
- public:响应被缓存,而且在多用户间共享。
- private:默认值,响应只可以做为私有的缓存(e.g., 在一个浏览器中),不能再用户间共享;
- no-cache:响应不会被缓存,而是实时向服务器端请求资源。
- max-age:数值,单位是秒,从请求时间开始到过时时间之间的秒数。基于请求时间(Date字段)的相对时间间隔,而不是绝对过时时间;
注:HTTP/1.0 没有实现 Cache-Control,因此为了兼容HTTP/1.0出现了Pragma字段。
-
Pragma: 只有一个用法Pragma: no-cache,它和Cache-Control:no-cache做用如出一辙。(Cache-Control: no-cache是http 1.1才提供的, 所以Pragma: no-cache可使no-cache应用到http 1.0 和http 1.1。)
-
Expires:指定了在浏览器上缓冲存储的页距过时还有多少时间,等同Cache-control中的max-age的效果,若是同时存在,则被Cache-Control的max-age覆盖。若把其值设置为0,则表示页面当即过时。而且若此属性在页面当中被设置了屡次,则取其最小值。
注:这个规则容许源服务器,对于一个给定响应,向 HTTP/1.1(或以后)缓存比 HTTP/1.0 提供一个更长的过时时间。
-
Date:生成消息的具体时间和日期;
-
Last-Modified/If-Modified-Since:本地文件在服务器上的最后一次修改时间。缓存过时时把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比,若是时间一致,那么返回304,客户端就直接使用本地缓存文件。
-
Etag/If-None-Match:(EntityTags)是URL的tag,用来标示URL对象是否改变,通常为资源实体的哈希值。和Last-Modified相似,若是服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。Etag的优先级高于Last-Modified,Etag主要为了解决 Last-Modified 没法解决的一些问题。
- 文件也许会周期性的更改,可是他的内容并不改变,不但愿客户端从新get;
- If-Modified-Since能检查到的粒度是s级;
- 某些服务器不能精确的获得文件的最后修改时间。
本地缓存过时后,浏览器会像服务器发送请求,request中会携带如下两个字段:
其中在图右侧的“file modified?”判断中,服务器会读取请求头这两个值,判断出客户端缓存的资源是否最新,若是是的话服务器就会返回HTTP/304 Not Modified响应头,但没有响应体。客户端收到304响应后,就会从缓存中读取对应的资源;不然返回HTTP/200和响应体。
meta是html语言head区的一个辅助性标签,其中的http-equiv字段定义了服务器和用户代理的一些行为。在以前的规范中,meta的http-equiv字段中有如下值与http header缓存相关的字段功能相似。
-
Cache-Control
-
Pragma
-
Expires
<meta http-equiv="Cache-Control" content="no-cache" /> <!-- HTTP 1.1 -->
<meta http-equiv="Pragma" content="no-cache" /> <!-- 兼容HTTP1.0 -->
<meta http-equiv="Expires" content="0" /> <!-- 资源到期时间设为0 -->复制代码
但如今
w3c的规范字段中这些值已经被移除,一个很好的理由是:
Putting caching instructions into meta tags is not a good idea, because although browsers may read them, proxies won't. For that reason, they are invalid and you should send caching instructions as real HTTP headers.
其实也很好理解,写在meta标签中表明必须解析读取html的内容,但代理服务器是不会去读取的。大多浏览器已经再也不支持,会忽略这样的写法,因此缓存仍是经过HTTP headers去设置。
注:HTTP Headers中的缓存设置优先级比meta中http-equiv更高一些。
二、HTML5 Application Cachehtml5
Application Cache是html5引入的本地存储方案之一,能够构建离线缓存。目前除IE10-外其余浏览器均支持。
application cache是经过mannifest文件来管理的,manifest文件是简单的文本文件,内容是须要被缓存供离线使用的文件列表,及不须要被缓存或读取缓存失败的文件控制。
mannifest文件可使用任意拓展名,但须要在服务器中添加MIME类型匹配,使用apache比较简单,若是使用.manifest做为拓展名在apache配置文件中添加。
AddType text/cache-manifest .appcache复制代码
<html lang="zh" manifest="main.manifest">复制代码
注:千万不要把manifest文件自己放在缓存文件列表中,否则浏览器没法更新manifest文件文件,最好在manifest文件的http headers中设置其当即过时。
-
Creating Application Cache with manifest(访问到带manifest属性的html文件,将manifest文件存储,加载html文件及其余资源文件);
-
Application Cache Checking event(检查要缓存的文件列表)
-
Application Cache Downloading event(开始下载缓存文件)
-
Application Cache Progress event (0 of 4)(依次下载缓存文件)
-
……
-
Application Cache Progress event (4 of 4)
-
Application Cache Cached event(文件缓存完毕)
-
Document was loaded from Application Cache with manifest(从缓存中读取html文件和其余静态资源文件,供页面展现)
-
Application Cache Checking event(获取新的manifest文件,检查是否更新)
-
Application Cache Obsolete event(删除本地缓存中的全部文件,再也不使用缓存)
-
Application Cache会默认缓存引用manifest文件的HTML文档,对于动态更新的html页面来讲是个坑(可使用tricky的iframe嵌入方式来避免);
-
只要缓存列表中的一个资源加载失败,全部文件都将缓存失败;
-
若是资源没有被缓存,而又没有设置NETWORK的状况下,将会没法加载,因此Network中必须使用通配符配置;
-
缓存更新后第一次只能加载manifest文件,其余静态资源须要第二次加载才能看到最新效果;
-
缓存文件清单中的文件自己更新浏览器是不会从新缓存,那怎么告诉浏览器缓存须要更新了呢?
- 更新manifest文件:修改注释的版本号或者日期。
- 经过Application Cache提供的接口(window.applicationCache.swapCache)来检查更新。
还有最后一个问题,该标准已经从 Web 标准中删除……
该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在将来的某个时间中止支持,请尽可能不要使用该特性。在此刻使用这里描述的应用程序缓存功能高度不鼓励; 它正在处于从Web平台中被删除的过程。请改用
Service Workers 代替。
三、PWA(Service Worker)apache
PWA全称为“Progressive Web Apps”,渐进式网页应用,Service Worker是其几大核心技术之一。
Service worker is a programmable network proxy, allowing you to control how network requests from your page are handled.
没错,这就是官方建议替代Application Cache的方案。早在2014年,W3C就公布了Service Worker的草案。它做为一个独立的线程,是一段在后台运行的脚本。它的出现使得web app也能够具备相似native app的离线使用、消息推送、后台自动更新等能力。
一、首先,要使用Service Worker,须要添加一个Service Worker的js的文件,而后在咱们的html页面中注册对这个文件的引用。
<script>
navigator.serviceWorker
.register('./sw.js')
.then(function (registration) {
// 注册成功
});
</script>复制代码
二、其次,咱们在js文件中补充Service Worker的生命周期事件。Service Worker生命周期有三部曲:注册,安装和激活。
self.addEventListener('install', function(event) {
/* 安装后... */
// cache.addAll:把缓存文件加进来,如a.css,b.js
});
self.addEventListener('activate', function(event) {
/* 激活后... */
// caches.delete :更新缓存文件
});
self.addEventListener('fetch', function(event) {
/* 请求资源后... */
// cache.put 拦截请求直接返回缓存数据
});复制代码
对于获取文件和缓存文件,Service worker依赖了两个 API:
Fetch (经过网络从新获取内容的标准方式) 和
Cache(应用数据的内容存储,此缓存独立于浏览器缓存和网络状态)。
index.html文件中引用了static/js/main.js,main.js中注册了service-worker.js。service-worker.js中咱们能够看到有 precacheConfig(缓存列表)和 cacheName(版本号)两个变量。断开网络,咱们看到precacheConfig列表中的文件仍能从本地加载。
以注册文件为service-worker.js为例,每次访问ServiceWorker控制的页面,浏览器都会加载最新的service-worker.js文件,跟当前service-worker.js文件对比,只要内容有任何不一样,浏览器都会获取并安装新文件。可是不会当即生效,原有的ServiceWorker仍是会运行,只有当ServiceWorker控制的页面所有关闭后,新的ServiceWorker才会被激活。
LocalStorage虽是浏览器端缓存一种,但有多少人会用它来缓存文件呢?首先缓存读取须要依靠js的执行,因此前提条件就是可以读取到html及js代码段;其次文件的版本更新控制会带来更多的代码层面的维护成本,因此LocalStorage更适合关键的业务数据而非静态资源。
这是一种以空间换时间的方案,减小了用户的访问延时,也减小的源站的负载。
客户端浏览器先检查是否有本地缓存是否过时,若是过时,则向CDN边缘节点发起请求,CDN边缘节点会检测用户请求数据的缓存是否过时,若是没有过时,则直接响应用户请求,此时一个完成HTTP请求结束;若是数据已通过期,那么CDN还须要向源站发出回源请求。
CDN边缘节点缓存策略因服务商不一样而不一样,但通常都会遵循http标准协议,经过http响应头中的Cache-control: max-age的字段来设置CDN边缘节点数据缓存时间。另外可经过CDN服务商提供的“刷新缓存”接口来更新缓存。
prebrowsing
预加载是浏览器对未来可能被使用资源的一种暗示,一些资源能够在当前页面使用到,一些可能在未来的某些页面中被使用。做为开发人员,咱们比浏览器更加了解咱们的应用,因此咱们能够对咱们的核心资源使用该技术。
经过prebrowsing能够提早缓存部分文件,可做为一种静态资源加载优化的手段。prebrowsing有如下几种:
-
dns-prefetch:DNS预解析,告诉浏览器将来咱们可能从某个特定的 URL 获取资源,当浏览器真正使用到该域中的某个资源时就能够尽快地完成 DNS 解析。多在使用第三方资源时使用。
-
preconnect:预链接,完成 DNS 预解析同时还将进行 TCP 握手和创建传输层协议。
-
prerender:预渲染,预先加载文档的全部资源,相似于在一个隐藏的 tab 页中打开了某个连接 – 将下载全部资源、建立 DOM 结构、完成页面布局、应用 CSS 样式和执行 JavaScript 脚本等。
-
prefetch:预获取,使用 prefetch 声明的资源是对浏览器的提示,暗示该资源可能『将来』会被用到,适用于对可能跳转到的其余路由页面进行资源缓存。被 prefetch 的资源的加载时机由浏览器决定,通常来讲优先级较低,会在浏览器『空闲』时进行下载。
-
preload:预加载,主动通知浏览器获取本页的关键资源,只是预加载,加载资源后并不会执行;
对于前面三种很多浏览器已经内部默认作了优化,而prefetch & preload须要开发者根据状况代码手动设置。
从
prefetch和
preload的浏览器支持状况来看,prefetch除了safari外基本浏览器都有所支持,但preload做为新出的规范,兼容性差些,但safari正慢慢支持这一标准,如在iOS的safari高级选项的
试验性Webkit功能中已经有Link Preload这一选项。
preload 是声明式的 fetch,能够强制浏览器请求资源,同时不阻塞文档
onload 事件,是对浏览器指示预先请求当前页须要的资源(关键的脚本,字体,主要图片)。
prefetch 提示浏览器这个资源未来可能须要,可是把决定是否和什么时间加载这个资源的决定权交给浏览器。prefetch 应用场景稍微有些不一样 —— 用户未来可能在其余部分(好比视图或页面)使用到的资源。
从以上的描述能够看出,对于preload和prefetch声明,preload明显高于prefetch。
注:prebrowsing 好用但千万不要乱用,除非你很是明确会加载要prebrowsing的文件,否则会加剧浏览器负担拔苗助长。
接触过
Next.js的同窗都知道,next.js提供了一个具备预获取功能的模块:next/prefetch,看起来功能与prefetch相似,但其优先级与preload相似。
<Link prefetch href='/'><a>Home</a></Link>
<Link prefetch href='/features'> <a>Features</a></Link>
{ /* we imperatively prefetch on hover */ }
<Link href='/about'>
<a onMouseEnter={() => { Router.prefetch('/about'); console.log('prefetching /about!') }}>About</a>
</Link>
<Link href='/contact'><a>Contact (<small>NO-PREFETCHING</small>)</a> </Link>复制代码
因为features连接设置了prefetch,访问Index页面时浏览器会在页面加载完毕后从服务器取feature.js的文件,在index页面访问features页面时不会再从服务器请求features.js文件,直接从本地缓存中读取;contact没有作处理,从index访问contact时会从服务器请求concact.js文件。
咱们还能够发现,在next.js打包出来的html文件头中,都会将index.js / error.js / app.js 3个文件做为preload加载,由于这3个文件是本页面中必须用到的资源。
优化尝试
虽然大多数html只会在每次发布上线时才会改变,如更新js/css资源的引用地址,因此通常将HTTP Headers中设置一个比较短的max-age值,如cache-control: max-age=300,除此以外建议服务器开启Etag。
但以实时内容为主的网站(如金融类)为了页面的打开速度,会采起后台服务生产的方式 ,将全部首页数据所有生成到html中,省去用户首次加载时的后台接口请求等待时间。通常会设置cache-control: no-cache。
如今通常都经过文件名进行版本控制。Webpack打包命名可根据文件内容生成文件名的hash值,每次打包只有当内容改才从新生成hash值。此种状况之下,能够在HTTP Headers设置一个较大的缓存时间,如max-age=2592000,尽可能避免304请求和服务器进行请求链接。
// js
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
}
// css
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
}),复制代码
webfont文件比较特殊,正如
这篇文章中所说:
其实不一样浏览器下载font文件的时间不太同样,有的碰到css的声明就会加载,有的会等到dom节点匹配css声明时加载。
根据以上罗列的缓存建议,对当前的一个移动端项目进行优化。项目背景以下:
这个单页页面会打开几个小的页面(红色圈部分),经过webpack打包以后大概这个样子:
-
index.ef15ea073fbcadd2d690.js
-
static/js/0.1280b2229fe8e5582ec5.js
-
static/js/1.f3077ec7560cd38684db.js
-
static/js/2.39ecea8ad91ddda09dd0.js
-
static/js/3.d7ecc3abc72a136e8dc1.js
其中第一个index.js会在页面初次加载,其余4个js会在路由切换时动态加载。考虑下这个页面的业务场景,只要进入到这个页面,其余几个路由是必定会访问到的。因此若是在页面加载完成以后,趁户思考之际就主动把剩下几个js加载好,岂不完美。
webpackConfig.plugins.push(new PreloadWebpackPlugin({
rel: 'prefetch',
}));复制代码
rel属性还能够选择preload / prefetch模式。打包出来是这样:
访问页面能够看到,在不影响dom加载的状况下,浏览器预先加载了另外几个后面将会用到的js,当切换到对应路由时,也会直接从缓存取,不从服务器请求资源。
非动态加载(路由)页面的css会单独打包,在html文件中进行引用。除了使用一些打包插件优化代码体积外,可将css更细粒度拆分,如首页的css+弹窗css+页面标签切换的css等。除首页css外的先预加载,而后动态获取。但通常来讲一个页面的css大小在合理的代码状况下通过gzip压缩后都不会过大,因此优化的效果并不会太明显。
动态加载路由中css没有单独拆分而是在路由的js中,因此只能随着js优化了。
对于font文件,除了减小文件大小,设置缓存时间以外,也能够经过预加载的方式提早让浏览器下载来提升首屏渲染速度。预加载webfont须要与webpack的
html-webpack-plugin结合,打包时将制定的字体插入到html中。网上找了一圈没有找到现成的插件,本身来写一个。
npm install fontpreload-webpack-plugin --save-dev复制代码
- 在webpack的config文件的HtmlWebpackPlugin插件以后增长:
const FontPreloadWebpackPlugin = require('fontpreload-webpack-plugin');复制代码
webpackConfig.plugins.push(new FontPreloadWebpackPlugin({
rel: 'prefetch',
fontNameList: ['fontawesome-webfont'],
crossorigin: true,
}));复制代码