资源预加载 - 性能优化需知~

首发地址:https://mp.weixin.qq.com/s/8_...css

本文主要介绍前端性能优化中的资源预加载,不只会介绍常规的一些预加载手段;还会介绍工程实践中的应用。 html

涉及内容:前端

  • link相关(rel、as、media、defer、async);
  • 缓存(4种缓存、缓存策略、ServiceWork);
  • 优化网络(HTTP/2 ServerPush、Preload/Prefetch、域名拆分);
  • 同步接口JSON数据内联,加速首页渲染;
  • 浏览器中各资源加载的优先级;
  • 实践:webpack插件、quiklink.

前言node

    当咱们须要某些网络资源时,加载和执行每每耦合在一块儿,下载完当即执行,而加载过程是阻塞式的,延长了onload时间。所以如何在资源执行前预加载资源,减小等待网络的开销即是咱们要探讨的问题。webpack

1. 常规作法git

附一张不一样资源浏览器优先级的图示(来源):github

1)async/defer: 无阻塞加载
async.pngweb

    defer:DOMContentLoaded事件触发前执行;在现实当中,延迟脚本并不必定会按照顺序执行,也不必定会在DOMContentLoaded事件触发前执行,所以最好只包含一个延迟脚本;ajax

    async:加载完当即执行,没法控制执行时机和执行顺序。适用于无依赖的外部独立资源。chrome

不足:仅限于脚本资源;执行时机不可控或存在执行顺序问题,用于非关键资源。

2)使用ajax加载资源:能够实现预加载。

不足:优先级较低,没法对首屏资源提早加载。

3)Webkit浏览器预测解析:chrome的预加载扫描器html-preload-scanner经过扫描节点中的 "src" , "link"等属性,找到外部链接资源后进行预加载,避免了资源加载的等待时间,一样实现了提早加载以及加载和执行分离。

    原始解析作法:

    

    采用预解析(扫描)器:

    

不足:

    - 仅限解析HTML中收集到的外链资源,对JS异步加载的资源没法提早收集。

    - 未暴露相似于Preload的onload事件。

4)Server Push   图片来源

资源推送:

Link: <https://example.com/other/styles.css>; rel=preload; as=style;

资源仅预加载,不推送:

Link: <https://example.com/other/styles.css>; rel=preload; as=style;nopush

目标:减小请求数量和提升页面加载速度。

特色:多页面共享push cache(动态数据json除外)

适用场景:若是不推送这个资源,浏览器就会请求这个资源。

须要注意:要确保没有发起没必要要的推送,浪费流量。可使用preload标签代替,或者在HTTP头中加nopush属性。

Tips:若是服务器或者浏览器不支持 HTTP/2,那么浏览器就会按照 preload 来处理这个头信息,预加载指定的资源文件。

不足:

- Edge和Safari的支持很差,慎用;

- 若是浏览器不从push cache中获取资源,推送反而不利于网页性能;

- 只能推送不带请求体的GET和HEAD请求;

- push cache中的资源只能使用一次;

- 不考虑客户端的缓存,始终推送:

  - 只对第一次访问的用户开启服务器推送;

  - 保守起见,推送本来内联的资源,这样即便多推,也比内联效果好点;

  - 将资源文件的缓存状态更新至客户端的Cookie。

-仅能推送同源资源;

-push cache: 只在会话中存在,一旦会话结束就会被释放;

- 即便push的是最新的资源,若是http缓存中max-age没有过时,仍然使用http缓存中的资源。(资源依次查找缓存的顺序:内存缓存、Service Worker缓存、Disk缓存、Push缓存)

- 无load/error事件

【问题】

不只js渲染阻塞,有时须要等待js执行后获取一些数据(JSON)后,才能真正渲染完成,如何解决?(参考图片来源)

1)动态资源的提早推送,注意参数需固定,不带随机变量的;

    有一丢丢像webpack异步模块加载中__webpack_require__.e的实现,感兴趣的童鞋看这里

2)服务端渲染(直出同构)

    好比node层模板渲染时,将同步接口中拉取的数据同步数据先缓存起来,如挂载在window._data,使用时直接从全局变量上读取。

    好了,上面介绍了一些经常使用的预加载方法,下面主角登场~

preload和prefetch

概念

preload

    声明式的 fetch,能够强制浏览器请求资源,同时不阻塞文档 onload 事件。使用场景:当前页面使用(通常用于和首屏渲染无关的逻辑,好比数据打点等),尽早下载,优先级较高;

