🎓Vue首屏优化全解析❤️

1,343 阅读1分钟

打开GZIP

直接打包称为xxx.gz文件

在前端打包过程中即生成gz文件可以减少部署服务器的打包过程,可以减少服务器的性能压力,提高响应时间。

使用webpack插件CompressionWebpackPlugin来实现gzip打包。

在vue项目根目录新建webpack.config.js来增加webpack配置。

下面是一个基本配置,具体详细见其文档。

const CompressionPlugin = require("compression-webpack-plugin");

let config = {
    plugins: []
}

module.export = (env, argv) => {
    if(env.mode === 'production'){
        let comress = new CompressionPlugin({
        //检测并压缩js和css文件
        test: /\.(js|css|svg)$/,
        //默认即为gzip,可以不写
        algorithm: 'gzip',
        //压缩选项,可以在node文档中找到所有选项
        compressionOptions: {
            level: 1
        },
        //最小压缩体积,单位bytes,默认0
        threshold: 1600,
        //最小压缩效率,ration = 压缩后体积/压缩前体积,只有效率小于这个值的时候才进行压缩,否则不压缩。默认0.8。传1则全部压缩。
        minRatio: 0.5,
        //输出的文件名字,默认即为'[path][base].gz',其他参数见文档
        filename: '[path][base].gz',
        //是否删除源文件,默认false
        deleteOriginalAssets: true
    })
       config.plugins.push(compress)
    }
    return config
}

其中压缩选项[compressionOptions]可以在这里查看。

注意这里有一个生产和开发两个环境来确定是否注入插件。

我们可以通过Vue.config.productionTip = false /true来改变当前环境。

打开部署服务器gzip

一般我们使用nginx来部署前端项目。所以我们这里仅仅介绍nginx的配置。

由于我们之前已经配置了直接打包为gz,此时服务器就不需要再进行压缩,直接传递给浏览器即可。此时我们再nginx的config配置文件中加入对应的配置项即可。

nginx官方文档中,关于gzip多个API:

常用的有以下几个:

| Syntax: | **gzip** on | off; | | :------- | ---------------------------------------------- | | 默认 | gzip off; | | 父级节点 | http, server, location, if in location | | 介绍 | 是否打开gzip |

Syntax:**gzip_comp_level** *level*;
默认gzip_comp_level 1;
父级节点http, server, location
介绍压缩等级
Syntax:**gzip_min_length** *length*;
默认gzip_min_length 20;
父级节点http, server, location
介绍文件被压缩的临界大小

这些API若直接属于http节点内,则所有的sever都会生效,而置于对应的server节点的话就只有对应的server会生效,示例:

server {
	gzip            on;
    gzip_min_length 1000;
    #gzip_proxied    expired no-cache no-store private auth;
    #gzip_types      text/plain application/xml;
}

实际上我们再服务器放置的文件都是经过压缩的gz文件,所以我们只需要打开gzip on就行了。

通过SplitChunks实现手动分包,公共包共享

webpack中,官方增加了optimization.splitChunks来手动进行分包。 其默认有一定的规则(只影响异步chunks):

  • 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
  • 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
  • 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
  • 当加载初始化页面时,并发请求的最大数量小于或等于 30

对应的默认配置:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

这里需要了解几个重要的API,其他的参见webpack官方文档

  1. splitChunks.chunks:哪些chunks被执行拆分规则。默认:async
    1. all:所有的chunks都会被执行拆分。
    2. async:只有异步chunks才会被执行拆分。
    3. initial:只有入口chunks才会被执行拆分。
  2. splitChunks.maxAsyncRequests:按需加载时的最大并行请求数。默认:30
  3. splitChunks.maxInitialRequests:入口点的最大并行请求数。默认:30
  4. splitChunks.minChunks:拆分前必须共享模块的最小 chunks 数,即一个被共享的模块想要被拆分,那么他被引用的次数至少为minChunks。默认:1
  5. splitChunks.minSize:生成 chunk 的最小体积(以 bytes 为单位)。默认:20000即2.5KB。
  6. splitChunks.maxSize:告诉 webpack 尝试将大于 maxSize 个字节的 chunk 分割成较小的部分。
  7. splitChunks.maxAsyncSizemaxAsyncSizemaxSize 的区别在于 maxAsyncSize 仅会影响按需加载 chunk。
  8. splitChunks.maxInitialSizemaxInitialSizemaxSize 的区别在于 maxInitialSize 仅会影响初始加载 chunks。
  9. splitChunks.name:拆分 chunk 的名称。设为 false 将保持 chunk 的相同名称,因此不会不必要地更改名称。这是生产环境下构建的建议值。
  10. splitChunks.minRemainingSize:确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块。其仅在剩余单个 chunk 时生效。

