深入浅出 webpack
最近在写一下对自己的思考,发现还有两篇草稿没发,记得应该是去年年初的时候写的,后来公司一直忙,就很少来社区了,今天先发一篇了,这篇记得当时还找到作者,加了微信
webpack 项目优化
webpack 版本不同,配置也会有一些地方不一样的,这里是 webpack 4
- 1.优化构建速度。在项目庞大时构建耗时可能会变的很长,每次等待构建的耗时加起来也会是个大数目。
- 4-1 缩小文件搜索范围
- 4-2 使用 DllPlugin
- 4-3 使用 HappyPack
- 4-4 使用 ParallelUglifyPlugin
- 2.优化使用体验。通过自动化手段完成一些重复的工作,让我们专注于解决问题本身。
- 4-5 使用自动刷新
- 4-6 开启模块热替换
- 3.优化输出质量 优化输出质量的目的是为了给用户呈现体验更好的网页,例如减少首屏加载时间、提升性能流畅度等。
这至关重要,因为在互联网行业竞争日益激烈的今天,这可能关系到你的产品的生死。 优化输出质量本质是优化构建输出的要发布到线上的代码,分为以下几点:- 减少用户能感知到的加载时间,也就是首屏加载时间。
- 4-7 区分环境
- 4-8 压缩代码
- 4-9 CDN 加速
- 4-10 使用 Tree Shaking
- 4-11 提取公共代码
- 4-12 按需加载
- 提升流畅度,也就是提升代码性能。
- 4-13 使用 Prepack
- 4-14 开启 Scope Hoisting
- 优化的关键是找出问题所在,这样才能一针见血,
- 4-15 输出分析 教你如何利用工具快速找出问题所在。
- 4-16 优化总结 对以上的优化方法做一个总结
- 减少用户能感知到的加载时间,也就是首屏加载时间。
4-1 缩小文件搜索范围
- 4-1-1 优化 loader 配置
为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
复制代码
- 4-1-2 优化 resolve.alias 配置
- 4-1-3 优化 resolve.extensions 配置
4-2 使用DllPlugin
用过 Windows 系统的人应该会经常看到以 .dll 为后缀的文件,这些文件称为动态链接库,
在一个动态链接库中可以包含给其他模块调用的函数和数据。要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库需要被加载。
为什么给 Web 项目构建接入动态链接库的思想后,会大大提升构建速度呢? 原因在于包含大量复用模块的动态链接库只需要编译一次,
在之后的构建过程中被动态链接库包含的模块将不会在重新编译,
而是直接使用动态链接库中的代码
4-3 使用 HappyPack
分解任务和管理线程的事情 HappyPack 都会帮你做好
4-4 使用 ParallelUglifyPlugin
用过 UglifyJS 的你一定会发现在构建用于开发环境的代码时很快就能完成,
但在构建用于线上的代码时构建一直卡在一个时间点迟迟没有反应,其实卡住的这个时候就是在进行代码压缩。由于压缩 JavaScript 代码需要先把代码解析成用 Object 抽象表示的 AST 语法树,
再去应用各种规则分析和处理 AST,导致这个过程计算量巨大,耗时非常多。为什么不把在4-3 使用 HappyPack中介绍过的多进程并行处理的思想也引入到代码压缩中呢?
ParallelUglifyPlugin 就做了这个事情。
当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个个挨着压缩再输出,
但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,
每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。
所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。使用 ParallelUglifyPlugin 也非常简单,把原来 Webpack 配置文件中内置的 UglifyJsPlugin 去掉后,再替换成 ParallelUglifyPlugin,
不过看到 GitHub 上说是支持并行的,uglifyjs-webpack-plugin/#parallel
4-5 使用自动刷新
要让 Webpack 开启监听模式,有两种方式: 在配置文件
webpack.*.config.js
中设置watch: true
。 在执行启动Webpack
命令时,带上--watch
参数,完整命令是webpack --watch
文件监听工作原理: 在 Webpack 中监听一个文件发生变化的原理是定时的去获取这个文件的最后编辑时间,
每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。
配置项中的 watchOptions.poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次。
- 优化文件监听性能
watchOptions: {
// 不监听的 node_modules 目录下的文件
ignored: /node_modules/,
}
复制代码
4-6 开启模块热替换
webpack 内置插件
HotModuleReplacementPlugin
,
配置 devServer
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
复制代码
要优化模块热替换的构建性能,思路和在4-5 使用自动刷新中提到的很类似:
监听更少的文件,忽略掉 node_modules 目录下的文件。
但是其中提到的关闭默认的 inline 模式手动注入代理客户端的优化方法不能用于在使用模块热替换的情况下,
原因在于模块热替换的运行依赖在每个 Chunk 中都包含代理客户端的代码。
4-7 区分环境
4-8 压缩代码
要在 Webpack 中接入 UglifyJS 需要通过插件的形式,目前有两个成熟的插件,分别是:
UglifyJsPlugin:通过封装 UglifyJS 实现压缩。
ParallelUglifyPlugin:多进程并行处理压缩
- 压缩 CSS
把 cssnano 接入到 Webpack 中也非常简单,因为 css-loader 已经将其内置了,
要开启 cssnano 去压缩代码只需要开启 css-loader 的 minimize 选项
const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 通过 minimize 选项压缩 CSS 代码
use: ['css-loader?minimize']
}),
},
]
},
plugins: [
// 用 WebPlugin 生成对应的 HTML 文件
new WebPlugin({
template: './template.html', // HTML 模版文件所在的文件路径
filename: 'index.html' // 输出的 HTML 的文件名称
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 Hash 值
}),
],
}
复制代码
```javascript
+ 压缩 ES6
## 4-9 CDN 加速
> 之前的相对路径,都变成了绝对的指向 CDN 服务的 URL 地址,配置中的path 也需要换成 CDN 地址前缀
## 4-10 使用 Tree Shaking
> Tree Shaking 可以用来剔除 JavaScript 中用不上的死代码。它依赖静态的 ES6 模块化语法
## 4-11 提取公共代码
> Webpack 内置了专门用于提取多个 Chunk 中公共部分的插件 CommonsChunkPlugin,CommonsChunkPlugin 大致使用方法如下:
</code></pre>
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
new CommonsChunkPlugin({
// 从哪些 Chunk 中提取
chunks: ['a', 'b'],
// 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
name: 'common'
})
复制代码
<pre><code class="language-javascript line-numbers"><br />## 4-12 按需加载
> router 按需加载
## 4-13 使用 Prepack
> 在前面的优化方法中提到了代码压缩和分块,这些都是在网络加载层面的优化,
> 除此之外还可以优化代码在运行时的效率,Prepack 就是为此而生。
> Prepack 由 Facebook 开源,它采用较为激进的方法:
> 在保持运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。
> 实际上 Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。
> Prepack 通过在编译阶段预先执行了源码得到执行结果,再直接把运行结果输出来以提升性能
+ Prepack 的工作原理和流程大致如下:
> 通过 Babel 把 JavaScript 源码解析成抽象语法树(AST),以方便更细粒度地分析源码;
> Prepack 实现了一个 JavaScript 解释器,用于执行源码。
> 借助这个解释器 Prepack 才能掌握源码具体是如何执行的,并把执行过程中的结果返回到输出中。
> 从表面上看去这似乎非常美好,但实际上 Prepack 还不够成熟与完善。
+ Prepack 目前还处于初期的开发阶段,局限性也很大,例如:
+ 不能识别 DOM API 和 部分 Node.js API,如果源码中有调用依赖运行环境的 API 就会导致 Prepack 报错;
+ 存在优化后的代码性能反而更低的情况;
+ 存在优化后的代码文件尺寸大大增加的情况。
+ 总之,现在把 Prepack 用于线上环境还为时过早
+ 接入 Webpack
</code></pre>
<pre><code> const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
module.exports = {
plugins: [
new PrepackWebpackPlugin()
]
};
复制代码
```
4-14 开启 Scope Hoisting
Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,
是在 Webpack3 中新推出的功能好处是:
- 代码体积更小,因为函数申明语句会产生大量代码;
-
代码在运行时因为创建的函数作用域更少了,内存开销也随之变小。
-
Scope Hoisting 的实现原理其实很简单: 分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。
因此只有那些被引用了一次的模块才能被合并。
由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码必须采用 ES6 模块化语句,不然它将无法生效。
原因和4-10 使用 TreeShaking 中介绍的类似。
4-15 输出分析
为了更简单直观的分析输出结果,社区中出现了许多可视化的分析工具。
这些工具以图形的方式把结果更加直观的展示出来,让你快速看到问题所在。两种分析工具:
- 1.官方的可视化分析工具: Webpack Analyse: 在线 Web 应用
- 2.webpack-bundle-analyzer:
4-15-1 生成 stats.json
在启动 Webpack 时带上以上两个参数,启动命令如下:
webpack --profile --json > stats.json,
复制代码
如果没有问题,你会发现项目中多出了一个 stats.json 文件。
这个 stats.json 文件是给后面介绍的可视化分析工具使用的。
可是我在 vue 项目中使用时出现了一个问题
web>webpack --profile --json > stats.json
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory
.
Use --help to display the CLI options.
复制代码
- 它的意思是,假如没有指定配置文件,会在当前目录寻找webpack.config.js 作为配置文件
- 解决: 使用 config 指定配置文件,
webpack --config ./build/webpack.dev.conf.js --json > stats.json
复制代码
webpack –profile –json 会输出字符串形式的 JSON,
stats.json 是 UNIX/Linux 系统中的管道命令,
含义是把 webpack –profile –json 输出的内容通过管道输出到 stats.json 文件中。
4-15-2 官方的可视化分析工具: Webpack Analyse: 在线 Web 应用
打开 Webpack Analyse 链接的网页后,你就会看到一个弹窗提示你上传 JSON 文件,
也就是需要上传上面讲到的 stats.json 文件
4-15-3 webpack-bundle-analyzer
发现 vue-cli 2 版本中 webpack.prod.conf.js 里面有关于是否开启 webpack-bundle-analyzer 配置; 也就是说
npm run build --report
的时候,BundleAnalyzerPlugin
能以可视化的方式展示打包结果;如果单独使用 webpack-bundle-analyzer:
- 1.安装 webpack-bundle-analyzer 到全局,执行命令 npm i -g webpack-bundle-analyzer;
- 2.按照上面提到的方法生成 stats.json 文件;
- 3.在项目根目录中执行 webpack-bundle-analyzer 后,浏览器会打开对应网页看到以上效果
4-16 优化总结
按照开发环境和线上环境为该项目配置了两份文件,下面是使用
webpack4
版本
- 侧重优化开发体验的配置文件 webpack.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'],
}),
// 4-11提取公共代码
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
],
watchOptions: {
// 4-5使用自动刷新:不监听的 node_modules 目录下的文件
ignored: /node_modules/,
}
};
复制代码
- 侧重优化输出质量的配置文件 webpack-dist.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,
// 4-14开启ScopeHoisting
new ModuleConcatenationPlugin(),
// 4-3使用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`,
}),
// 4-11提取公共代码
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,
}
},
}),
]
};
复制代码
吴浩麟拥有本书的著作权。
其它人不能将本书用于商用用途,不能转载,不能以任何形式发行,违者将追究法律责任。
参考
作者:fairyly
链接:https://juejin.im/post/5cd19a9bf265da03904c2d61
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com