prefetch

    首次渲染时不须要,以后可能须要。优先级较低,在浏览器空闲时才会下载。使用场景:好比当前页可能跳转的页面,或者条件加载的资源。

特色

- preload的资源存储在内存缓存中(没有设置资源的缓存策略时);

- 下载但不执行;

- 异步加载,不影响当前页面的渲染;

- 提早加载资源,在真正使用时,直接从从缓存中读取;

- 使用场景

    - 当分析当前页面用户高频点击的连接,分析提取跳转页上的资源,使用prefetch预加载。

    - font字体文件的预加载因为字体文件必须等到CSSOM构建完成而且做用到页面元素了才会开始加载,会致使页面字体样式闪动。而浏览器为了不FOUT,会尽可能等待字体加载完成后,再显示应用了该字体的内容,会致使加载完成前显示空白。

检测prelaod和prefetch的支持状况

let { relList } = document.createElement('link');
return relList && relList.supports && relList.supports('preload');

如何使用

//link标签
<link rel="preload"  as="style" herf="./a.css"/>
<link rel="prefetch" as="script" href="./b.js"/>

//动态建立
let preLink = document.createElement('link');
preLink.rel ='prefetch';  //感受动态建立不适合preload
prelink.as = 'script';
preLink.href = './a.js';

as属性值

不一样值代表资源类型,对应的优先级不一样:style, script, image, media, document, font。(问题:官方说法:不带 “as”属性的 preload 的优先级将会等同于异步请求。测试后发现不写as并无发请求。)

注意事项

- 形成二次下载

    - 同一资源分别使用as='style'和as='script'预加载,会形成二次下载;

    - prefetch和preload同时对同一资源使用,会形成二次下载;

    - 实际是script脚本,但使用as='style'会形成二次下载;

    - preload字体时不带crossOrigin(默认指定anonymous匿名,不带认证信息),一样会形成二次下载preload字体时即便同域也须要带crossOrigin,不然一样会形成二次下载;Requests without credentials use a separate connection。

- 没有使用preload资源,Chrome会在onload事件3s后作出警示,避免无效的优化,浪费流量。

-浏览器对同一域名有并行加载数限制,所以考虑域名拆分等优化。

实践

下面是twitter页面中应用:
//head中
<link rel="preload" href="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" as="script">
//body底部
<script src="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" async></script>

preload的polyfill

背景知识

//若支持preload,异步下载完不会当即执行
<link rel="preload" >
//下载完当即应用到DOM树
<link rel="stylesheet" >
//异步下载,只有打印的时候才会应用,不符合则不会应用,所以不会阻塞渲染
<link rel="stylesheet" media='print' >

polyfill实现思路

参见loadCSS,提供了css的preload的polyfill实现(只展现关键代码):

// 1. 【支持preload的状况】
//因为preload只是获取样式,不会当即应用,所以使用onload改变link的rel使其当即生效9)
<link rel="preload" href="style.css" as="style" onload="this.onload = null;this.rel ='stylesheet'">
注:设置onload=null主要是由于有些浏览器会在rel改变时再次出发load事件。

// 2. 【不支持preload的状况:核心仍是media的使用】
// 1)获取所有link
let links = document.getElementsByTagName("link");
// 2)缓存每一个link的media
var finalMedia = link.media || "all";
// 3)改变link的rel和media(异步下载但不会应用)
link.rel = "stylesheet";
link.media = "only x";
// 4)若是绑定onload事件(为了启用media)
if( link.addEventListener ){
link.addEventListener( "load", enableStylesheet );
} else if( link.attachEvent ){
link.attachEvent( "onload", enableStylesheet );
}
// 5)为了应对旧的浏览器不支持link的onload事件
setTimeout( enableStylesheet, 3000 );
// 6)enableStylesheet回调中作以下操:将media恢复,样式当即应用
link.setAttribute( "onload", null ); 
link.media = finalMedia;

注意这里实现的是对CSS的预加载。

适用于对于首页无关的样式:

  因为preload可以异步加载样式,所以能够避免在加载首页无关样式时阻塞初始渲染。

对于首页初始渲染中重要的样式:

  1)内联 (注意,会将静态资源的缓存策略与页面的缓存策略捆绑)

  2)HTTP/2的serverPush

 知道了preload和prefetch的用途,那如何结合项目实践呢?因为webpack目前基本是项目必备,因此首先介绍结合webpack的使用;而后对quiklink进行简单介绍。

