vue-cli创建项目webpack打包优化记录

568 阅读8分钟

基于vue-cli自身的webpack配置进行优化,做的优化基本大同小异,基本是相当固定、通用的操作。

前置

在vue.config.js添加自己的配置

  • 第一种,使用configureWebpack进行配置,如果只是添加插件等,为该属性配置一个对象即可,如果需要根据环境参数进行配置,也可以配置一个函数,根据条件返回配置对象,该函数会在环境变量被设置之后懒执行,如下例
configureWebpack: (config) => {
    if (process.env.NODE_ENV === 'development') {
      return developmentConfig
    } else if (process.env.NODE_ENV === 'production') {
      return productionConfig
    }
  },

查看vue-cli的webpack配置

在终端输入npx vue-cli-service inspect --mode production >> webpack.config.production.js 即可生成mode="production"的配置,文件位于src目录下

分析工具

  • speed-measure-webpack-plugin 可以查看各个asset的大小,总体打包时间等,主要就是看打包时间,new SpeedMeasurePlugin()即可。
  • webpack-bundle-analyzer,会在本地启动一个服务,可视化展示各个chunk的大小,使用:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
...
new BundleAnalyzerPlugin()

优化

cache---持久化缓存

  • 使用方法:cache: { type: 'filesystem' },(webpack5)
  • 配置参数:图片.png
  • 作用: 将首次构建的过程与结果数据持久化保存到本地文件系统,在下次执行构建时跳过解析、链接、编译等一系列非常消耗性能的操作,直接复用上次的 Module/ModuleGraph/Chunk 对象数据,迅速构建出最终产物。
  • 原理: webpack在构建阶段首先会根据入口文件分析,根据import\require递归遍历,调用loader分析文件,使用Acorn生成AST,生成依赖图结构,其中包含大量AST解析等CPU密集型操作,每次打包都要进行这些重复操作。开启cache后,webpack会将构建对应数据存储,下次打包时对时间戳和哈希值进行对比,如果相同,则直接复用。

thread-loader---多线程优化

  • 使用方法:
config.module
      .rule('thread')
      .test(/\.js$/)
      .use('threadloader')
      .loader('thread-loader')
      .options({
       workers: 2,
        workerParallelJobs: 50
      })
      .end()
      .use('babelloader')
      .loader('babel-loader')
      .end()
  • 作用:thread-loader 会在加载文件时创建新的进程,在子进程中使用 loader-runner 库运行 thread-loader 之后的 Loader 组件,执行完毕后再将结果回传到 Webpack 主进程,从而实现性能更佳的文件加载转译效果。与此类似的还有happypack
  • 其他: 除此之外,还有Parallel-Webpack,其原理是根据entry为每个入口创建一个webpack进程(webpack5不支持)
  • 由于创建销毁线程的性能损耗较大,所以项目体积不大的情况下基本是负优化。

Terser---并行压缩

  • 内置配置已经默认开启
  • 该插件主要提供的是多线程并行压缩能力

lazyCompilation---按需编译

  • 使用方法:
      experiments: {
        lazyCompilation: true
      }
    
  • 作用: 编译时webpack会把所有chunk进行编译,包括异步chunk,然而这在冷启动时耗费的时间是无谓的,因为你可能只访问到其中的一些模块,使用这个特性,就可以达到一个异步按需编译的效果,类似vite。

exclude约束loader执行范围

loader的options里有一个属性:exclude可以排除loader编译的范围,我们可以把node_modules排除在外。

noParse| externals跳过文件编译

有一部分文件无需二次编译即可在浏览器运行,如:

  • Vue2 的 node_modules/vue/dist/vue.runtime.esm.js 文件;
  • React 的 node_modules/react/umd/react.production.min.js 文件;
  • Lodash 的 node_modules/lodash/lodash.js 文件。 我们可以使用noParse跳过webpack对这些文件的解析编译阶段,使用方法如下:
// webpack.config.js
module.exports = {
  //...
  module: {
    noParse: /lodash|react/,
  },
};

externals应用较多,常常搭配CDN进行使用,比如我们使用在运行时使用CDN引入vue,那么我们则需要在打包时将vue排除在构建流程外。

module.exports = {
  //...
  externals: {
    vue: 'vue',
  },
};

开发环境禁用产物优化

  • 这里的产物优化包括Tree-Shaking、SplitChunks、Minimizer 等,这些优化能显著降低产物大小,但同时也增加了构建的性能负担,在开发环境没有必要使用这些性能优化
  • vue-cli在开发环境并未禁用产物优化,所以我们可以使用如下配置项
module.exports = {
    ...
    optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
    minimize: false,
    concatenateModules: false,
    usedExports: false
  },
}

最小化watch监控范围

使用npx webpack --watch启动项目后,在代码发生变化后会发生rebuild,但实际上node_modules内的文件并不会频繁发生更新,所以我们可以使用watchOptions.ignored 属性忽略这些文件.

// webpack.config.js
module.exports = {
  //...
  watchOptions: {
    ignored: /node_modules/
  },
};

splitChunksPlugin分包

首先看看生产环境的默认配置

