目录
我的博客原文:Webpack性能优化
前言
最近接到一个老的项目,使用webpack构建的,构建打包居然需要11分钟,也就意味着每次上线至少在12分钟以上,每次上线简直就是折磨,明明可以早15分钟跑路,实在忍不了了,开干,卷起来,下面我记录了此次优化的一些方法,供大家参考。
一、减少打包时间
优化 Loader
对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。
首先我们可以优化 Loader 的文件搜索范围,让尽可能少的文件被 Loader 处理。可以通过 test/include/exclude 三个配置项来命中 Loader 要应用规则的文件。
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}
对于 Babel 来说,我们肯定是希望只作用在 JS 代码上的,然后 node_modules
中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。
当然这样做还不够,我们还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。
loader: 'babel-loader?cacheDirectory=true'
HappyPack
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
安装HappyPack插件:
npm i -D happypack
配置HappyPack:
const HappyPack =require (’happypack’);
// 构造出共享进程池,在进程池中包含 5 个子进程
const happyThreadPool = HappyPack.ThreadPool({ size : 5 )) ;
/**** 使用HappyPack实例化 *****/
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
// 用唯一的标识符id来代表当前的HappyPack 处理一类特定的文件
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4,
// 使用共享进程池中的子进程去处理任务。
threadPool: happyThreadPool,
// 是否允许HappyPack输出日志,默认为true
verbose: false
})
]
属性解读:
threads: 代表开启几个子进程去处理这一类文件,默认是3个,必须是整数。
verbose: 是否允许HappyPack输出日志,默认为true。
threadPool: 代表共享进程池。即多个HappyPack实列都使用同一个共享进程池中的子进程去处理任务。以防止资源占用过多。
DllPlugin
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
接下来我们就来学习如何使用 DllPlugin
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin
将依赖文件引入项目中。
// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
代码压缩
JS代码压缩
terser
是一个JavaScript
的解释、绞肉机、压缩机的工具集,可以帮助我们压缩、丑化我们的代码,让bundle
更小。
在production
模式下,webpack
默认就是使用 TerserPlugin
来处理我们的代码的。如果想要自定义配置它,配置方法如下:
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true // 电脑cpu核数-1
})
]
}
}
属性介绍:
属性 | 描述 |
extractComments | 默认值为true,表示会将注释抽取到一个单独的文件中,开发阶段,我们可设置为 false ,不保留注释 |
parallel | 使用多进程并发运行提高构建的速度,默认值是true,并发运行的默认数量: os.cpus().length - 1 |
terserOptions | 设置我们的terser相关的配置 |
compress | 设置压缩相关的选项,mangle:设置丑化相关的选项,可以直接设置为true |
mangle | 设置丑化相关的选项,可以直接设置为true |
toplevel | 底层变量是否进行转换 |
keep_classnames | 类的名称 |
keep_fnames | 类的名称 |
CSS代码压缩
CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin
npm install css-minimizer-webpack-plugin -D
如何配置:
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
parallel: true
})
]
}
}
Html压缩
使用HtmlWebpackPlugin
插件来生成HTML
的模板时候,通过配置属性minify
进行html
优化
module.exports = {
...
plugin:[
new HtmlwebpackPlugin({
...
minify:{
minifyCSS:false, // 是否压缩css
collapseWhitespace:false, // 是否折叠空格
removeComments:true // 是否移除注释
}
})
]
}
设置了minify
,实际会使用另一个插件html-minifier-terser。
图片压缩
一般来说在打包之后,一些图片文件的大小是远远要比 js
或者 css
文件要来的大,所以图片压缩较为重要。
如何配置:
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
},
{
loader: 'image-webpack-loader',
options: {
// 压缩 jpeg 的配置
mozjpeg: {
progressive: true,
quality: 65
},
// 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
optipng: {
enabled: false,
},
// 使用 imagemin-pngquant 压缩 png
pngquant: {
quality: '65-90',
speed: 4
},
// 压缩 gif 的配置
gifsicle: {
interlaced: false,
},
// 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
webp: {
quality: 75
}
}
}
]
},
]
}
文件大小压缩
对文件的大小进行压缩,减少http
传输过程中宽带的损耗
安装插件:
npm install compression-webpack-plugin -D
如何配置:
new ComepressionPlugin({
test:/\.(css|js)$/, // 哪些文件需要压缩
threshold:500, // 设置文件多大开始压缩
minRatio:0.7, // 至少压缩的比例
algorithm:"gzip", // 采用的压缩算法
})
文件监听
在开启监听模式时,默认情况下会监听配置的 Entry 文件和所有 Entry 递归依赖的文件,在这些文件中会有很多存在于 node_modules 下,因为如今的 Web 项目会依赖大量的第三方模块, 所以在大多数情况下我们都不可能去编辑 node_modules 下的文件,而是编辑自己建立的源码文件,而一个很大的优化点就是忽略 node_modules 下的文件,不监听它们。
module.export = {
watchOptions : {
//不监听的 node_modules 目录下的文件
ignored : /node_modules/,
}
} module.export = {
watchOptions : {
//不监听的 node_modules 目录下的文件
ignored : /node_modules/,
}
}
采用这种方法优化后, Webpack 消耗的内存和 CPU 将会大大减少。
小优化点
resolve.extensions
用来表明文件后缀列表,默认查找顺序是 ['.js', '.json']
,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面。
module.exports = {
resolve : {
//尽可能减少后缀尝试的可能性
extensions : ['js'],
}
}
resolve.alias
resolve.alias 配置项通过别名来将原导入路径映射成一个新的导入路径。
在实战项目中经常会依赖一些庞大的第三方模块,以 React 库为例,发布出去的 React 库中包含两套代码
- 一套是采用 CommonJS 规范的模块化代码,这些文件都放在 lib 录下,以 package.json 中指定的入口文件 react.js 为模块的入口
- 一套是将 React 的所有相关代码打包好的完整代码放到一个单独的文件中, 这些代码没有采用模块化,可以直接执行。其中 dist/react.js 用于开发环境,里面包含检查和警告的代码。dist/react.min.js 用于线上环境,被最小化了。
在默认情况下, Webpack 会从入口文件 ./node_modules/react/react.js 开始递归解析和处理依赖的几十个文件,这会是一个很耗时的操作 通过配置 resolve.alias, 可以让 Webpack 在处理 React 库时,直接使用单独、完整的 react.min.js 文件,从而跳过耗时的递归解析操作。
module.exports = {
resolve: {
//使用 alias 将导入 react 的语句换成直接使用单独、完整的 react.min.js 文件,
//减少耗时的递归解析操作
alias: {
'react': path.resolve( __dirname ,'./node_modules/react/dist/react.min.js'),
}
}
}
module.noParse
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库如 jQuery
module.exports = {
module: {
noParse: /jquery/,
}
}; module.exports = {
module: {
noParse: /jquery/,
}
};
二、减少 Webpack 打包后的体积
按需加载
项目中都会存在十几甚至更多的路由页面。如果我们将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定是希望首页能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。
vue为例实现懒加载:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [{
path: '/',
name: 'Home',
// 将子组件加载语句封装到一个function中,将function赋给component
component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
总结:
当然不仅仅路由可以按需加载,对于 loadash
这种大型类库同样可以使用这个功能。按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise
,当 Promise
成功以后去执行回调。
Scope Hoisting
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如我们希望打包两个文件
// test.js
export const a = 1
// index.js
import { a } from './test.js'
对于这种情况,我们打包出来的代码会类似这样
[
/* 0 */
function (module, exports, require) {
//...
},
/* 1 */
function (module, exports, require) {
//...
}
]
但是如果我们使用 Scope Hoisting 的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码。
[
/* 0 */
function (module, exports, require) {
//...
}
]
这样的打包方式生成的代码明显比之前的少多了。
如何配置:
如果在 Webpack4 中你希望开启这个功能,只需要启用optimization.concatenateModules 就可以了。
module.exports = {
optimization: {
concatenateModules: true
}
}
Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码,比如Tree Shaking 可以实现删除项目中未被引用的代码,比如
// test.js
export const a = 1
export const b = 2
// index.js
import { a } from './test.js'
对于以上情况,test
文件中的变量 b
如果没有在项目中使用到的话,就不会被打包到文件中。
如何配置:
如果你使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
// 其他配置项
};
需要注意:
要确认Webpack版本、使用ES6的语法、使用正确的工具,以及在Webpack配置文件中启用Tree-Shaking。同时,需要注意在Tree-Shaking优化过程中可能会出现的一些问题,例如配合动态导入使用时需要进行额外的配置等。
CND加速
CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。
随着项目越做越大,依赖的第三方 npm 包越来越多,构建之后的文件也会越来越大。再加上又是单页应用,这就会导致在网速较慢或者服务器带宽有限的情况出现长时间的白屏。此时我们可以使用CDN的方法,优化网络加载速度。
配置如下:
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
rules:[{
test: /\.css/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'],
publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
}),
},{
test: /\.png/,
use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值
}]
},
plugins:[
new WebPlugin({
template: './template.html',
filename: 'index.html',
stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
}),
new ExtractTextPlugin({
filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
})
]
如果你的项目已经在用 Webpack ,不妨试验一下以上方案,看看项目会不会变得更加丝滑。
欢迎在评论区交流。
如果文章对你有所帮助,❤️关注+点赞❤️鼓励一下!博主会持续更新。。。。
往期回顾