结合webpack的实践

1. 使用插件:PreloadWebpackPlugin

经常使用的配置以下:

new PreloadWebpackPlugin({
   //preload or  prefetch方式
   rel: '',
   /*
    *即<link as='' />中的as,代表资源类型,不一样的类型决定了不一样的执行优先级
    *好比:script的优先级大于style
   */
   as: '',
  //排除的html页面集合,即只关联要配置的页面
   excludeHtmlNames: [],
   //所关联页面须要使用preload或prefetch的资源
   include: []
})

其中include的两种使用:

1) 根据chunk类型进行处理:

include: 'asyncChunks' | 'initial' | 'allChunks'

  asyncChunks:import()动态导入的模块,可使用prefetch方式异步加载模块;

  initial:初始化须要的模块;

  allChunks:处理全部模块(asyncChunks & initial)。

  2) 对已知命名的chunk,能够更精确的使用数组的方式配置须要使用的chunk

include: ['vendor', 'index']

注意事项

1)须要结合HtmlWebpackPlugin插件使用

2)必须放到HtmlWebpackPlugin后面,由于PreloadWebpackPlugin须要使用其提供的hook钩子将构造的<link>插入html中:

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin()
]

使用效果

对某个页面中include的资源,最终会在对应页面head中插入link标签:

<link as="script" href="/common.js" rel="preload">
<link  as\="script"  href\="/asyncChunk.js"  rel\="prefetch"\>

当真正使用时,因为已经下载到本地,直接读取执行,性能获得较大的提高。

2. 结合import()

好处:拆分chunk,减小首屏js体积。

若是工程没有使用HtmlWebpackPlugin,能够对动态导入的资源作以下处理:

import(/* webpackPrefetch: true */)
import(/\* webpackPreload: true \*/)

【版本限制】需webpack v4.6.0+ 才支持预取和预加载。本地测试后,发现prefetch可用,preload无效。

quiklink

旨在成为根据用户viewport中的连接预取内容的简易解决方案。

工做原理

经过获取页面中a标签的href,试图更快的加载接下来可能要访问的页面。

1) IntersectionObserver(交叉观察器): 检测当前视口的links

let target= document.getElementById('a');
io = new IntersectionObserver(
    entries => {}, 
    {
      threshold: [1]  //交叉区域为1时会触发callback
    }
);
io.observe(target);

【备注】常规的主要是经过getBoundingClientRect()获取元素在视口中的详细位置,来实现滚动加载以及吸附等功能。

2) requestIdleCallback:等到浏览器空闲时;

    注意其和requestAnimationFrame的区别.

3) 检查当前的网络环境:

navigator.connection.effectiveType //4G、2G...;

4) 提供了多种方式预取连接

小巧的js库,使用了如上4个特性,每个都值得细细品味。有兴趣的能够看下源码:https://github.com/GoogleChro...

工做流程:

1) 浏览器空闲时,获取页面全部a标签的连接links;

2) 使用IntersectionObserver对link进行监听;

3) 在视口区的link,使用prefetch下载;

4) 判断当前网络情况,若使用的是2G或者开启了省流模式(data-saver),则不作处理。

data-saver: The user may enable such preference, if made available by the user agent, due to high data transfer costs, slow connection speeds, or other reasons.

题外话:prefetch有点偷流量的意思,用户想看什么才消耗对应资源产生的流量,而prefetch则会擅自作主,偷偷下载不少可能并不须要的资源(在早前流量特贵的时候这么作,估计会被骂...)

5) 下载连接资源,三种下载方式:fetch、xhr、<link rel=prefetch href="" />

使用说明

  • 只支持prefetch;
  • 经过a标签获取links;
  • 最佳实践是对后续可能访问的页面的提早下载,后续真正访问时,直接从本地获取执行。

总结

综合来看,PreloadWebpackPlugin更适合对chunk而非html文件的处理;而quikLink更适合博客类的网站,或者服务端渲染的页面,这样才能实现"秒开"的预期效果。

参考文献

  • preload-webpack-plugin
  • quicklink
  • IntersectionObserver
  • Preload, Prefetch And Priorities in Chrome
  • loadCSS
  • A Tale of Four Caches
  • React 16 加载性能优化指南
  • PWA直出
  • 亿万级访问量下的前端同构直出实践

欢迎[ 扫码 ]关注,最新文章,不按期更新~

    

相关文章
相关标签/搜索