不到一秒才叫优化

1,014 阅读5分钟

之前做完的一个项目,业务逻辑写完之后,首屏渲染能到3~4秒,这对于用户体验是不能接受的,所以忙里偷闲把项目优化完之后:

http发送到响应时间:705ms,DOM构建完毕:452ms,页面加载完毕:678ms,清爽的感觉很上头~看来优化还是很有必要的!所以本篇记录一下优化过程。

正文

  1. UglifyJsPlugin

    UglifyJsPlugin 是一个webpack的解析、混淆、压缩JS的工具,低版本的 UglifyJsPlugin 是不支持ES6的,所以在使用的时候要注意要么升级版本,要么添加文件 .bablelrc,在里面添加

    {
      "pressets": ["es2015"]
    }
    
  2. cdn

    cdn 的话,尽可能把所有资源都使用cdn,比如 vuevuexvue-routeraxiosechart,举个例子:

    // cdn链接
    const cdn = {
      externals: {
        echarts: "echarts",
        "ant-design-vue": "AntDesignVue",
        vue: "Vue",
        "vue-router": "VueRouter",
        vuex: "Vuex",
        axios: "axios"
      },
      // cdn的css链接
      css: [],
      // cdn的js链接
      js: [
        "https://cdn.jsdelivr.net/npm/echarts@4/dist/echarts.min.js",
        "https://cdn.bootcss.com/vue/2.6.10/vue.min.js",
        "https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js",
        "https://cdn.bootcss.com/vue-router/3.0.7/vue-router.min.js",
        "https://cdn.bootcss.com/axios/0.18.0/axios.min.js"
      ]
    };
    

    然后在两个地方使用,一个是webpack的 chainWebpack 配置,一个是 configureWebpack 的配置:

    // 是否为生产环境
    const isProduction = process.env.NODE_ENV !== "development";
    // 本地环境是否需要使用cdn
    const devNeedCdn = true;
    
    module.exports = {
      // webpack 配置
      chainWebpack: config => {
        // ...
        config.plugin("html").tap(args => {
          // 生产环境或者本地需要cdn时,才注入cdn
          if (isProduction || devNeedCdn) args[0].cdn = cdn;
          return args;
        });
    	},
      configureWebpack: config => {
        // 用cdn方式引入,则构建时要忽略相关资源
        if (isProduction || devNeedCdn) config.externals = cdn.externals;
      }
    }
    
    

    这样 cdn 就配置好了,如果想在开发环境也使用它的话,需要在入口文件 index.html 中把CSS和JS的资源加载改成:

    <!-- 使用CDN的CSS文件 -->
    <% for (var i in htmlWebpackPlugin.options.cdn &&
       htmlWebpackPlugin.options.cdn.css) { %>
    <link
       href="<%= htmlWebpackPlugin.options.cdn.css[i] %>"
       rel="stylesheet"
    />
    <% } %>
      
    <!-- 使用CDN的JS文件 -->
    <% for (var i in htmlWebpackPlugin.options.cdn &&
      htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script> 
    <% } %>
    

    相当于动态加载了webpack配置里的cdn文件。

  3. CompressionWebpackPlugin

    CompressionWebpackPlugin 是webpack的 Gzip 压缩插件,官方介绍是:提供带 Content-Encoding 编码的压缩版的资源,下面是它的配置项:

    配置项类型默认值描述
    test{RegExp}.处理所有匹配此 {RegExp} 的资源
    asset{String}[path].gz[query]目标资源名称。[file] 会被替换成原资源。[path] 会被替换成原资源路径, [query] 替换成原查询字符串
    filename{Function}false一个 (asset) => asset 函数,接收原资源名(通过 asset 选项)返回新资源名
    algorithm{String|Function}gzip可以是 (buffer, cb) => cb(buffer) 或者是使用 zlib 里面的算法的 {String}
    threshold{Number}0只处理比这个值大的资源。按字节计算
    minRatio{Number}0.8只有压缩率比这个值小的资源才会被处理
    deleteOriginalAssets{Boolean}false是否删除原资源

    这边我用了网上大家常用的配置:

    configureWebpack: config => {
      const productionGzipExtensions = ["html", "js", "css"];
      config.plugins.push(
        new CompressionWebpackPlugin({
          filename: "[path].gz[query]",
          algorithm: "gzip",
          test: new RegExp("\\.(" + productionGzipExtensions.join("|") + ")$"),
          threshold: 10240, // 只有大小大于该值的资源会被处理 10240
          minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
          deleteOriginalAssets: false // 删除原文件
        })
      );
    }
    

    注意,如果使用 Gzip 的话,打包后会出现一个 x.gz 文件,比如 index.html,打包后会多一个 index.html.gz 文件,如果服务器端(nginx等)不开启 gzip 功能,加载的其实还是 index.html,开启之后就会加载 index.html.gz 来替代加载 index.html了。

    在 nginx 中可以这么配置(nginx.conf):

    #gzip  on;
    # 开启gzip
    gzip on;
    
    # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
    gzip_min_length 1k;
    
    # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间,后面会有详细说明
    gzip_comp_level 2;
    
    # 进行压缩的文件类型。javascript有多种形式,后面的图片压缩不需要的可以自行删除
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
    
    # 是否在http header中添加Vary: Accept-Encoding,建议开启
    gzip_vary on;
    
    # 设置压缩所需要的缓冲区大小     
    gzip_buffers 4 16k;
    

    重启之后查看请求接口的响应头的 Content-Encodinggzip 的话说明开启成功。

  4. moment

    由于用到了 moment,所以也得把它压缩一下,它可是贼大的,因为它的npm包里加载了很多的语言版本,所以一般情况下,只需要中文版就可以了,所以在webpack的配置里,把moment的语言包给忽略掉,然后在 main.js 中加载一个语言包 import "moment/locale/zh-cn",这样体积就会小很多。

  5. splitChunks

    splitChunks 可以对大包进行拆分,对小包进行合并,防止模块重复打包,从而减少请求资源的大小和此时,这个要根据实际情况去调整。

  6. 代码逻辑

    加了以上的操作之后,可以使用插件 webpack-bundle-analyzer 来看下优化的效果,如果还有大块的,就把业务继续优化,主要从引用、封装入手,能抽出来的尽量抽出来,把工具和业务分开。以vue为例的话,可以注意以下几点:

    1. v-if 和 v-show

      v-if 适合条件改变很少的情况,v-show 适合频繁切换条件的情况

    2. computed 和 watch

      computed 适合在计算和依赖场景使用,watch 适合数据变化的异步操作

    3. v-for 必须添加 key

      vue的 diff 算法会关联到 key,所以当状态更新时,key的作用很大,为了优化效率,建议添加key(现在开发工具已经做了提醒)

    4. v-for 里不使用 v-if

      v-for 的优先级要比 v-if 高,如果vue的循环节点每次都重新遍历整个数组,就需要花很多时间

    5. event

      在页面上使用了事件之后,离开的时候最好把事件给销毁(全局除外),可以防止内存泄漏(比如图表的加载和重绘)

    6. 图片懒加载

      图片资源懒加载可以使用 vue-lazyload 插件,保证只加可视区域内的图片

    7. 路由懒加载

      如果不用路由懒加载的话,页面访问就会把所有路由都加载完毕之后再加载,如果资源过多,白屏的出现几率就会大大增加,所以路由要改成只在访问的时候加载:component: () => import("./views/Login.vue")

    8. 三方插件按需引入

      很多三方插件包很大,所以它们大部分也都给了按需加载的使用说明

    9. 防抖和节流

      对于触发事件的时间和时机判定,防止用户频繁点击和误操作,它们两个也算优化的一部分