【性能优化篇】-优化打包体积和速度

530 阅读8分钟

分析打包

webpack 打包体积分析

vue cli项目可以通过添加--report命令:"build": "vue-cli-service build --report",打包后 dist 目录会生成 report.html 文件,用来分析各文件的大小或者通过安装 webpack-bundle-analyzer 插件来分析,步骤如下:

1)安装

npm install webpack-bundle-analyzer -D

2)vue.config.js中 引入

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  configureWebpack: {
      plugins: [
         new BundleAnalyzerPlugin()
      ]
  }
}

vite 分析打包

安装 rollup-plugin-visualizer 插件,此插件可以展示构建时长、chunk 数量及大小,是分析构建的绝佳利器。

pnpm add rollup-plugin-visualizer -D

在 vite.config.ts 文件中引入该插件:

import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ 
 plugins: [ 
    // ...
    // 将 visualizer 插件放到最后的位置 
    visualizer() 
 ] 
});

配置好分析插件后,执行 构建命令,此时在根目录下会生成一个 stats.html 文件,此文件就是当前项目打包的chunk 分析图。

优化方案

文件路径查询优化

1、resolve.alias 的优化

resolve.alias配置将原始导入路径映射到新的导入路径,具有两个目的:

  1. 创建别名
  2. 减少查找时间。

例如:

resolve: {
 alias: {
   '@': resolve(__dirname, './src'),
 }
}

2、文件扩展名的优化

resolve.extensions 表示要尝试的文件扩展名列表。它还会影响构建性能,并默认为:

extensions: ['.js''.json']'.js''.json']

例如,当遇到像 require('./data') 这样的导入语句时,Webpack 首先会查找 ./data.js。如果未找到,它将搜索 ./data.json,如果仍未找到,它将抛出错误。

因此,建议尽量保持扩展名列表尽可能简洁,省略不太可能出现的情况。将最常用的文件扩展名放在最前面,以加快搜索过程。

resolve: {
 extensions: ['.js''.vue''.json'],'.js''.vue''.json'],
}

Tree Shaking按需引入优化

1、Tree Shaking(摇树优化)

Tree shaking 是一种从项目中删除未使用代码的技术。它依赖于 ES 模块语法。例如,当使用 lodash 时:

// Bad: This imports the entire lodash library
import _ from 'lodash';
// Good: This imports only the 'isEmpty' function from lodash
import _isEmpty from 'lodash/isEmpty';

Tree Shaking 在很大程度上减少了捆绑包的大小,是性能优化的重要部分。 Vite 和 Webpack 4.x 默认情况下都启用了 Tree Shaking。

2、组件库的按需引入

为什么没有使用 externals 的方式处理组件库呢?

externals缺点:直接在html内引入的,失去了按需引入的功能,只能引入组件库完整的js和css

组件库按需引入的原理:最终只引入指定组件和对应的样式

elementUI 需要借助 babel-plugin-component 插件实现,插件的作用如下:

import { Button } from 'element-ui'

Vue.component(Button.nameButton)

编译后的文件(自动引入 button.css):

import _Button from "element-ui/lib/button";
import _Button2 from "element-ui/lib/theme-chalk/button.css";
// base.css是公共的样式
import "element-ui/lib/theme-chalk/base.css";

Vue.component(_Button.name, _Button);

通过该插件,最终只引入指定组件和样式,来实现减少组件库体积大小

1)安装 babel-plugin-component

npm install babel-plugin-component -D

2)babel.config.js中引入

module.exports = {
  presets: ['@vue/app'],
  plugins: [
    [
      'component',
      {
        libraryName'element-ui',
        styleLibraryName'theme-chalk'
      }
    ]
  ]
};

组件库按需引入后,包体积大小变小、打包速度得到提升,同时 chunk-vendors.css 的体积也有了明显的减少

3、webpack的魔法注释

  • vue-router使用按需引入,结合webpack的魔法注释
  • 弹窗等懒加载组件,按需引入

4、减小三方依赖的体积

例如:剔除掉无用的语言包

打包速度优化

1、HappyPack 多线程打包

HappyPack 就能实现多线程打包,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,来提升打包速度

1)安装

npm install HappyPack -D

2)vue.config.js 中引入

const HappyPack = require('happypack');
const os = require('os');
// 开辟一个线程池,拿到系统CPU的核数,happypack 将编译工作利用所有线程
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
  configureWebpack: {
     plugins: [
       new HappyPack({
        id'happybabel',
        loaders: ['babel-loader'],
        threadPool: happyThreadPool
      })
     ]
  }
}

