在使用webpack进行项目构建的过程中, 当我们的项目需求累积到一定层度的时候, 总是需要做一些构建优化,以及项目结构优化, 来保证页面加载的速度, 更好的用户体验. 以下我们将通过几个方面来了解 webpack项目分析,以及更好的性能优化.

一、构建分析

1.1 打包速度分析

  • speed-measure-webpack-plugin
 npm install speed-measure-webpack-plugin -D
 

作用:

1. 分析整个打包总耗时
2. 每个 loader 和 plugin的耗时情况
 

用法:

// 导入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
// 实例化插件
const smp = new SpeedMeasurePlugin();
module.exports = {
configureWebpack: smp.wrap({
plugins: [
// 这里是自己项目里需要使用到的其他插件
new yourOtherPlugin()
]
})
}
 

1.2 体积分析

  • webpack-bundle-analyzer
 npm install webpack-bundle-analyzer -D
 

作用:

分析打包之后每个文件以及每个模块对应的体积大小
 

用法:

// 导入速度分析插件
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
// 导入体积分析插件
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
// 实例化速度分析插件
const smp = new SpeedMeasurePlugin();
module.exports = {
configureWebpack: smp.wrap({
plugins: [
// 实例化体积分析插件
new BundleAnalyzerPlugin()
]
})
 

运行:

npm run build --report
 

二、性能优化

2.1 优化构建速度

2.1.1 合理使用hash值命中缓存
1. hash: 项目每次编译生成一个hash值。如果所有的文件都无变化,则hash值不变 , 反之, 则会发生改变. 所有的文件名. hash相同
2. chunkHash: 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值.  不同的chunkHash不同, 只有对应的chunk内部发生了改变, chunkHash才会改变
3. contentHash: 针对每个文件内容级别产生的哈希值, 只有文件内容发生改变了, contentHash才会改变
 

用法:

module.exports = {
...
output: {
// filename: 'bundle.[contentHash:8].js',
// 打包代码时,加上 contentHash
filename: '[name].[contentHash:8].js',
}
...
}
 
2.1.2 减少检索范围
  • loader 使用include和exclude减少检索范围
module.exports = {
...
module: {
rules: [
//babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
{
test: /.js$/,
loader: ['babel-loader'],
include: resolve('src'),
exclude: /node_modules/
},
]
}
...
}
 
  • resolve.alias减少检索路径
module.exports = {
...
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
...
}
 
2.1.3 优化babel-loader
1. 添加exclude配置
2. 添加.babelrc文件
3. 开启cacheDirectory 缓存
 

在babel-loader执行的时候,在运行期间可能会产生一些重复的公共文件,造成代码冗余,减慢编译效率:

//webpack.base.conf.js
module.exports = {
...
module: {
rules: [
//babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
{
test: /.js$/,
loader: 'babel-loader?cacheDirectory',
include: resolve('src'),
exclude: /node_modules/
},
]
}
...
}
 
// .babelrc
// npm install babel-plugin-transform-runtime
{
// "presets": [["@babel/preset-react", {"modules": false }]],
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties", "babel-plugin-transform-runtime"]
}
 
2.1.4 热更新

webpack在本地开发提升效率的最有效的办法就是使用热更新, 热更新模块时在webpack的devServer中配置:

// npm install webpack-dev-server -D
devServer: {
contentBase: '../../dist',
open: true,
port: 8080,
hot: true, //对有变化的文件打包
hotOnly: true,
historyApiFallback: true,
publicPath: '/'
}
 

配置 hot: true , 每次保存的时候, webpack都会对有变化的文件打包, 然后实时更新到页面

2.1.5 懒加载
  • vue异步组件
{
path: '/index',
component: (resolve) => {  require(['../components/index/index'], resolve) }
}
 
  • ES6 import() 异步引入文件
//路由
{
path:'/index',
component: () => import('../components/index/index')
}
{
path:'/index',
component: resolve => { import('../components/index/index').then(module=>resolve(module)) }
}
//es6
setTimeout(()=> {
import('../components/index/index').then(exports =>{
...
})
},1000)
 
  • webpack 的 require 和 ensure()
{
path: '/index',
component: r => require.ensure([], () => r(require('../components/index/index')), 'index')
}
 
2.1.6 webpack自带小插件优化
  • webpack.IgnorePlugin
// 忽略本地文件
module.exports = {
...
plugins:[
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/./locale/, /moment/),
]
...
}
 
  • DllPlugin和DllReferencePlugin预编译
作用: 直接使用打包好的第三方包, 不需要每次都打包
 

假设项目中使用了Vue, vue-router, element-ui , 首先需要出创建 webpack.dll.conf.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
vue: ['vue', 'vue-router'],
ui: ['element-ui']
},
output: {
path: path.join(__dirname, '../src/dll'),
filename: '[name].dll.js',
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '../src/dll', '[name]-manifest.json'),
name: '[name]'
}),
new webpack.optimize.UglifyJsPlugin()
]
}
 

