Vue项目构建优化

578次阅读  |  发布于8月以前

本文作者为 360 奇舞团前端开发工程师 宁航

在开发大型前端项目时,往往是一个需求对应一个分支,当完成需求后,就需要将代码打包、部署。代码通常需要部署到多个环境中,这些环境包括:日常环境、测试环境、回归环境和生产环境。回归环境用于在发布前进行测试,生产环境是用户访问的版本。随着时间的推移,项目中会不断引入许多新的依赖(如第三方库、插件等)和图片资源,代码数量也会逐渐增多,从而导致构建项目更加耗时,这也意味着部署项目需要消耗更长的时间。

我负责的项目构建需要 66 秒,有时将代码部署到日常环境后,还需要临时修改重新部署;随后再依次部署到测试环境、回归环境,验证无误后,才能部署到线上环境。如果碰到要紧急上线的任务,这一过程无疑是十分费时的。因此,我决定针对项目构建时间过长的问题进行优化,以此来提高工作效率。本文将对如何优化项目构建速度进行详细介绍,具体过程如下:

一、前期准备

当前的构建工具有很多种,例如Rollup、Webpack、Vite等,在进行优化工作前,首先要明确项目使用的是哪个构建工具,我的项目是使用Vue CLI 3创建的,Vue CLI 在创建项目时会默认使用 Webpack 来构建项目,但在 Vue CLI中,默认情况下是不直接暴露 webpack 配置的,只能通过 vue.config.js 文件来修改配置。

其次,由于webpack在不断更新,新版本会增加许多优化策略,因此,还要明确项目使用的webpack版本,再基于这个版本,采用更有针对性的优化方法。经查询,发现Vue CLI 3对应webpack4,后续的优化方法将围绕该版本展开。

最后,我们还需要对构建过程进行详细分析,以便制定合理的优化策略。当我们运行如下命令,就会开始构建:

yarn build

yarn build会执行package.json中定义的构建脚本,在我的项目中,实际上运行了vue-cli-service build,该命令会进行如下操作:

(1)检查配置文件:Vue CLI首先会查找并解析项目中的配置文件vue.config.js,以获取构建配置和其他相关的配置信息。

(2)代码转译和打包:Vue CLI会使用webpack和相关的加载器(例如babel-loader)对项目的源代码进行转译和打包。这包括将Vue单文件组件转换为JavaScript、处理CSS预处理器(如SassLess)等。

(3)静态资源处理:Vue CLI会处理项目中的静态资源,如图片、字体等文件。这可能包括复制这些文件到输出目录,并在构建过程中引入适当的路径。

(4)压缩和优化: 构建过程还涉及到对输出的JavaScript、CSS和其他资源文件进行压缩和优化,以减小文件大小并提高应用性能。

核心步骤如下图所示:

现在,我们可以思考下可以在哪个阶段进行优化了。“源代码打包”是最先开始的操作,我首先想到的是这一阶段消耗的时间必然与代码体积呈正相关,即代码体积越大,需要编译的时间就越长,大致如下图所示。因此,如果能减少需要打包的代码体积,就可以节省一部分时间了。

此外,“源代码打包”阶段还会使用配置的loader对代码进行处理,在一个项目中可以配置多个loader,例如用vue-svg-inline-loaderSVG 文件转换为 Vue 组件中的内联 SVG ,用babel-loader来转译js文件。但webpack为单线程模式,只能依次使用每个loader处理代码,如果遇到耗时较长的loader,后续loader就只能等待。因此,如果能找到耗时较长的loader,让它们同时运行,也能节省一些时间。

构建时还会引入项目的图片,如果大尺寸的图片过多,也会影响构建性能。所以,我们还需要将大尺寸的图片进行替换,以提升构建速度。

针对以上3个优化点,我寻找了多个方案进行尝试,最终生效的方案如下图所示,后续将会对前两个方案进行详细介绍。

二、提前编译第三方库

引入webpack-bundle-analyzer插件分析项目体积

Webpack Bundle Analyzer 插件是一个用于分析 Webpack 打包结果的工具,它提供了一个直观的可视化界面,展示了项目打包后的文件结构,各个模块的大小、占比、依赖关系等信息。我们可以根据分析结果,针对性地优化文件大小,减少不必要的资源占用。

  1. 安装
yarn add -D webpack-bundle-analyzer
  1. 使用
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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

该插件将以树状图的形式展示项目打包后的内容, 从中可以看出每个文件的体积大小。

文件的体积参数有以下3种:

  1. 项目体积分析

我在项目中引入webpack-bundle-analyzer插件后,得到了如下的分析图:

通过观察该树状图,可知项目的总体积为 28 MB,其中3个最大的文件分别为index.js(8.71 MB), preview.js(7.5 MB), 和survey.js(7.45 MB)。这三个文件都引用了element-ui、moment等第三方库。第三方库的代码往往比较稳定,不会频繁变化。如果将这些第三方库打包成一个动态链接库,并在相关页面引入,那么在每次构建主项目时就不需要重新构建这些库了,从而使得源代码体积减少,打包速度加快。

使用动态链接库技术

动态链接库(Dynamic Link Library,DLL)是一种在Windows操作系统中常见的技术,可以用来在程序运行时加载共享的代码和资源。动态链接库中的函数、变量和资源可以被多个程序共享使用。这意味着不同的程序可以同时使用同一个动态链接库,从而减少磁盘空间和内存占用。

Webpack提供了两个插件DllPluginDllReferencePlugin来配置动态链接库,DllPlugin用于将第三方库(例如 VueReact 等)打包到一个或多个独立的动态链接库(DLL)中。DllReferencePlugin用于在主项目中引用预先打包好的动态链接库。以下是使用这 2 个插件的基本步骤:

  1. 创建一个用于打包第三方库的配置文件

首先,在项目根目录下创建 webpack.dll.config.js 文件,用于配置 DllPlugin

// 导入 path 和 webpack 模块
const path = require('path');
const webpack = require('webpack');

// 导出配置对象
module.exports = {
  // 指定 webpack 模式为生产模式
  mode: 'production', 

  // 入口配置,将需要打包的第三方库列出
  entry: {
    vendor: ['vue', 'vue-router', 'vuex', /* 此处可以继续添加其他第三方库 */ ]
  },

  // 输出配置,指定生成的动态链接库文件名称和路径
  output: {
    filename: '[name].dll.js', // 动态链接库文件名,[name] 表示入口名称
    path: path.resolve(__dirname, 'public/dll'), // 动态链接库文件输出目录
    library: '[name]' // 将动态链接库导出的内容赋值给变量名 [name]
  },

  // 插件配置,使用 webpack.DllPlugin 插件
  plugins: [
    new webpack.DllPlugin({
      name: '[name]', // 全局变量名称,保持与 output.library 一致
      path: path.resolve(__dirname, 'public/dll/[name].manifest.json') // 动态链接库清单文件路径
    })
  ]
};
  1. 创建npm脚本

package.json 文件中,添加一个新的 npm 脚本,用于运行上述 webpack 配置文件:

"scripts": {
  // ...其他脚本
  "dll": "webpack --config webpack.dll.config.js"
}

运行yarn dll,会在public/dll文件夹下生产vendor.dll.jsvendor.manifest.json2个文件。

  1. vue.config.js 文件引入 DllReferencePlugin插件

configureWebpack 配置中引入 DllReferencePlugin

// 导入 webpack 模块
const webpack = require('webpack');

// 导出配置对象
module.exports = {
  // 其他配置...

  // 配置 webpack
  configureWebpack: {
    // 插件配置
    plugins: [
      // 使用 webpack.DllReferencePlugin 插件
      new webpack.DllReferencePlugin({
        // 指定上下文路径为当前工作目录
        context: process.cwd(),
        // 指定动态链接库清单文件的路径
        manifest: require('./public/dll/vendor.manifest.json')
      })
    ]
  }
};
  1. html文件中引入DLL文件

配置完成后,运行yarn build命令,得到如下结果。

从图中可以看出,项目的总体积减少到了 7.35 MB,其中3个最大的文件分别减少到 2.74 MB(index.js), 1.74 MB( preview.js), 和 1.68 MB (survey.js)。构建时间由 66 秒缩短到了 43 秒,显著加快。

如果项目中引入了新的第三方库,则需要将该库添加到 webpack.dll.config.jsentry 中,并重新运行yarn dll即可。

三、为耗时loader开启多线程

引入speed-measure-webpack-plugin插件分析loader耗时

speed-measure-webpack-plugin 是一个用于测量 Webpack 打包速度的插件,包括各个阶段的耗时情况,例如初始化、加载、编译、优化、打包等;也可以分析每个 loader 和插件在打包过程中的耗时情况。这有助于我们找到影响性能的具体原因,进行针对性的优化。

  1. 安装
yarn add -D speed-measure-webpack-plugin
  1. 使用

我们需要创建一个 SpeedMeasurePlugin 的实例,并使用它来包装配置对象。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

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

如下图所示,运行yarn build后,就可以看到每个pluginloader分别花费了多少时间。

使用thread-loader开启多线程