maxSize 只是一个提示,当模块大于 maxSize 或者拆分不符合 minSize 时可能会被违反。

splitChunks.cacheGroups

缓存组可以手动设置chunks的匹配规则。缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项。但是 testpriorityreuseExistingChunk 只能在缓存组级别上进行配置。

  1. splitChunks.cacheGroups.{cacheGroup}.priority:一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组。默认组的优先级为负,以允许自定义组获得更高的优先级(自定义组的默认值为 0)。默认:-20。

  2. splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk:如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。这可能会影响 chunk 的结果文件名。默认:true。

  3. splitChunks.cacheGroups.{cacheGroup}.test:这个API可以接受一个函数,字符串以及正则表达式。一般可以使用正则表达式来确定规定文件夹或文件。也可以传入一个函数。function (module, { chunkGraph, moduleGraph }) => boolean RegExp string。省略它会选择所有模块。它可以匹配绝对模块资源路径或 chunk 名称。匹配 chunk 名称时,将选择 chunk 中的所有模块。

  4. splitChunks.cacheGroups.{cacheGroup}.filename:输出的文件名字。可以使用以下的占位符。

    模板描述
    [file]filename 和路径,不含 query 或 fragment
    [query]带前缀 ? 的 query
    [fragment]带前缀 # 的 fragment
    [base]只有 filename(包含扩展名),不含 path
    [filebase]同上,但已弃用
    [path]只有 path,不含 filename
    [name]只有 filename,不含扩展名或 path
    [ext]带前缀 . 的扩展名(对 output.filename 不可用)

也可以传入函数string function (pathData, assetInfo) => string

  1. splitChunks.cacheGroups.{cacheGroup}.enforce:告诉 webpack 忽略 splitChunks.minSizesplitChunks.minChunkssplitChunks.maxAsyncRequestssplitChunks.maxInitialRequests 选项,并始终为此缓存组创建 chunk。(注意,这里说的是外部的minSize等,而缓存组内部的minSize是不会被忽略的,还是会被拆分。)

下面贴一个拆分element-plus的拆分示例:

//这里是elementplus的推荐的按需引用
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  configureWebpack: {
    plugins: [
      Components({
        resolvers: [ElementPlusResolver()],
      }),
    ],
    optimization: {
      splitChunks:{
        chunks: "all",
        maxSize: 30000,
        minSize: 20000,
        cacheGroups: {
          // vendors: {
          //   name: "chunk-vendors",
          //   test: /[\\/]node_modules[\\/]/,
          //   chunks: "initial",
          //   priority: 2,
          //   reuseExistingChunk: true,
          //   enforce: true
          // },
          elementUI: {
            name: "chunk-elementplus",
             //最大12KB,这里可以自定义,也可以不加
            maxSize: 10000,
            minSize: 1000,
             //正则表达式来获取node_modules/element-plus/ 下的模块
            test: /[\\/]node_modules[\\/]element-plus[\\/]/,
             //所有的chunks都被拆分,异步或同步
            chunks: "all",
            priority: 3,
            reuseExistingChunk: true,
             //这里会忽略外部的minSize等,但内部的minSize仍然会被遵守
            enforce: true
          },
        }
      }
    }
  }
}

注意cacheGroups在组件是按需加载时,也是可以进行chunks拆分的。这是极好的,可以的按需加载的组件分包对优化十分友好。

路由懒加载

当某一个组件十分大且不是常用组件的时候,使用路由按需加载就是一个不错的方法。

并且vue-router提供很友好的API来让我们进行路由懒加载。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

通过上述方式,被动态引入(使用import()函数动态导入)的组件将会被单独打包为一个chunk。

且我们可以通过注释自定义chunk的名称。

另外也可以使用:

const Foo = () =>
  Promise.resolve({
    /* 组件定义对象 */
  })

来定义路由懒加载。

值得注意的是:import()动态引入也可以用在平时的组件导入。并且这个动态引入的组件也会被分包。按需加载。

组件按需引入

实际上我们平时的应用中,自己的代码不会很大,占体积大的一般都是一些第三方组件,比如UI组件,图表组件等。但是这些组件中并不是所有的组件我们都会用到。特别是图表类组件。这个时候使用按需引入十分关键。

一般来说按需引入即为我们手动在使用对用组件的父组件中引入和声明组件。比如Element-ui中。但是在一些新的UI组件,特别是Vue3出来之后,很多组件有了更加智能化的按需导入,我们不用全局导入,只需要使用和配置webpack插件,插件即可自动完成组件的引入,十分方便,比如下列的element-plus的示例:

//安装插件
npm install unplugin-vue-components
// webpack.config.js
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  // ...
  plugins: [
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
}

注意上述是webpack.config.js,使用vue时,需要配置到vue.config.js中。具体见webpack 相关