splitChunks: {
    // 配置两个缓存组: defaultVendors & common
    // defaultVendors 命中所有node_modules下的文件,将其打包成initialChunk,
      cacheGroups: {
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        // common将所有引用次数大于等于2的module打包为initialChunk
        // priority表示优先级,相同的module会被打包进优先级更大的chunk
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },

一般来说,基于默认配置和在路由处配置的异步chunk即可满足大部分分包需求。这里特别提一下两个配置项:

  • maxInitialRequest:用于设置 Initial Chunk 最大并行请求数;
  • maxAsyncRequests:用于设置 Async Chunk 最大并行请求数。
    这里的请求数实际上是指,根据splitchunks配置的分包策略,浏览器在加载chunk时,除了加载主chunk,还要加载由主chunk分出的分chunk,也就是说请求数 = 分包数 + 主包数。

JS压缩

webpack5默认使用terser对js进行压缩,大部分情况使用默认值即可,但我们也可以通过配置TerserWebpackPlugin来对其中的一些配置项进行修改,其中比较重要的配置项是切换压缩器的选项,如下

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        minify: TerserPlugin.swcMinify,
        // `terserOptions` 将被传递到 `swc` (`@swc/core`) 工具
        // 具体配置参数可参考:https://swc.rs/docs/config-js-minify
        terserOptions: {},
      }),
    ],
  },
};

提示:TerserPlugin 内置如下压缩器:

  • TerserPlugin.terserMinify:依赖于 terser 库;
  • TerserPlugin.uglifyJsMinify:依赖于 uglify-js,需要手动安装 yarn add -D uglify-js
  • TerserPlugin.swcMinify:依赖于 @swc/core,需要手动安装 yarn add -D @swc/core
  • TerserPlugin.esbuildMinify:依赖于 esbuild,需要手动安装 yarn add -D esbuild

另外,terserOptions 配置也不仅仅专供 terser 使用,而是会透传给具体的 minifier,因此使用不同压缩器时支持的配置选项也会不同。

CSS压缩

CSS压缩首先是基于MiniCssExtractPlugin将css抽取成单独文件,才能使用压缩器对css进行压缩。一般使用CssMinimizerPlugin进行压缩,vue-cli默认在生产配置,配置如下:

new CssMinimizerPlugin({
     parallel: true,
     minimizerOptions: {
       preset: [
         'default',
       {
       mergeLonghand: false,
       cssDeclarationSorter: false
      }
    ]
 }
})

同js压缩,minifiy选项也支持使用不同的压缩器,默认使用cssnano即可

动态加载

动态加载是webpack默认自带的能力, webpack通过注入支持动态导入的运行时实现,一般来讲都是在SPA中实现页面级别的动态加载

HTTP缓存优化

HTTP缓存主要依赖于产物文件名的不变进行缓存,即只有当产物文件名改变导致资源依赖路径改变,才会重新请求资源,达到http持久化缓存的效果,而文件名根据产物改变而改变主要依赖于webpack提供的hash,分为如下几种:

  • [fullhash]:整个项目的内容 Hash 值,项目中任意模块变化都会产生新的 fullhash
  • [chunkhash]:产物对应 Chunk 的 Hash,Chunk 中任意模块变化都会产生新的 chunkhash
  • [contenthash]:产物内容 Hash 值,仅当产物内容发生变化时才会产生新的 contenthash,因此实用性较高。 然后在output内或loader,pluginoptions相应配置项内指定即可。如下
module.exports = {
  // ...
  entry: { index: "./src/index.js", foo: "./src/foo.js" },
  output: {
    filename: "[name]-[contenthash].js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [new MiniCssExtractPlugin({ filename: "[name]-[contenthash].css" })],
};

此时,产物文件不会被重复下载,一直到文件内容发生变化,引起 Hash 变化生成不同 URL 路径之后,才需要请求新的资源文件,能有效提升网络性能,因此,生产环境下应尽量使用 [contenthash] 生成有版本意义的文件名。

但有一种情况比较特殊,即async chunk发生变化,会导致其主chunk也发生变化,这是因为主chunk运行时内存在对异步chunk的引用,所以我们通常把运行时单独抽离为一个模块,如下

module.exports = {
    ...
    optimization: { runtimeChunk: { name: "runtime" } },
}

这一点vue-cli的默认配置是没有的,需要我们自行配置

Scope Hoisting合并模块

默认情况下 Webpack 会将模块打包成一个个单独的函数, 如

// common.js
export default "common";

// index.js
import common from './common';
console.log(common);

会被打包成

"./src/common.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
     const __WEBPACK_DEFAULT_EXPORT__ = ("common");
     __webpack_require__.d(__webpack_exports__, {
      /* harmony export */
      "default": () => (__WEBPACK_DEFAULT_EXPORT__)
      /* harmony export */
    });
  }),
"./src/index.js":
  ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
      var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js");
      console.log(_common__WEBPACK_IMPORTED_MODULE_0__)
  })

为此,Webpack 提供了 Scope Hoisting 功能,用于 将符合条件的多个模块合并到同一个函数空间

((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    ;// CONCATENATED MODULE: ./src/common.js
    /* harmony default export */ const common = ("common");
    
    ;// CONCATENATED MODULE: ./src/index.js
    console.log(common);
})

但是这种特性无法在以下情况使用

  • 非ESM,动态的导入导出无法保证依赖性的确定
  • 单个module被多个chunk引用