然后 webpack --config ./build/webpack.dll.conf.js打包:

使用 DllReferencePlugin 在 build/webpack.base.config.js 中添加下列插件:

    ...
plugins: [
new webpack.DllReferencePlugin({
manifest: path.join(__dirname, '../src/dll/ui-manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: path.join(__dirname, '../src/dll/vue-manifest.json')
}),
],
 

再次执行npm run build , 比较前后两次打包的输出时间:

使用 DllPlugin 之前: npm run build
 
使用 DllPlugin 之后: npm run dll -> npm run build
 
2.1.7 多进程并行解析

多线程构建的方案比较知名的有以下三个:

1. thread-loader (推荐使用这个)
2. parallel-webpack
3. HappyPack
 
  • thread-loader
npm install thread-loader -D
 

用法:

module.exports = {
...
module: {
rules: [
//babel-loader处理js文件, 配合的依赖有 @babel/core @@babel/preset-env, 在根目录新增.babelrc文件
{
test: /.js$/,
loader: ['thread-loader', 'babel-loader'],
include: resolve('src'),
exclude: /node_modules/
},
]
}
...
}
//带options的配置
use: [
{
loader: "thread-loader",
// loaders with equal options will share worker pools
// 设置同样option的loaders会共享
options: {
// worker的数量,默认是cpu核心数
workers: 2,
// 一个worker并行的job数量,默认为20
workerParallelJobs: 50,
// 添加额外的node js 参数
workerNodeArgs: ['--max-old-space-size=1024'],
// 允许重新生成一个dead work pool
// 这个过程会降低整体编译速度
// 开发环境应该设置为false
poolRespawn: false,
//空闲多少秒后,干掉work 进程
// 默认是500ms
// 当处于监听模式下,可以设置为无限大,让worker一直存在
poolTimeout: 2000,
// pool 分配给workder的job数量
// 默认是200
// 设置的越低效率会更低,但是job分布会更均匀
poolParallelJobs: 50,
// name of the pool
// can be used to create different pools with elsewise identical options
// pool 的名字
//
name: "my-pool"
}
},
...
]
 

前后编译速度的比较, 提升了 201ms

  • HappyPack
把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。 对file-loader、url-loader 支持的不友好
 
npm install happypack -D
 

用法:

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
...
module: {
rules: [
{
test: /.js$/,
//把对.js 的文件处理交给id为happyBabel 的HappyPack 的实例执行
use: ['happypack/loader?id=happyBabel'],
include: resolve('src'),
exclude: /node_modules/
},
...
]
},
plugins: [
...
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'happyBabel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务。
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
}),
]
}
 

前后编译速度的比较, 提升了 122ms

2.2 优化产出代码

2.2.1 小图片Base64编码
npm install url-loader image-webpack-loader -D
 

用法:

//webpack.prod.config.js
module.exports = {
...
module: {
rules: [
{
test: /.(png|jpg|gif|bmp/)$/i,
use: [
{
loader: 'url-loader',
options: {
name:'[name].[ext]',
outputPath: 'images/',
limit: 8192 //小于8192b,就可以转化成base64格式。大于就会打包成文件格式
}
},
{
loader:'image-webpack-loader',  //对图片资源进行压缩处理
}
]
}
]
}
...
}
 
2.2.2 抽离CSS代码和压缩
使用mini-css-extract-plugin 代替 extract-text-webpack-plugin , 更好的支持异步加载,重复编译,性能更好, 只针对CSS, 代码简单, 但不支持HMR
 
  • mini-css-extract-plugin
作用: 将js中import的css文件提取出来,以link方式插入html, 该方式会产生额外的Http请求
版本: webpack4.0
 
1. npm install mini-css-extract-plugin -D
2. 在webpack.prod.config.js中:
//抽离css样式 和extract-text-webpack-plugin的差异: 支持异步加载, 不重复编译,性能更好,只针对css
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
module: {
rules: [
//抽离css
{
test: /.css$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
'css-loader',
'postcss-loader'
]
},
//抽离less -> css
{
test: /.less$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
'css-loader',
'less-loader',
'postcss-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
//新增生产环境变量
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
}),
//抽离css文件
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
],
}
 
  • extract-text-webpack-plugin
作用: 处理js中import的css文件 通过css-loader、style-loader、extract-text-webpack-plugin@next将js中import的css文件以link的方式插入到html
版本: webpack3.x以下
 
1. npm install extract-text-webpack-plugin -D
1. 在webpack.prod.config.js中:
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
module.exports = {
module:{
rules:[
{
test:/.css$/,
use:ExtractTextWebpackPlugin.extract({
fallback:'style-loader',
use:'css-loader'
})
}
]
},
plugins:[
new ExtractTextWebpackPlugin({
filename: '[name]-[contentHash:8].css'
})
]
}
 
  • optimize-css-assets-webpack-plugin
