对于前端页面来讲,静态资源的加载对页面性能起着相当重要的做用。本文将介绍浏览器提供的两个资源指令-preload/prefetch,它们可以辅助浏览器优化资源加载的顺序和时机,提高页面性能。javascript
如上图所示,咱们开发了一个简单的收银台,支付过程当中能够展开优惠券列表选择相应的券。从动图能够看到,列表第一次展开时,优惠券背景有一个逐渐显示的过程,体验上不是很好。css
问题的缘由也很明显,因为背景使用了视觉特地设计的图片,优惠券列表展开时须要去加载图片,背景渐显的过程实际上就是图片加载的过程;当网速慢的时候,这个问题会更加明显。那么,怎样解决这个问题呢?html
仔细分析一下,咱们会发现问题的缘由在于背景图的加载时机太晚。前端
若是能在优惠券列表渲染前加载好背景图,这个问题就不会出现。从这个思路出发,咱们可能想到如下两个方案:vue
preloadImage() { const imgList = [ require('@/assets/imgs/error.png'), require('@/assets/imgs/ticket_bg.png') ]; for (let i = 0; i < imgList.length; i++) { const newIMG = new Image(); newIMG.src = imgList[i]; } }
这种方案主要是利用浏览器的缓存机制,由js代码在特定时机提早加载相应图片,优惠券列表渲染时就能够直接从缓存获取。不过,这种方案增长了额外的代码,须要本身控制好加载时机,而且将图片的url硬编码在了逻辑中。 java
能够看出,以上两种方案可以解决咱们的问题,但都存在一些缺点。webpack
那么,有没有更好的解决方案呢?答案是prefetch-一种由浏览器原生提供的预加载方案。web
prefetch(连接预取)是一种浏览器机制,其利用浏览器空闲时间来下载或预取用户在不久的未来可能访问的文档。网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,即可以快速的从浏览器缓存中获得。--MDNvue-cli
具体来讲,浏览器经过<link rel="prefetch" href="/library.js">标签来实现预加载。浏览器
其中rel="prefetch"被称为Resource-Hints(资源提示),也就是辅助浏览器进行资源优化的指令。
相似的指令还有rel="preload",咱们会在后文说起。
<head> ... <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png"> ... </head>
查看如今优惠券列表的加载效果。
果真,成功达成了咱们指望的效果。那么浏览器是如何作的呢?咱们打开Chrome的Network面板一探究竟:
能够看到,在首屏的请求列表中已经出现了优惠券背景图ticket\_bg.png的加载请求,请求自己看起来和普通请求没什么不一样;展开优惠券列表后,network中增长了一次新的ticket\_bg.png访问请求,咱们很快发现,这个请求的status虽然也是200,但有一个特殊的标记—prefetch cache,代表此次请求的资源来自prefetch缓存。这个表现验证了上文中prefetch的定义,即浏览器在空闲时间预先加载资源,真正使用时直接从浏览器缓存中快速获取。
从上面的案例,咱们体会到了浏览器预加载资源的强大能力。实际上,预加载是一个广义的概念,prefetch只是具体实现方式之一,本节咱们介绍下另一种预加载方式preload。上文咱们提到,preload与prefetch同属于浏览器的Resource-Hints,用于辅助浏览器进行资源优化。为了对二者进行区分,prefetch一般翻译为预提取,preload则翻译为预加载。
元素的rel属性的属性值preload可以让你在你的HTML页面中元素内部书写一些声明式的资源获取请求,能够指明哪些资源是在页面加载完成后即刻须要的。对于这种即刻须要的资源,你可能但愿在页面加载的生命周期的早期阶段就开始获取,在浏览器的主渲染机制介入前就进行预加载。这一机制使得资源能够更早的获得加载并可用,且更不易阻塞页面的初步渲染,进而提高性能。
简单来讲,就是经过<link rel="preload" href="xxx" as="xx">标签显式声明一个高优先级资源,强制浏览器提早请求资源,同时不阻塞文档正常onload。咱们一样用一个实际案例进行详细介绍。
上图是咱们开发的另一个收银台,出于本地化的考虑,设计上使用了自定义字体。开发完成后咱们发现,页面首次加载时文字会出现短暂的字体样式闪动(FOUT,Flash of Unstyled Text),在网络状况较差时比较明显(如动图所示)。究其缘由,是字体文件由css引入,在css解析后才会进行加载,加载完成以前浏览器只能使用降级字体。也就是说,字体文件加载的时机太迟,须要告诉浏览器提早进行加载,这偏偏是preload的用武之地。
咱们在入口html文件head加入preload标签:
<head> ... <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin> <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin> ... </head>
再次查看页面首次加载的效果:
字体样式闪动的现象没有了!咱们对比下使用preload先后的network面板。
使用前:
使用后:
能够发现字体文件的加载时机明显提早了,在浏览器接收到html后很快就进行了加载。
注意:preload link必须设置as属性来声明资源的类型(font/image/style/script等),不然浏览器可能没法正确加载资源。
前文中咱们举的两个例子,都是在入口html手动添加相关代码:
<head> ... <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png"> ... </head>
<head> ... <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin> <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin> ... </head>
这显然不够方便,并且将资源路径硬编码在了页面中(实际上,ticket_bg.a5bb7c33.png后缀中的hash是构建过程自动生成的,因此硬编码的方式不少场景下自己就行不通)。webpack插件preload-webpack-plugin能够帮助咱们将该过程自动化,结合htmlWebpackPlugin在构建过程当中插入link标签。
const PreloadWebpackPlugin = require('preload-webpack-plugin'); ... plugins: [ new PreloadWebpackPlugin({ rel: 'preload', as(entry) { //资源类型 if (/\.css$/.test(entry)) return 'style'; if (/\.woff$/.test(entry)) return 'font'; if (/\.png$/.test(entry)) return 'image'; return 'script'; }, include: 'asyncChunks', // preload模块范围,还可取值'initial'|'allChunks'|'allAssets', fileBlacklist: [/\.svg/] // 资源黑名单 fileWhitelist: [/\.script/] // 资源白名单 }) ]
PreloadWebpackPlugin配置整体上比较简单,须要注意的是include属性。该属性默认取值'asyncChunks',表示仅预加载异步js模块;若是须要预加载图片、字体等资源,则须要将其设置为'allAssets',表示处理全部类型的资源。
但通常状况下咱们不但愿把预加载范围扩得太大,因此须要经过fileBlacklist或fileWhitelist进行控制。
对于异步加载的模块,还能够经过webpack内置的/_ webpackPreload: true _/标记进行更细粒度的控制。
如下面的代码为例,webpack会生成<link rel="preload" href="chunk-xxx.js" as="script">标签添加到html页面头部。
import(/* webpackPreload: true */ 'AsyncModule');
备注:prefetch的配置与preload相似,但无需对as属性进行设置。
从前文的介绍可知,preload的设计初衷是为了尽早加载首屏须要的关键资源,从而提高页面渲染性能。
目前浏览器基本上都具有预测解析能力,能够提早解析入口html中外链的资源,所以入口脚本文件、样式文件等不须要特地进行preload。
可是一些隐藏在CSS和JavaScript中的资源,如字体文件,自己是首屏关键资源,但当css文件解析以后才会被浏览器加载。这种场景适合使用preload进行声明,尽早进行资源加载,避免页面渲染延迟。
与preload不一样,prefetch声明的是未来可能访问的资源,所以适合对异步加载的模块、可能跳转到的其余路由页面进行资源缓存;对于一些未来大几率会访问的资源,如上文案例中优惠券列表的背景图、常见的加载失败icon等,也较为适用。
基于上面对使用场景的分享,咱们能够总结出一个比较通用的最佳实践:
- 大部分场景下无需特地使用preload
- 相似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
- 异步加载的模块(典型的如单页系统中的非首页)建议使用prefetch
- 大几率即将被访问到的资源可使用prefetch提高性能和体验
默认状况下,一个Vue CLI应用会为全部初始化渲染须要的文件自动生成preload提示。这些提示会被@vue/preload-webpack-plugin注入,而且能够经过chainWebpack的config.plugin('preload')进行修改和删除。
默认状况下,一个Vue CLI应用会为全部做为async chunk生成的JavaScript文件(经过动态import()按需code splitting的产物)自动生成prefetch提示。这些提示会被@vue/preload-webpack-plugin注入,而且能够经过chainWebpack的config.plugin('prefetch')进行修改和删除。
一、preload和prefetch的本质都是预加载,即先加载、后执行,加载与执行解耦。
二、preload和prefetch不会阻塞页面的onload。
三、preload用来声明当前页面的关键资源,强制浏览器尽快加载;而prefetch用来声明未来可能用到的资源,在浏览器空闲时进行加载。
四、不要滥用preload和prefetch,须要在合适的场景中使用。
五、preload的字体资源必须设置crossorigin属性,不然会致使重复加载。
缘由是若是不指定crossorigin属性(即便同源),浏览器会采用匿名模式的CORS去preload,致使两次请求没法共用缓存。
六、关于preload和prefetch资源的缓存,在Google开发者的一篇文章中是这样说明的:若是资源能够被缓存(好比说存在有效的cache-control和max-age),它被存储在HTTP缓存(也就是disk cache)中,能够被如今或未来的任务使用;若是资源不能被缓存在HTTP缓存中,做为代替,它被放在内存缓存中直到被使用。
然而咱们在Chrome浏览器(版本号80)中进行测试,结果却并不是如此。将服务器的缓存策略设置为no-store,观察下资源加载状况。
能够发现ticket_bg.png第二次加载并未从本地缓存获取,仍然是从服务器加载。所以,若是要使用prefetch,相应的资源必须作好合理的缓存控制。
七、没有合法https证书的站点没法使用prefetch,预提取的资源不会被缓存(实际使用过程当中发现,缘由未知)。
八、最后咱们来看下preload和prefetch的浏览器兼容性。
能够看到,二者的兼容性目前都还不是太好。好在不支持preload和prefetch的浏览器会自动忽略它,所以能够将它们做为一种渐进加强功能,优化咱们页面的资源加载,提高性能和用户体验。
做者: Sha Chaoheng