2)vue.config.js 中引入

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

module.exports = {
  configureWebpack: {
     plugins: [
      new CompressionPlugin({
        test/.(js|css)(?.*)?$/i//需要压缩的文件正则
        threshold1024//文件大小大于这个值时启用压缩
        deleteOriginalAssetsfalse //压缩后保留原文件
      })
     ]
  }
}

第三方依赖抽离优化

1、webpack externals 提取项目依赖

从上面的打包分析页面中可以看到:

  • chunk-vendors.js 体积中最大的几个文件都是一些公共依赖包,那么只要把这些依赖提取出来,就可以解决 chunk-vendors.js 过大的问题

  • 可以使用 externals 来提取这些依赖包,告诉 webpack 这些依赖是外部环境提供的,在打包时可以忽略它们,就不会再打到 chunk-vendors.js 中

1)vue.config.js 中配置:

module.exports = {
  configureWebpack: {
    externals: {
      vue'Vue',
      'vue-router''VueRouter',
      axios'axios',
      echarts'echarts'
    }
}

2)在 index.html 中使用 CDN 引入依赖

  <body>
    <script src="http://lib.baomitu.com/vue/2.6.14/vue.min.js"></script>
    <script src="http://lib.baomitu.com/vue-router/3.5.1/vue-router.min.js"></script>
    <script src="http://lib.baomitu.com/axios/1.2.1/axios.min.js"></script>
    <script src="http://lib.baomitu.com/echarts/5.3.2/echarts.min.js"></script>
  </body>

2、vite 提取项目依赖

对于固定的外部库,可以直接采用CDN的方式引入,无需打包进chunk中。

首先安装 rollup-plugin-external-globals 插件,该插件可以告诉 Rollup 哪些库是不需要打包的。

pnpm add rollup-plugin-external-globals -D

使用方法:在vite.config.ts中添加打包配置

import externalGlobals from 'rollup-plugin-external-globals';

const globals = externalGlobals({
  moment'moment',
  'video.js''videojs',
  jspdf'jspdf',
  xlsx'XLSX',
});

export default defineConfig({
    build: {
        rollupOptions: {
            external: ['moment''video.js''jspdf','xlsx'],
            plugins: [globals],
        }
    }
});

同时需要在 index.html 模版中引入对应库的CDN。

具体的CDN链接请根据自己的需要去官网或是CDN网站查询

<script src="https://.../moment.min.js"></script>
<script src="https://.../xlsx.full.min.js"></script>
<script src="https://.../jspdf.polyfills.umd.js"></script>
<script src="https://.../jspdf.umd.min.js"></script>
<script src="https://.../video.min.js"></script>

### 3、DllPlugin 动态链接库

`DllPlugin` 与 externals 的作用相似,都是将依赖抽离出去,节约打包时间。区别是 DllPlugin 是将依赖单独打包,这样以后每次只构建业务代码,而 externals 是将依赖转化为 CDN 的方式引入

当公司没有很好的 CDN 资源或不支持 CDN 时,就可以考虑使用 DllPlugin ,替换掉 externals

DllPlugin 配置流程大致分为三步:

1)创建 dll.config.js 配置文件