静态资源手动压缩,使用CDN

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

我们可以将自己的静态资源上传到CDN中,在用户进行请求的时候,CDN会根据其位置选择合适的服务器进行资源传送。以获得更好网络性能。

值得注意的是,某些比较大库也可以用CDN引入,不仅可以减少服务器网络压力。还可以保证其稳定性。比如一些公共的CDN站点,比如bootCDN

  1. 在HTML文件中引入script标签。

    <!DOCTYPE html>
    <html lang="">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <!-- 关键,引入对应的标签 -->
        <script src="https://unpkg.com/vue@next"></script>
        <title><%= htmlWebpackPlugin.options.title %></title>
      </head>
      <body>
        <noscript>
          <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
        </noscript>
        <div id="app"></div>
        <!-- built files will be auto injected -->
      </body>
    </html>
    
    
  2. 配置webpackexternals,下面是vue.config.js

    module.exports = {
        //其他参数
      configureWebpack: {
        externals: {
          'vue': 'Vue'
        }
      }
        //其他参数
    }
    
  3. 值得注意有两点:

    1. 库的引入要直接从库中引入,而不是从路径中引入(一般安装进node_modules都是从库中引入,所以只是注意一下)。即:
    import { createApp } from "vue"
    
    1. packge.json中的版本应该与html中引入的一致,否则会发生错误。

经测试,Vue使用外部CDN,vender体积在gzip下的62.79 KiB下降到37.84KiB。优化明显!

国内各大云平台都有CDN服务,比如阿里云,腾讯云等。下面简单演示将vue.js替换为CDN。

使用ES6模块替代Node模块,帮助webpack进行tree-shake

因为webpack的Tree-shake是基于ES6的静态结构。所谓静态结构。即不能动态的根据代码条件来导入。

即只能

import a from 'mw'

而不能

//Error syntax
import {a} from 'mw'
if(a){
    import(xxx)
}else{
    import(yyy)
}

不能动态引入即可以在词法分析阶段就可以确定模块之间的引入关系。进而就可以通过直接去掉没被引用的模块。

所以我们可以尽量使用ES6的模块,并且我们可以在ackage.json中配置sideEffects属性来帮助webpack确定哪些模块是纯的ES6模块,可以直接进行Tree-shake

关闭source-map

由于打包生成打代码都是经过压缩的,但是在产生错误的时候就会不好调试,此时就诞生了source-map。在源代码产生错误或者信息的时候,我们通过source-map查找到未压缩的源代码的位置。

但是在生产环境中,我们不想把源代码暴露给用户,并且source-map由于没有压缩,体积一般偏大。所以我们一般可以在生产环境中将其关闭。

vue可以这样关闭:

//vue.config.js
module.exports = {
    productionSourceMap: true
}

删除冗余代码

webpack原生的压缩工具minimizer功能有限,我们想要极致的压缩合并配置可以使用UglifyJS,而UglifyJS内核是 uglify-js

其包含很多配置,详细地址Compress options,下面简单给出一些用的比较多的:

  • drop_console (default: false) — Pass true to discard calls to console.* functions. If you wish to drop a specific function call such as console.info and/or retain side effects from function arguments after dropping the function call then use pure_funcs instead.
  • drop_debugger (default: true) — remove debugger; statements
  • booleans (default: true) — various optimizations for boolean context, for example !!a ? b : c → a ? b : c

前两个默认是关闭的,打开之后就可以在代码中去掉consoledebugger

下面是一个简单的示例:

module.exports = {
    //...
    configureWebpack : {
        optimization: {
          minimizer: [
              new UglifyJsPlugin({
                test: /\.js(\?.*)?$/i,
                parallel: true,
                uglifyOptions: {
                    compress: {
                        drop_console: true,
                        drop_debugger: true
                    }
                }
              }),
        ],
        }
    }
    //...
}

ssr或者Prerendering

如果仅仅是首页优化,那么使用prerendering更好。因为相较于ssr,不需要服务器承担html文件的编译。prerendering即是把对应的路由先生成html

prerendering一个简单的示例如下:

//vue.config.js
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')

module.exports = {
  configureWebpack = {
    plugins: [
    ...
    new PrerenderSPAPlugin({
      // Required - The path to the webpack-outputted app to prerender.
      staticDir: path.join(__dirname, 'dist'),
      // Required - Routes to render.
      routes: [ '/', '/about', '/some/deep/nested/route' ],
    })
  ]
}
}

参考

  1. CompressionWebpackPlugin
  2. nginx官方文档
  3. webpack官方文档-split-chunks-plugin
  4. vue官网-动态组件 & 异步组件
  5. prerendering

说明

本人能力有限,难免会有错误,如有错误请大家斧正。