全局安装 webpack-bundle-analyzer
插件
npm i -g webpack-bundle-analyzer
运行 webpack-bundle-analyzer
webpack-bundle-analyzer
// webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap(prodWebpackConfig)
开始打包,需要获取所有的依赖模块
搜索所有的依赖项,这需要占用一定的时间,即搜索时间,那么就确定了:
需要优化的第一个时间就是搜索时间。
解析所有的依赖模块(解析成浏览器可运行的代码)
Webpack 根据配置的 loader 解析相应的文件。日常开发中需要使用 loader 对 JS、CSS、图片、字体等文件做转换操作,并且转换的文件数据量也是非常大。由于 JS 单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。
需要优化的第二个时间就是解析时间。
将所有的依赖模块打包到一个文件
将所有解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减小白屏时间),所以 Webpack 会对代码进行优化。 JS 压缩是发布编译的最后阶段,通常 Webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。
需要优化的第三个时间就是压缩时间。
二次打包
当更改项目中一个小小的文件时,需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。
需要优化的第四个时间就是二次打包时间。
使用最新版本的 Webpack 和 Node.js,因为高版本的 Webpack、Node.js 在内置的 API、算法上都更优。
Webpack 打包时,会从配置的 entry
出发,解析入口文件的导入语句,再递归的解析,在遇到导入语句时 Webpack 会做两件事情:
可以通过 test
、include
、exclude
来限制 Loader 要应用的文件.
test
:配置要解析的文件类型include
:配置要解析的文件exclude
:配置不解析的文件// webpack.config.js
module.exports = {
module: {
rules: [
{
// 匹配 js、mjs 文件
test: /\.m?js$/,
// 排除 node_modules、bower_components 目录下的文件搜索
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}
resolve.modules
配置resolve.modules
用于配置 webpack 解析模块时应该搜索的目录。
resolve.modules
的默认值是 ['node_modules']
,含义是先去当前目录下的 ./node_modules
目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules
中找,再没有就去 ../../node_modules
中找,以此类推。
node_modules
,则递归到父目录下查找,直到找到// webpack.config.js
module.exports = {
resolve: {
// 将 node_modules 目录下的文件视为 module
modules: [path.resolve(__dirname, 'node_modules')],
},
};
resolve.alias
配置resolve.alias
配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。
// webpack.config.js
const path = require('path');
module.exports = {
resolve: {
alias: {
// 将当前目录下的 src 目录配置别名为 @
'@': path.resolve(__dirname, 'src'),
},
},
};
resolve.extensions
配置在导入语句没带文件后缀时,webpack 会根据 resolve.extension
自动带上后缀后去尝试询问文件是否存在。
// webpack.config.js
module.exports = {
resolve: {
// 在解析未带后缀的文件时,会按照 .js -> .css 的顺序进行查找
extensions: ['.js', '.css'],
},
};
resolve.mainFields
配置有一些第三方模块会针对不同环境提供几份代码。 Webpack 会根据 mainFields
的配置去决定优先采用哪份代码:
// webpack.config.js
module.exports = {
resolve: {
// 先采用 jsnext:main 的代码,再采用 main 的代码
mainFields: ['jsnext:main', 'main'],
},
};
module.noParse
配置module.noParse
配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。
原因是一些库,例如:JQuery 、Lodash, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
// webpack.config.js
module.exports = {
module: {
// 忽略 jquery 和 lodash 库
noParse: /jquery|lodash/,
// 第二种写法
noParse: (content) => /jquery|lodash/.test(content),
},
};
resolve.symlinks
配置如果不使用 symlinks(例如:npm link
或者 yarn link
),可以设置 resolve.symlinks: false
。
// webpack.config.js
module.exports = {
resolve: {
symlinks: false
},
};
如果使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false
。
// webpack.config.js
module.exports = {
resolve: {
cacheWithContext: false
},
};
安装 thread-loader
npm i thread-loader
配置 webpack.config.js
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
"thread-loader", // 一定要放在其他 loader 后面
],
},
],
},
};
使用 Webpack 资源模块(asset module)代替旧的 Assets Loader(例如:file-loader
/url-loader
/raw-loader
等),减少 Loader 配置数量。
HappyPack 能让 Webpack 多线程解析 Loader,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
安装 happypack
npm i -D happypack
配置 webpack.config.js
// webpack.config.js
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
// 把对 css 文件的处理转交给 id 为 css 的 HappyPack 实例
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
id: 'css',
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};
安装 webpack-parallel-uglify-plugin
npm i -D webpack-parallel-uglify-plugin
配置 webpack.config.js
// webpack.config.js
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在 UglifyJs 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 console 语句,可以兼容 ie 浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};
安装 terser-webpack-plugin
npm i terser-webpack-plugin
配置 webpack.config.js
// webpack.config.js
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4,
terserOptions: {
parse: {
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
})
],
},
};
// webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({ parallel: 4, }),
],
}
}
// webpack.config.js
module.exports = {
optimization: {
/**
* 把 JS 文件打包成3中类型:
* 1. vendor:第三方 lib 库,基本不会改动,除非依赖版本升级
* 2. common:业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}:不同页面 entry 里业务组件代码的差异部分,会经常改动
* 这样分的好处是尽量按改动频率来区分,利用好浏览器缓存
*/
splitChunks: {
chunks: 'all',
maxInitialRequests: 4, // 防止切分粒度过细,请求过多,约束为4
cacheGroups: {
vendor: { // 第三方
test: /[\\/]node_modules[\\/]/, // 打包 node_module 中的文件
name: 'vendor', // 模块名称
priority: 10, // 优先级 数字越大 优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 重用已有模块
},
common: { // 公共模块
name: 'common',
minChunks: 2, // 被引用两处即被归纳到公共模块
minSize: 1, // 最小 size
priority: 5, // 优先级
reuseExistingChunk: true, // 重用已有模块
},
},
},
// 将 webpack 运行时生成代码打包到 runtime.js
runtimeChunk: true,
},
}
Webpack v4 移除,通过 optimization.splitChunks 替代。
// webpack.config.js
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
module.exports = {
plugins: [
new CommonsChunkPlugin({
chunks: ['common', 'base'], // 从 common 和 base 两个现成的 Chunk 中提取公共的部分
name: 'base' // 把公共的部分放到 base 中
})
]
}
Tree Shaking 是指打包时移除哪些没有使用(未引入)的代码。
代码中通过 import()
按需加载 Chunk
// main.js
window.document.getElementById('btn').addEventListener('click', function () {
// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
import(/* webpackChunkName: "show" */ './show').then((show) => {
show('Webpack');
})
});
在 webpack.config.js
中配置动态 Chunk 的输出
// webpack.config.js
module.exports = {
entry: {
main: './main.js',
},
output: {
filename: '[name].js',
// 为动态加载的 Chunk 配置输出文件的名称
chunkFilename: '[name].js',
}
};
Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。
// webpack.config.js
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};
增加初次构建时间,缩短后续构建时间。
cache-loader
,HardSourceWebpackPlugin
、babel-loader
的 cacheDirectory
标志等。// webpack.config.js
module.exports = {
cache: {
type: 'filesystem',
},
};
// webpack_dll.config.js - 构建出动态链接库文件
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry: {
// 把 React 相关模块的放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPluginnew
DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
],
};
// webpack.config.js
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
};
// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, '../dist'),
// 给 js 文件加上 contenthash
filename: 'js/chunk-[contenthash].js',
clean: true,
},
}
[fullhash]/[chunkhash]/[contenthash]
等工具。webpack-dev-server
等插件。webpack.dev.config.js
// webpack.dev.config.js
const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
// 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用const autoWebPlugin = new AutoWebPlugin('./src/pages', {
// HTML 模版文件所在的文件路径
template: './template.html',
// 提取出所有页面公共的代码
commonsChunk: {
// 提取出公共代码 Chunk 的名称
name: 'common',
},
});
module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './src/base.js',
}),
output: {
filename: '[name].js',
},
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件,使用 Tree Shaking 优化// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['jsnext:main', 'main'],
},
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// 使用 HappyPack 加速构建
use: ['happypack/loader?id=babel'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
{
test: /\.js$/,
use: ['happypack/loader?id=ui-component'],
include: path.resolve(__dirname, 'src'),
},
{
// 增加对 CSS 文件的支持
test: /\.css$/,
use: ['happypack/loader?id=css'],
},
]
},
plugins: [
autoWebPlugin,
// 使用 HappyPack 加速构建new HappyPack({
id: 'babel',
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
// UI 组件加载拆分
id: 'ui-component',
loaders: [{
loader: 'ui-component-loader',
options: {
lib: 'antd',
style: 'style/index.css',
camel2: '-'
}
}],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['style-loader', 'css-loader'],
}),
// 提取公共代码new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
],
watchOptions: {
// 使用自动刷新:不监听的 node_modules 目录下的文件
ignored: /node_modules/,
}
};
webpack.prod.config.js
// webpack.prod.config.js
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const { AutoWebPlugin } = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
// HTML 模版文件所在的文件路径
template: './template.html',
// 提取出所有页面公共的代码
commonsChunk: {
// 提取出公共代码 Chunk 的名称
name: 'common',
},
// 指定存放 CSS 文件的 CDN 目录 URL
stylePublicPath: '//css.cdn.com/id/',
});
module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './src/base.js',
}),
output: {
// 给输出的文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目录 URL
publicPath: '//js.cdn.com/id/',
},
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, 'node_modules')],
// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
mainFields: ['jsnext:main', 'main'],
},
module: {
rules: [
{
// 如果项目源码中只有 js 文件就不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// 使用 HappyPack 加速构建
use: ['happypack/loader?id=babel'],
// 只对项目根目录下的 src 目录中的文件采用 babel-loader
include: path.resolve(__dirname, 'src'),
},
{
test: /\.js$/,
use: ['happypack/loader?id=ui-component'],
include: path.resolve(__dirname, 'src'),
},
{
// 增加对 CSS 文件的支持
test: /\.css$/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
// 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
publicPath: '//img.cdn.com/id/'
}),
},
]
},
plugins: [
autoWebPlugin,
// 开启ScopeHoisting
new ModuleConcatenationPlugin(),
// 使用HappyPack
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
// UI 组件加载拆分
id: 'ui-component',
loaders: [{
loader: 'ui-component-loader',
options: {
lib: 'antd',
style: 'style/index.css',
camel2: '-'
}
}],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
// 通过 minimize 选项压缩 CSS 代码
loaders: ['css-loader?minimize'],
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 提取公共代码
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在 UglifyJs 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
]
};