```js
import { DllPlugin } from "webpack";

export default {
    // 需要抽离的依赖
    entry: {
        vendor: ["vue""vue-router""axios""echarts"]
    },
    mode: "production",
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendor: {
                    chunks: "all",
                    name: "vendor",
                    test: /node_modules/
                }
            }
        }
    },
    output: {
        filename: "[name].dll.js"// 输出路径和文件名称
        library: "[name]"// 全局变量名称:其他模块会从此变量上获取里面模块
        path: AbsPath("dist/static"// 输出目录路径
    },
    plugins: [
        new DllPlugin({
            name: "[name]"// 全局变量名称:减小搜索范围,与output.library结合使用
            path: AbsPath("dist/static/[name]-manifest.json"// 输出目录路径
        })
    ]
};

2)package.json 配置脚本

"build:dll": "webpack --config ./dll.config.js",

3)使用 DllReferencePlugin 将打包生成的dll文件,引用到需要的预编译的依赖上来,并通过 html-webpack-tags-plugin 在打包时自动插入dll文件

vue.config.js 配置如下

import { DllReferencePlugin } from "webpack";
import HtmlTagsPlugin from "html-webpack-tags-plugin";

export default {
  configureWebpack: {
    plugins: [
      new DllReferencePlugin({
        manifestAbsPath("dist/static/vendor-manifest.json"// manifest文件路径
      }),
      new HtmlTagsPlugin({
        appendfalse// 在生成资源后插入
        publicPath"/"// 使用公共路径
        tags: ["static/vendor.dll.js"// 资源路径
      })
    ]
  }
};

先运行 npm run build:dll 打包生成依赖文件,以后只用运行 npm run build 构建业务代码即可

拆包优化

1、webpack SplitChunksPlugin拆包

module.exports = {  
    //...  
    optimization: {  
        splitChunks: {  
            chunks: 'async', // 异步加载chunk  
            minSize: 30000,  
            maxSize: 0,  
            minChunks: 1,  
            maxAsyncRequests: 5,  
            maxInitialRequests: 3,  
            automaticNameDelimiter: '~', // 文件名中chunk分隔符  
            name: true,  
            cacheGroups: {  
                vendors: {  
                    test: /[\\/]node_modules[\\/]/, //  
                    priority: -10  
                },  
                default: {  
                    minChunks: 2, // 最小的共享chunk数  
                    priority: -20,  
                    reuseExistingChunk: true  
                }  
            }  
        }  
    }  
};

2、Vite 拆分代码

build: {
   rollupOptions: {
     output: {
       chunkFileNames: ‘js/[name]-[hash].js’,
       entryFileNames: ‘js/[name]-[hash].js’,
       assetFileNames: ‘[ext]/[name]-[hash].[ext]’
     }
   }
}

Gzip 压缩文件优化

1、webpack Gzip压缩

线上的项目,一般都会结合构建工具 webpack 插件或服务端配置 nginx,来实现 http 传输的 gzip 压缩,目的就是把服务端响应文件的体积尽量减小,优化返回速度

html、js、css资源,使用 gzip 后通常可以将体积压缩70%以上

这里介绍下使用 webpack 进行 gzip 压缩的方式,使用 compression-webpack-plugin 插件

1)安装

npm install compression-webpack-plugin -D

2、vite Gzip压缩

  • 使用 vite-plugin-compress2插件对文件进行 gzip 压缩,减小构建包体积
import { compression } from 'vite-plugin-compression2'

export default ({ mode }) => defineConfig({
  plugins: [vue(), compression()],
})

移除不必要的打包配置

1、在 Vite 中禁用不必要的构建配置

Vite 还提供了优化构建配置的选项。以下是禁用某些构建功能的示例:

build: { 
 terserOptions: {
   compress: {
     // Remove console in production// Remove console in production
     drop_consoletrue,
     drop_debuggertrue,
   },
 },

 // Disable file size reporting
 reportCompressedSizefalse,
 // Disable source map generation to reduce bundle size (important for production)
 sourcemapfalse// Disable this for production to minimize bundle size

}

polyfill 低版本浏览器兼容优化

1、在 Vite 中低版本浏览器兼容

判断当前环境是否为测试环境,测试环境下不使用@vitejs/plugin-legacy插件。

注意构建命令要使用 vite build --mode test

import legacy from '@vitejs/plugin-legacy';

export default defineConfig(({ mode }: { mode: string }) => {
    // 判断当前是否为测试环境,注意构建命令要使用 vite build --mode test
    const isTest = mode === 'test';
    
    return {
        plugins: [
            !isTest && legacy({
              targets: ['ie >= 11'],
              additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
            }),
        ]
    }
})

测试环境下不使用legacy插件,但是还是需要引入Polyfill的,否则无法适配低端浏览器,这里需要在html中引入提前生成好的 Polyfill 链接。

这里我们需要使用vite-plugin-html插件,动态插入该Polyfill链接,具体实现如下:

vite.config.ts文件中

import legacy from '@vitejs/plugin-legacy';

export default defineConfig(({ mode }: { mode: string }) => {
    // 判断当前是否为测试环境,注意构建命令要使用 vite build --mode test
    const isTest = mode === 'test';
    
    return {
        plugins: [
            createHtmlPlugin({
                minifytrue,
                entry'src/main.tsx',
                template'index.html',
                inject: {
                  data: {
                    polyfill: isTest
                      ? 'https://polyfill.io/v3/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022'
                      : '',
                  },
                },
             }),
        ]
    }
})

在 index.html 文件中

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <!-- 注意在这里引入插入的polyfill -->
    <script src="<%- polyfill %>"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>