npm install optimize-css-assets-webpack-plugin -D
作用: 压缩CSS代码
 

用法:

//用于优化压缩css代码
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = webpackMerge(webpackBaseConfig, {
mode: 'production',
...
optimization: {
//压缩CSS文件
minimizer: [new OptimizeCssAssetsWebpackPlugin({})],
}
})
 
2.2.3 分割公共代码

使用场景:

1. 多入口文件,需要抽出公共部分的时候。
2. 单入口文件,但是因为路由异步加载对多个子chunk, 抽离子每个children公共的部分。
3. 把第三方依赖,node_modules下所有依赖抽离为单独的部分。
4. 混合使用,既需要抽离第三方依赖,又需要抽离公共部分。
 

在 webpack4.0 之前, 采用webpack.optimize.CommonsChunkPlugin来做代码分割, 这里做一个简单的介绍使用:

//webpack.prod.config.js
module.exports = {
...
plugins:[
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
//minChunks 可以是数字、函数或者Infinity
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
})
]
...
}
 

在 webpack4.0 之后, 采用config.optimization.splitChunks 实现代码分割, 抽离公共部分, 具体的使用情况如下:

常见的使用场景:

1. 抽离第三方类库(vue, vue-router等)
2. 抽离项目中相同模块引用的代码
 

项目结构: (采用多入口模式进行模拟)

项目代码如下:

// common/util.js
export function sum(a, b) {
return a * b;
}
//index.js
import { sum } from './common/util'
const vue = require('vue');
const router = require('vue-router')
console.log(sum(10, 20));
//indexA.js
import { sum } from './common/util'
console.log(sum(40, 60));
 
//webpack.config.js 简单配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
index: path.join(__dirname, '../src/index.js'),
indexA: path.join(__dirname, '../src/indexA.js'),
},
output: {
filename: '[name].[contentHash:8].js',
path: path.join(__dirname, '../dist')
},
module: {
rules: [{
test: /.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
}]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'index.html',
filename: 'index.html',
path: path.join(__dirname, '../dist'),
chunks: ['index']
}),
new HtmlWebpackPlugin({
template: 'indexA.html',
filename: 'indexA.html',
path: path.join(__dirname, '../dist'),
chunks: ['indexA']
})
],
optimization: {
minimizer: [new TerserWebpackPlugin({})],
splitChunks: {
/**
* 需要被分割的模块
* initial 入口 chunk,对于异步导入的文件不处理
* async 异步 chunk,只对异步导入的文件处理
* all 全部 chunk
*/
chunks: 'all',
//缓存的分组
cacheGroups: {
//分割第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!!
test: /node_modules/,
//minSize 规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取。
//maxSize 把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小
minSize: 0, // 大小限制
//
minChunks: 1 // 最少复用过几次
},
//抽离共同代码模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次 index.js和indexA.js相同部分
}
}
}
}
}
 

编译后文件:

在整个构建过程中, 可以通过设置 minChunks 的限制来抽离共同代码, 控制common文件的生成, 因为minSize 默认为 3kb , 所以为了测试这个场景, 可以设置为 0

2.2.4 多进程并行压缩

由于 JS 是单线程, 为此可以通过开启多进程的方式, 来加快压缩效率; 目前使用并行压缩比较主流的三个方案如下:

1. 使用webpack-parallel-uglify-plugin, 一般搭配happyPack使用
2. 使用 uglifyjs-webpack-plugin 开启parallel 参数
3. 使用terser-webpack-plugin 开启 parallel 参数 (推荐使用这个,支持 ES6 语法压缩)
 
  • webpack-parallel-uglify-plugin
npm install webpack-parallel-uglify-plugin -D
 
//webpack.config.js配置
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
...
plugins:[
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
]
...
}
 
  • uglifyjs-webpack-plugin
npm install uglifyjs-webpack-plugin -D
 
//webpack.config.js配置
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
...
optimization: {
minimizer: [new UglifyJsPlugin()],
}
...
}
//options配置项:
module.exports = {
...
optimization: {
minimizer: [
new UglifyJsPlugin({
test: /.js(?.*)?$/i,  //测试匹配文件,
include: //includes/, //包含哪些文件
excluce: //excludes/, //不包含哪些文件
//允许过滤哪些块应该被uglified(默认情况下,所有块都是uglified)。
//返回true以uglify块,否则返回false。
chunkFilter: (chunk) => {
// `vendor` 模块不压缩
if (chunk.name === 'vendor') {
return false;
}
return true;
}
}),
cache: false,   //是否启用文件缓存,默认缓存在node_modules/.cache/uglifyjs-webpack-plugin.目录
parallel: true,  //使用多进程并行运行来提高构建速度
],
},
...
};
 
  • terser-webpack-plugin
npm install terser-webpack-plugin -D
 
//webpack.config.js配置
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
...
optimization: {
minimizer: [new TerserWebpackPlugin({
//    parallel:2
})],
}
...
}
 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Scroll Up