基于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
}
},
- 第二种,使用
chainWebpack,具体方法参考:
cli.vuejs.org/zh/guide/we…
github.com/neutrinojs/…
查看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) - 配置参数:
- 作用: 将首次构建的过程与结果数据持久化保存到本地文件系统,在下次执行构建时跳过解析、链接、编译等一系列非常消耗性能的操作,直接复用上次的 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,plugin的options相应配置项内指定即可。如下
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引用