页面性能优化

1、常规优化手段

白屏 -> 首次渲染

- loading:
    - 在页面最前面加loading相关的html和css
    - 结合html-webpack-plugin插入loading(html+css)
    - prerender-spa-plugin(`暂记`)
    - 内联CSS(`不考虑缓存策略的话`)
- 骨架屏<br/>
    Q: css文件的加载阻塞骨架屏的渲染<BR/>
    A:使用preload,伪代码以下:
    ![](https://user-gold-cdn.xitu.io/2019/8/16/16c9923d9d085200?w=758&h=429&f=png&s=205515)
复制代码

缓存策略

精简打包的代码

动态polyfill

  • polyfill.io:根据浏览器User-Agent头,判断其支持的特性,返回合适的polyfill。

webpack相关优化

  • Tree Shaking
    • 关闭babel的模块处理:modules: false; pachage.json配置sideEffects
    • 对于lodash、underscore这样的工具库来讲尤为重要,开启了这个特性后:
      import { capitalize } from 'lodash-es';
      复制代码
      • 效果
      import {a} from xx 
      转换为 
      import {a} from 'xx/a'
      复制代码
      • 原理
        // a.js
        export function a() {}
        // b.js
        export function b(){}
        // package/index.js
        import a from './a'
        import b from './b'
        export { a, b }
        // app.js
        import {a} from 'package'
        console.log(a)
        复制代码
        结果treeShaking后:
        // a.js
        export function a() {}
        // b.js 再也不导出 function b(){}
        function b() {}
        // package/index.js 再也不导出 b 模块
        import a from './a'
        import b from './b'
        export { a }
        // app.js
        import {a} from 'package'
        console.log(a)
        复制代码
        配合 webpack 的 scope hoistinguglify 以后,b 模块的痕迹会被彻底抹杀掉。
      • 反作用
        若是 b 模块中添加了一些反作用,好比一个简单的 log:
        // b.js
        export function b(v) { reutrn v }
        console.log(b(1))
        复制代码
        处理后 b 模块内容变成了:
        // b.js
        console.log(function (v){return v}(1))
        复制代码
        注意:b文件中保留了console的代码。
      • sideEffects 设置为false,即无论它是否真的有反作用,只要它没有被引用到,整个模块/包都会被完整的移除。(上面的b文件被移除)
      • 注意事项: shim或者polyfill慎用!
        不是导出使用的模块需声明:
        "sideEffects": [
            "*.css",
            "src/javascript/base/da.js"
          ]
        复制代码
  • splitChunks
  • import()

可交互 -> 内容加载完成

  • 懒加载
    • 监听scroll事件
    • Intersection Observer获取元素可见性
  • placeholder(提早占位)
    和骨架屏不一样。其解决的问题:文本图片加载完先后,因为高度被撑开,致使闪屏的现象。三方组件:react-placeholderreact-hold

直出HTML(同构)优化 (缓存时注意拉取CGI接口的参数处理)

  • 方案
    • 接口动静分离 & Redis缓存(node层缓存html)
      • 静态数据在node层获取而后渲染;动态数据前端拉取并渲染
    • PWA直出优化(前端缓存html)
  • 同构直出的容灾策略
    • 前端渲染和直出页面的访问路径不一样
    • 直出出错时,转发到前端渲染路径

PWA

  • 核心:Web App Manifest,Service Worker,Push API & Notification API,App Shell & App Skeleton

Service Worker

  • 基于HTTPS
  • 大部分API都是基于promise-based
  • 运行在独立的worker进程(webworker)
  • 离线缓存
  • 弱网快速访问
  • 使用lighthouse测试页面性能,根据评估结果,针对性优化。
  • 错误监控 / 数据统计

  • 通常监听load事件注册
  • 做用域
    //只会对topics/下面的路径进行优化
    navigator.serviceWorker.register('/topics/sw.js');
    复制代码

  • 生命周期javascript

    • 注册

      指定serviceworkerJS文件的位置,加载解析执行;load事件中注册css

    • 安装

      将指定的静态资源进行离线缓存html

    • 激活

      对旧缓存作删除等处理;接管控制权前端

  • SW更新机制
    java

    背景SW 没有自动更新的逻辑,它须要在页面加载(一次跳转)以后才会去请求sw.js
    解决:因为浏览器判断sw.js是否更新是经过字节方式,所以修改cacheName会从新触发install并缓存资源。此外,在activate事件中,咱们须要检查cacheName是否变化,若是变化则表示有了新的缓存资源,原有缓存须要删除。node

    • 新的SW.js文件下载,并触发install事件。
    • 此时,旧的SW还在工做,新的SW进入waiting状态(此时两个SW同时存在,旧的SW掌管当前页面)。
      • 等到下一次页面跳转(二次跳转)才能展现最新的页面。
      • install中缓存资源后,self.skipWaiting():
        //caches是全局变量
        self.addEventListener('install',e =>{
          e.waitUntil(
            caches.open(cacheStorageKey)
            .then(cache => cache.addAll(cacheList))
            .then(() => self.skipWaiting())
          )
        })
        复制代码
      • 【workbox另外一种思路】由于新的SW会进入waiting状态,因此在waiting阶段,采用必定的策略来进行页面的刷新:如弹窗提示用户是否刷新,若刷新则调用wb.messageSW({type: 'SKIP_WAITING'});触发message事件。【webWorker】main.js中作相关逻辑处理,经过postMessage传递消息控制worker
        self.addEventListener('message', (event) => {
            if (event.data && event.data.type === 'SKIP_WAITING') {
                // the new v2 Service Worker will immediately kill the old v1 activated Service     Worker once the v2 Service Worker installs.
                self.skipWaiting();
            }
        });
        复制代码
    • 一旦新的SW接管,则会触发activate事件;能够在此处对旧缓存作删除等处理;不重刷,接管控制权:self.clients.claim()
      self.addEventListener('activate',function(e){
          e.waitUntil(
              //获取全部cache名称
              caches.keys().then(cacheNames => {
              return Promise.all(
                  // 获取全部不一样于当前版本名称cache下的内容
                  cacheNames.filter(cacheNames => {
                      return cacheNames !== cacheStorageKey
                  }).map(cacheNames => {
                      return caches.delete(cacheNames)
                  })
              )
              }).then(() => {
                  //直接接管当前页面的权限
                  return self.clients.claim()
              })
          )
      })
      复制代码
  • 注意react

    • 设置sw.jsmanifest.json静态资源的缓存策略:不缓存,必须校验
    • 冷启动,预加载
    • 只缓存重要的页面如主页,连接,最近的文章等
    • 不缓存图片,视频和大的文件
    • 按期清除旧的缓存文件
    • 提供一个”缓存到本地”的按钮,以便用户能够自行选择
    • 降级方案:增长降级开关
      if('serviceWorker' in navigator) {
          fetch('./cas').then(() => {
              if(降级) {
                  //注销掉全部sw
                  unregister();
              }else {
                  //注册
                  register();
              }
          })
      }
      复制代码
  • 首次启动优化
    背景:首次加载时没有资源,因此会走线上,等于没优化
    方案:构建时,把整个项目用到的资源输出到一个list,而后inline到sw.js。当sw install,就会把这个list的资源所有请求进行缓存。这样作的结果是,不管用户第一次进入咱们站点的哪一个页面,都会把整个站点全部的资源都加载回来并缓存。 webpack

Redux 与 IndexDB 结合


2、资源预加载(静态 & 动态)

涉及内容:
 - link相关(rel、media)
 - defer、async
 - 缓存(4种缓存、缓存策略、ServiceWork)
 - 优化网络(H2 PushPreload/Prefetch、域名拆分)
 - 推送JSON/json内联,加速首页渲染
 - 浏览器中各资源加载的优先级git

前言

  • 背景

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

  • 常规作法

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

    不一样资源浏览器优先级

    1. async/defer: 无阻塞加载

      async defer

      • defer:DOMContentLoaded事件触发前执行
        在现实当中,延迟脚本并不必定会按照顺序执行,也不必定会在DOMContentLoaded事件触发前执行,所以最好只包含一个延迟脚本
      • async:加载完当即执行,没法控制执行时机和执行顺序。适用于无依赖的外部独立资源

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

    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属性。
      【若是服务器或者浏览器不支持 HTTP/2,那么浏览器就会按照 preload 来处理这个头信息,预加载指定的资源文件。】

      不足:

      • Edge和Safari的支持很差,慎用
      • 若是浏览器不从push cache中获取资源,推送反而不利于网页性能。
      • 只能推送不带请求体的GET和HEAD请求
      • push cache中的资源只能使用一次
      • 不考虑客户端的缓存,始终推送。
        • 只对第一次访问的用户开启服务器推送;
        • 保守起见,推送本来内联的资源,这样即便多推,也比内联效果好点。
        • 将资源文件的缓存状态更新至客户端的Cookie
          • cookie空间有限,可使用 Golomb-compressed sets算法生成指纹,编码为base64,而后存入Cookie
          • 需自行处理缓存策略
      • 仅能推送同源资源
      • push cache: 只在会话中存在,一旦会话结束就会被释放
      • 即便push的是最新的资源,若是http缓存中max-age没有过时,仍然使用http缓存中的资源。(【扩展】资源依次查找缓存的顺序:内存缓存、Service Worker缓存、Disk缓存、Push缓存
      • 无load/error事件

【扩展】

 问题1:不只js渲染阻塞,同时js执行后可能获取一些数据(JSON),才能真正渲染完成,如何解决?

  1. 用于动态资源的提早推送,注意参数需固定,不带随机变量的
  2. 服务端渲染(直出同构)
  3. 内联JSON(目前有些工程使用此方法传递同步数据)
    另外两种较常见的渲染方式图片来源:

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 的优先级将会等同于异步请求。 测试:没有发请求。

  • 注意事项

    • 形成二次下载
    • 没有使用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
        // 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;
    复制代码

    适用于对于首页无关的样式:因为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类型进行处理:
    • 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

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

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

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

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

    • requestIdleCallback:等到浏览器空闲时

      【备注】注意其和requestAnimationFrame的区别

    • 检查当前的网络环境:navigator.connection.effectiveType //4G、2G...

    • prefetch缓存的待下载的url

    小巧的js库,使用了如上4个特性,每个都值得细细品味。

  • 工做流程

    • 浏览器空闲时,获取页面全部a标签的连接link
      • 使用IntersectionObserver监听link
        • 在视口区的link,使用prefetch下载
          • 判断当前网络情况,若使用的是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擅自为我作主,偷偷下载不少我可能并不须要的资源(在早前流量特贵的时候这么作,估计会被打死...)。

          • 三种下载资源的方式:fetchxhr<link rel=prefetch href="" />

  • 使用说明

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

总结

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

参考文献

【欢迎留言】本文是否对你有帮助,亦或有所遗漏笔误等,烦请告知。

相关文章
相关标签/搜索