三种hash策略
hash
hash和每次 build有关,没有任何改变的情况下,每次编译出来的hash都是一样的,但当你改变了任何一点东西,它的hash就会发生改变。简单理解,你改了任何东西,hash 就会和上次不一样了。
chunkhash
chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的代码块(chunk),生成对应的哈希值,某文件变化时只有该文件对应代码块(chunk)的hash会变化contenthash
它的出现主要是为了解决,让
css文件不受js文件的影响。比如foo.css被foo.js引用了,所以它们共用相同的chunkhash值。但这样子是有问题的,如果foo.js修改了代码,css文件就算内容没有任何改变,由于是该模块的hash发生了改变,其css文件的hash也会随之改变。这个时候我们就可以使用
contenthash了,保证即使css文件所处的模块里有任何内容的改变,只要css文件内容不变,那么它的hash就不会发生变化。contenthash你可以简单理解为是moduleId + content所生成的hash。
优化策略
优化loader查找范围
在loader中结合test include exclude三个配置项来缩⼩loader的处理范围,推荐include
// ...
rules: [
{ test: /\.js$/, use: 'babel-loader', exclude: '/node_modules/' },
]
// ...
exclude优先级要优于include和test,所以当三者配置有冲突时,exclude会优先于其他两个配置。
优化resolve.modules配置
resolve.modules⽤于配置webpack去哪些⽬录下寻找第三⽅模块,默认是[‘node_modules’]
寻找第三⽅模块,默认是在当前项⽬⽬录下的node_modules⾥⾯去找,如果没有找到,就会去上⼀级⽬录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制很类似。
如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。
module.exports={
resolve:{
modules: [path.resolve(__dirname, "./node_modules")]
}
}
优化resolve.alias配置
resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径,拿react为例,我们引⼊的react库,⼀般存在两套代码
cjs
采⽤commonJS规范的模块化代码
umd
已经打包好的完整代码,没有采⽤模块化,可以直接执⾏
默认情况下,webpack会从⼊⼝⽂件./node_modules/bin/react/index开始递归解析和处理依赖的⽂件。我们可以直接指定⽂件,避免这处的耗时。
resolve: {
alias: {
"@assets": path.resolve(__dirname, "../src/assets"),
"@src": path.join(__dirname, "./src"),
"react": path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),
"react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js")
},
},
优化resolve.extensions配置
resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在
默认值:
extensions:['.js','.json','.jsx','.ts']
- 后缀尝试列表尽量的⼩
- 导⼊语句尽量的带上后缀
利⽤多线程提升构建速度
由于运⾏在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的事情需要⼀件⼀件的做,不能多件事⼀起做。我们需要 Webpack 能同⼀时间处理多个任务,发挥多核 CPU 电脑的威⼒。
thread-loader是针对 loader 进⾏优化的,它会将 loader 放置在⼀个 worker 池⾥⾯运⾏,以达到多线程构建。thread-loader 在使⽤的时候,需要将其放置在其他 loader 之前,如下⾯实例:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader'
// 你的⾼开销的loader放置在这后面 (e.g babel-loader)
]
}
]
}
};
缓存cache相关
Webpack 中打包的核⼼是 JavaScript ⽂件的打包,JavaScript 使⽤的是 babel-loader,其实打包时间⻓很多时候是babel-loader 执⾏慢导致的。这时候我们不仅要使⽤ exclude 和 include 来尽可能准确的指定要转换内容的范畴,还需要关注 babel-loader 在执⾏的时候,可能会产⽣⼀些运⾏期间重复的公共⽂件,造成代码体积⼤冗余,同时也会减慢编译的速度。
babel-loader提供了 cacheDirectory 配置给 Babel 编译时给定的⽬录,并且将⽤于缓存加载器的结果,但是这个设置默认是 false 关闭的状态,我们需要设置为 true ,这样 babel-loader 将使⽤默认的缓存⽬录 。node_modules/.cache/babel-loader ,如果在任何根⽬录下都没有找到 node_modules ⽬录,将会降级回退到操作系统默认的临时⽂件⽬录。
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true
},
}
];
压缩速度优化
相对于构建过程⽽⾔,压缩相对我们来说只有⽣产环境打包才会做,⽽且压缩我们除了添加 cache 和多线程⽀持之外,可以优化的空间较⼩。我们在使⽤ terser-webpack-plugin 的时候可以通过下⾯的配置开启多线程和缓存:
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
cache: true, // 开启缓存
parallel: true // 多线程
})
]
}
};
使⽤externals优化cdn静态资源
公司有cdn,静态资源有部署到cdn有链接了,我们使⽤cdn,我们的bundle⽂件⾥,就不⽤打包进去这个依赖了,体积会⼩很多,我们可以将⼀些JS⽂件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在index.html中通过标签引⼊,如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">root</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>
我们希望在使⽤时,仍然可以通过 import 的⽅式去引⽤(如 import $ from 'jquery' ),并且希望webpack 不会对其进⾏打包,此时就可以配置 externals 。
module.exports = {
//...
externals: {
//jquery通过script引⼊之后,全局中即有了 jQuery 变量
'jquery': 'jQuery'
}
}
使⽤静态资源路径publicPath(CDN)
CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把⽹⻚的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。
// webpack.config.js
output:{
publicPath: '//cdnURL.com', //指定存放JS⽂件的CDN地址
}
代码压缩
借助postcss和cssnano,实现css代码的压缩,借助terser-webpack-plugin,实现js的压缩,借助html-webpack-plugin,实现html的压缩
压缩html
plugin: [ new htmlWebpackPlugin({ title: "京东商城", template: "./index.html", filename: "index.html", minify: { // 压缩HTML⽂件 removeComments: true, // 移除HTML中的注释 collapseWhitespace: true, // 删除空⽩符与换⾏符 minifyCSS: true // 压缩内联css } }), ]压缩css
webpack.config.js{ test: /\.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] },postcss.config.jsmodule.exports = { plugins: [ require('autoprefixer'), require('cssnano') ] };压缩js
在
mode=production下,Webpack会⾃动压缩代码,我们可以⾃定义⾃⼰的压缩⼯具,这⾥推荐terser-webpack-plugin,它是Webpack官⽅维护的插件,使⽤terser来压缩JavaScript代码。UglifyJS在压缩ES5⽅⾯做的很优秀,但是随着ES6语法的普及,UglifyJS在ES6代码压缩上做的不够好,所以有了uglify-es项⽬,但是之后uglify-es项⽬不在维护了,terser是从uglify-es项⽬拉的⼀个分⽀,来继续维护。const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [ new TerserPlugin() ] } };在实际开发中,我们可以通过移除⼀些不⽤的代码从⽽达到优化代码体积的作⽤,
Tree-Shaking也是依赖这个插件的new TerserPlugin({ // 使⽤ cache,加快⼆次构建速度 cache: true, terserOptions: { comments: false, compress: { // 删除⽆⽤的代码 unused: true, // 删掉 debugger drop_debugger: true, // eslint-disable-line // 移除 console drop_console: true, // eslint-disable-line // 移除⽆⽤的代码 dead_code: true // eslint-disable-line } } });压缩是发布前处理最耗时间的⼀个步骤,在
Webpack配置中可以通过开启terser-webpack-plugin的多线程压缩来加速我们的构建压缩速度:const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimizer: [new TerserPlugin( parallel: true // 多线程 )], }, };
tree Shaking:擦除⽆⽤的JS,CSS
webpack4.x开始⽀持 tree shaking概念,顾名思义,”摇树”,清除⽆⽤ css,js(Dead Code)Dead Code ⼀般具有以下⼏个特征
代码不会被执⾏,不可到达
代码执⾏的结果不会被⽤到
代码只会影响死变量(只写不读)
Js tree shaking只⽀持ES module的引⼊⽅式!!!!
Css tree shaking
npm install glob-all purify-css purifycss-webpack -Dconst PurifyCSS = require('purifycss-webpack') const glob = require('glob-all') // ... plugins: [ // 清除⽆⽤ css new PurifyCSS({ paths: glob.sync([ // 要做 CSS Tree Shaking 的路径⽂件 path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html ⽂件进⾏ tree shaking path.resolve(__dirname, './src/*.js') ]) }) ] // ...JS tree shaking
只⽀持
import⽅式引⼊,不⽀持commonjs的⽅式引⼊⽣产模式不需要配置,默认开启
// ... optimization: { usedExports: true // 哪些导出的模块被使⽤了,再做打包 } // ...只要
mode是production就会⽣效,develpoment的tree shaking是不⽣效的,因为webpack为了⽅便你的调试sideEffects处理副作⽤
//package.json"sideEffects":false正常对所有模块进⾏
tree shaking, 仅⽣产模式有效,需要配合usedExports使用或者 在数组⾥⾯排除不需要
tree shaking的模块"sideEffects": ['*.css','@babel/polyfill']
代码分割 code Splitting
单⻚⾯应⽤spa:
打包完后,所有⻚⾯只⽣成了⼀个bundle.js
- 代码体积变⼤,不利于下载
- 没有合理利⽤浏览器资源
多⻚⾯应⽤mpa:
如果多个⻚⾯引⼊了⼀些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要
下载⼀次就缓存起来了,避免了重复下载。
其实code Splitting概念 与 webpack并没有直接的关系,只不过webpack中提供了⼀种更加⽅便的⽅法供我们实现代码分割
webpack的配置基于split-chunks-plugin
optimization: {
splitChunks: {
chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为⼀个单独的⽂件
},
},
optimization: {
splitChunks: {
chunks: 'async', //对同步 initial,异步 async,所有的模块有效 all
minSize: 30000, //最⼩尺⼨,当模块⼤于30kb
maxSize: 0, //对模块进⾏⼆次分割时使⽤,不推荐使⽤
minChunks: 1, //打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
maxAsyncRequests: 5, //最⼤异步请求数,默认5
maxInitialRequests: 3, //最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
automaticNameDelimiter: '-', //打包分割符号
name: true, //打包后的名称,除了布尔值,还可以接收⼀个函数function
cacheGroups: { //缓存组
vendors: {
test: /[\\/]node_modules[\\/]/, // 判断引入库是否是node_modules里的
name: "vendor", // 要缓存的 分隔出来的 chunk 名称
priority: -10, //缓存组优先级 数字越⼤,优先级越⾼
// filename: 'vendor.min.js' // 设置代码分割后的文件名,仅在chunks为initial时能用,否则报错
},
other:{
chunks: "initial", // 必须三选⼀: "initial" | "all" | "async" (默认就是async)
test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk,
name:"other",
minSize: 30000,
minChunks: 1,
},
default: {
minChunks: 2, // 在拆分之前共享模块的最小块数, 默认为1,表示只要有一个地方引入,就抽离到公共模块,设置为2表示 有两个chunk 引入同一个人文件才进行抽离,设置2更为合理一点
priority: -20,
reuseExistingChunk: true // 允许在模块完全匹配时重用现有的块,而不是创建新的块
}
}
}
}
webpack-bundle-analyzer: 分析webpack打包后的模块依赖关系:
// npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundleanalyzer').BundleAnalyzerPlugin;
module.exports = merge(baseWebpackConfig, {
//....
plugins: [
//...
new BundleAnalyzerPlugin(),
]
})
development vs Production模式区分打包
区分环境打包可以创建多个不同环境的config文件,执行打包命令的时候指定对应的config文件,我们可以定义一个基础文件webpack.common.config.js存放公用的配置,然后再创建其他环境的配置文件,例如webpack.prod.config.js、webpack.dev.config.js、webpack.test.config.js等,在里面进行公共配置的合并
例如:
const merge = require("webpack-merge")
const commonConfig = require("./webpack.common.js")
const devConfig = {
...
}
module.exports = merge(commonConfig,devConfig)
package.js
"scripts":{
"dev": "webpack --config ./webpack.dev.config.js",
"build": "webpack --config ./webpack.prod.config.js"
}
还可以借助cross-env基于环境变量区分,由于不同平台之间环境变量稍有不同,因此需要借助这个工具进行统一,这个工具可以在我们执行打包命令的时候,自动修改node的process.env环境变量的配置,比如
"scripts":{
"dev": "cross-env NODE_ENV='development' webpack --config ./webpack.dev.config.js",
"build": "cross-env NODE_ENV='production' webpack --config ./webpack.prod.config.js"
}
我们执行npm run dev的时候,process.env.NODE_ENV 被设置为development,而执行npm run build的时候process.env.NODE_ENV 被设置为production,然后我们可以根据这个环境变量,只写一份webpack.config.js配置,实现不同环境环境的打包;
例如:
const webpackEnv = process.env.NODE_ENV;
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
module.exports = {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// ...
}
webpack.config.js还可以导出一个函数,函数接收两个入参:
- env:当前运行
webpack的环境变量, 设置它时需要在启动webpack时候带上参数, 例如:webpack --env.production; - args:代表在
webpack启动时候通过命令行传入的所有参数; 例如:--config--env--devtool等;
例如:
"scripts": {
"dev": "webpack --env development --config ./webpack.dev.config.js",
},
module.exports = function(env, args) {
console.log(env, args)
const isEnvDevelopment = env === 'development';
const isEnvProduction = env === 'production';
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// ...
}
}
env、args打印结果
development // env
{ // args
_: [],
cache: null,
bail: null,
profile: null,
color: { level: 3, hasBasic: true, has256: true, has16m: true },
colors: { level: 3, hasBasic: true, has256: true, has16m: true },
env: 'development',
config: './webpack.dev.config.js',
'info-verbosity': 'info',
infoVerbosity: 'info',
'$0': 'node_modules\\webpack\\bin\\webpack.js'
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 lyucan_1@163.com