thread-loader可以将指定的 loader 放在 worker 池的子线程中运行,这样能充分利用多核 CPU 的性能,从而实现并行处理。thread-loader 适用于任何耗时的 loader,特别是那些需要大量计算的 loader,例如 Babel、TypeScript 等。每个 worker 是一个单独的 Node.js 进程。

在我的项目中,babel-loadervue-svg-inline-loader耗时较多,因此我把thread-loader 放置到这些loader之前,为这些loader开辟了单独的线程池。具体配置如下:

module.exports = {
  // 配置 webpack
  chainWebpack: config => {
    // 针对 .js 文件的规则配置
    config.module
      .rule('js') // 添加一个规则命名为 'js'
      .test(/\.js$/) // 匹配文件后缀为 .js 的文件
      .exclude // 排除特定目录
      .add(/node_modules/) // 添加排除目录为 node_modules
      .end() 
      .use('thread-loader') // 使用 thread-loader 处理 .js 文件
      .loader('thread-loader') // 指定 thread-loader 作为 loader
      .end() 
      .use('babel-loader') // 使用 babel-loader 处理 .js 文件
      .loader('babel-loader') // 指定 babel-loader 作为 loader
      .end() 

    // 针对 .vue 文件的规则配置
    config.module
      .rule('vue') // 添加一个规则命名为 'vue'
      .use('thread-loader') // 使用 thread-loader 处理 .vue 文件
      .loader('thread-loader') // 指定 thread-loader 作为 loader
      .options({ 
        workers: 2 // 指定 worker 数量为 2
      })
      .end() 
      .use('vue-loader') // 使用 vue-loader 处理 .vue 文件
      .loader('vue-loader') // 指定 vue-loader 作为 loader
      .end() 
      .use('vue-svg-inline-loader') // 使用 vue-svg-inline-loader 处理 .vue 文件
      .loader('vue-svg-inline-loader') // 指定 vue-svg-inline-loader 作为 loader
      .end() 
  }
}

如果不配置thread-loader,各个loader的加载过程如图:

为耗时loader开启单独线程后,加载过程如图:

通过为耗时的loader开启多线程,使得项目构建时间从 43 秒减少到了 34 秒。

四、小结

通过使用DllPluginDllReferencePlugin插件将第三方库打包成动态链接库,引入thread-loader将耗时较长的loader放入单独的线程池中加载,替换项目中的大图片,使得项目构建时间从 66 秒减少到了 34 秒,总共减少 32 秒,约 48% 。

项目构建优化是需要不断尝试的,许多方案都不通用,以上3个优化点在本项目中起到了作用,下面我还将记录没有生效的方案,希望能为读者提供一些不同的思路,或许这些方案对你的项目有帮助。

  1. HardSourceWebpackPlugin插件进行缓存

在优化工作开始后,我最先想到的方案是:在第一次构建时,将没有变化的模块(如第三方库)的打包结果缓存下来,后续构建时直接读取缓存,就可以节省很多时间了。经过一番查找,发现HardSourceWebpackPlugin插件很适合用来执行这项工作。

Webpack提供了HardSourceWebpackPlugin插件来为模块提供中间缓存。如前文所述,Webpack在构建时,会解析项目中的每个模块,并根据需要对其进行转换和编译。在这一过程中,该插件会把编译结果保存下来。在下一次构建时,HardSourceWebpackPlugin插件会比较当前的模块和缓存中的模块是否一致,如果没有变化,就直接使用缓存结果。

引入该插件后,在本地的测试时发现:第一次构建花费的时间与之前相同,后续的构建速度却显著提升。但是,由于我的项目是使用部门统一的工作台部署,每次都需要重新执行yarn install安装依赖,所以该插件并不能产生作用。如果你的项目不是这种工作模式,那我推荐你使用该插件。

  1. 压缩代码

Webpack4默认情况下会对输出的JavaScriptCSS和其他资源文件进行压缩,但是我们还可以通过一些插件自定义压缩行为。

我们可以使用terser-webpack-plugin插件来删除空格、注释及未使用的代码,使得压缩后的代码体积更小;此外,它还支持并行压缩。

mini-css-extract-plugin插件可以将打包生成的css代码从JavaScript bundle中提取出来,在多页面应用中,如果多个页面共享一些css样式,使用该插件可以避免重复打包这些共享的样式。此外,分离出的css文件也可以与JavaScript文件同时加载,从而提高页面加载速度。

需要注意的是插件本身也需一定的时间来加载,因此,我们还应比较引入插件的时间是否高于压缩文件后节省的时间,合理使用。

五、参考资料

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8