共计 15148 个字符,预计需要花费 38 分钟才能阅读完成。
webpack
如何打包资源优化你有理解吗?或者一个常常被问的面试题,首屏加载如何优化,其实无非就是从 http
申请、文件资源
、 图片加载
、 路由懒加载
、 预申请
, 缓存
这些方向来优化,通常在应用脚手架中,成熟的脚手架曾经给你做了最大的优化,比方压缩资源,代码的 tree shaking
等。
本文是笔者依据以往教训以及浏览官网文档总结的一篇对于 webpack 打包
方面的长文笔记,心愿在我的项目中有所帮忙。
注释开始 …
在浏览之前,本文将从以下几个点去探讨 webpack 的打包优化
1、webpack
如何做treeShaking
2、webpack
的 gizp 压缩
3、css
如何做treeShaking
,
4、入口依赖文件 拆包
5、图片资源
加载优化
treeShaking
在官网中有提到 treeShaking, 从名字上中文解释就是摇树,就是利用 esModule
的个性,删除上下文未援用的代码。因为 webpack 能够依据 esModule
做动态剖析,自身来说它是打包编译前输入,所以 webpack
在编译 esModule
的代码时就能够做上下文未援用的删除操作。
那么如何做treeshaking
? 咱们来剖析下
疾速初始化一个 webpack 我的项目
在之前咱们都是手动配置搭建 webpack
我的项目,webpack
官网提供了 cli
疾速构建根本模版,无需像之前一样手动配置 entry
、plugins
、loader
等
首先装置npm i webpack webpack-cli
,命令行执行 `
npx webpack init
一系列初始化操作后,就生成以下代码了
默认的webpack.config.js
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");
const isProduction = process.env.NODE_ENV == "production";
const stylesHandler = MiniCssExtractPlugin.loader;
const config = {
entry: "./src/index.js",
output: {path: path.resolve(__dirname, "dist"),
},
devServer: {
open: true,
host: "localhost",
},
plugins: [
new HtmlWebpackPlugin({template: "index.html",}),
new MiniCssExtractPlugin(),
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
module: {
rules: [
{test: /\.(js|jsx)$/i,
loader: "babel-loader",
},
{
test: /\.less$/i,
use: [stylesHandler, "css-loader", "postcss-loader", "less-loader"],
},
{
test: /\.css$/i,
use: [stylesHandler, "css-loader", "postcss-loader"],
},
{test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: "asset",
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
};
module.exports = () => {if (isProduction) {
config.mode = "production";
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {config.mode = "development";}
return config;
};
运行命令npm run serve
当初批改一下 index.js
, 并在src
中减少 utils
目录
// utils/index.js
export function add(a, b) {return a + b}
export function square(x) {return x * x;}
index.js
import {add} from './utils'
console.log("Hello World!");
console.log(add(1, 2))
在 index.js
中我只引入了 add
,相当于square
这个函数在上下文中并未援用。
usedExports
不过我还须要改下webpack.config.js
...
module.exports = () => {if (isProduction) {
config.mode = "production";
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = "development";
config.devtool = 'source-map'
config.optimization = {usedExports: true}
}
return config;
};
留神我只减少了 devtool:source-map
与optimization.usedExports = true
咱们看下package.json
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"build": "webpack --mode=production --node-env=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"watch": "webpack --watch",
"serve": "webpack serve"
},
默认初始化曾经给们预设了多个不同的打包环境,因而我只须要运行上面命令就能够抉择开发环境了
npm run build:dev
此时咱们看到打包后的代码未引入的 square
有一行正文
/* unused harmony export square */
function add(a, b) {return a + b;}
function square(x) {return x * x;}
square
上下文未援用,尽管给了标记,然而未真正革除。
光应用 usedExports:true
还不行,usedExports 依赖于 terser 去检测语句中的副作用
, 因而须要借助terser
插件一起应用,官网 webpack5
提供了 TerserWebpackPlugin
这样一个插件
在 webpack.config.js
中引入
...
const TerserPlugin = require("terser-webpack-plugin");
...
module.exports = () => {if (isProduction) {
config.mode = "production";
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = "development";
config.devtool = 'source-map'
config.optimization = {
usedExports: true, // 设置为 true 通知 webpack 会做 treeshaking
minimize: true, // 开启 terser
minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
}
}
return config;
};
你会发现,那个 square
函数就没有了
如果我将 usedExports.usedExports = false
, 你会发现square
没有被删除。
官网解释,当咱们设置 optimization.usedExports
必须为true
, 当咱们设置usedExports:true
,且必须开起minimize: true
,这样才会把上下文未应用的代码给革除掉,如果minimize: false
, 那么压缩插件将会生效。
当咱们设置usedExports: true
此时生成打包的代码会有一个这样的魔法正文,square
未应用
/* unused harmony export square */
function add(a, b) {return a + b;}
function square(x) {return x * x;}
当咱们设置 minimize: true
时,webpack5
会默认开启 terser
压缩,而后发现有这样的 unused harmony export square
就会删掉对应未引入的代码。
sideEffects
这个是 usedExports
摇树的另一种计划,usedExports
是查看上下文有没有援用,如果没有援用,就会注入 魔法正文
,通过terser
压缩进行去除未引入的代码
而 slideEffects
是对 没有副作用
的代码进行去除
首先什么是 副作用
,这是一个不太好了解的词,在react
中常常有听到
其实 副作用
就是一个纯函数中存在可变依赖的因变量,因为某个因变量会造成纯函数产生不可控的后果
举个例子
没有副作用的函数,输入输出很明确
function watchEnv(env) {return env === 'prd' ? 'product': 'development'}
watchEnv('prd')
有副作用, 函数体内有不确定性因素
export function watchEnv(env) {const num = Math.ceil(Math.random() * 10);
if (num < 5) {env = 'development'}
return env === 'production' ? '生产环境' : '测试开发环境'
}
咱们在 index.js
中引入watch.js
import {add} from './utils'
import './utils/watch.js';
console.log("Hello World!");
console.log(add(1, 2))
而后运行 npm run build:dev
, 打包后的文件有watch
的引入
在 index.js
中引入 watch.js
并没有什么应用, 然而咱们依然打包了进去
为了去除这引入但未被应用的代码,因而你须要在 optimization.sideEffects: true
,并且要在package.json
中设置 sideEffects: false
,在optimization.sideEffects
设置 true, 告知 webpack 依据 package.json 中的 sideEffects 标记的副作用或者规定,从而告知 webpack 跳过一些引入但未被应用的模块代码。具体参考 optimization.sideEffects
module.exports = () => {if (isProduction) {
config.mode = "production";
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = "development";
config.devtool = 'source-map',
config.optimization = {
sideEffects: true, // 开启 sideEffects
usedExports: true,
minimize: true, // 开启 terser
minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
}
}
return config;
};
{
"name": "my-webpack-project",
"version": "1.0.0",
"description": "My webpack project",
"main": "index.js",
"sideEffects": false,
...
}
此时你运行命令npm run build:dev
,查看打包文件
咱们就会发现,引入的 watch.js
就没有了
在官网中有这么一段话 应用 mode 为 "production" 的配置项以启用更多优化项,包含压缩代码与 tree shaking。
因而在 webpack5
中只有你设置 mode:production
那些代码压缩、tree shaking
统统默认给你做了做了最大的优化,你就无需操心代码是否有被压缩,或者 tree shaking
了。
对于是否被 tree shaking
还补充几点
1、肯定是 esModule
形式,也就是 export xxx
或者 import xx from 'xxx'
的形式
2、cjs
形式不能被tree shaking
3、线上打包生产环境 mode:production
主动开启多项优化,能够参考生产环境的构建 production
gizp 压缩
首先是是在 devServer
下提供了一个开发环境的compress:true
{
devServer: {
open: true,
host: "localhost",
compress: true // 启用 zip 压缩
}
}
- CompressionWebpackPlugin 插件 gizp 压缩
须要装置对应插件
npm i compression-webpack-plugin --save-dev
webpack.config.js
中引入插件
// Generated using webpack-cli https://github.com/webpack/webpack-cli
...
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const config = {
...
plugins: [
new HtmlWebpackPlugin({template: "index.html",}),
new MiniCssExtractPlugin(),
new CompressionWebpackPlugin(),
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
...
};
当你运行命令后,你就会发现打包后的文件有 gzip
的文件了
然而咱们发现 html
以及 map.js.map
文件也被 gizp
压缩了,这是没有必要的
官网提供了一个 exclude
, 能够排除某些文件不被gizp
压缩
{
plugins: [
new HtmlWebpackPlugin({template: "index.html",}),
new MiniCssExtractPlugin(),
new CompressionWebpackPlugin({exclude: /.(html|map)$/i // 排除 html,map 文件
})
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
}
比照开启 gizp
压缩与未压缩, 加载工夫很显著有晋升
css tree shaking
次要删除未应用的款式,如果款式未应用,就删除掉。
当初批改下 index.js
我在 body
中插入一个class
import {add} from './utils'
import './utils/watch';
import './css/index.css'
console.log("Hello World!");
console.log(add(1, 2))
// /*#__PURE__*/ watchEnv(process.env.NODE_ENV)
const bodyDom = document.getElementsByTagName('body')[0]
const divDom = document.createElement('div');
divDom.setAttribute('class', 'wrap-box');
bodyDom.appendChild(divDom);
对应的 css 如下
.wrap-box {
width: 100px;
height: 100px;
background-color: red;
}
执行npm run serve
然而咱们发现,款式竟然没了
于是苦思瞑想,不得其解, 于是一顿排查,当咱们把 sideEffects: false
时,神奇的是,款式没有被删掉
原来是 sideEffects:true
把引入的 css 当成没有副作用的代码给删除了,此时,你须要通知 webpack
不要删除我的这有用的代码, 不要误删了,因为 import 'xxx.css'
如果设置了 sideEffects: true
,此时引入的css
会被当成无副作用的代码,就给删除了。
// package.json
{
"sideEffects": ["**/*.css"],
}
当你设置完后,页面就能够失常显示 css 了
官网也提供了另外一种计划,你能够在 module.rules
中设置
{
module: {
rules: [
{
test: /\.css$/i,
sideEffects: true,
use: [stylesHandler, "css-loader", "postcss-loader"],
},
]
}
}
以上与在 package.json
设置一样的成果,都是让 webpack
不要误删了无副作用的 css 的代码
然而当初有这样的 css
代码
.wrap-box {
width: 100px;
height: 100px;
background-color: red;
}
.title {color: green;}
title
页面没有被援用,然而也被打包进去了
此时须要一个插件来帮忙咱们来实现 css 的摇树 purgecss-webpack-plugin
const path = require("path");
...
const glob = require('glob');
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const PATH = {src: path.resolve(__dirname, 'src')
}
const config = {
...
plugins: [
...
new PurgeCSSPlugin({paths: glob.sync(`${PATH.src}/**/*`, {nodir: true}),
})
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
...
};
未援用的 css 就曾经被删除了
分包
次要是缩小入口依赖文件包的体积,如果不进行拆包,那么咱们依据 entry
的文件打包就很大。那么也会影响首页加载的性能。
官网提供了两种计划:
- entry 分多个文件,举个栗子
引入loadsh
// index.js
import {add} from './utils';
import _ from 'loadsh';
import './utils/watch';
import './css/index.css';
console.log("Hello World!");
console.log(add(1, 2))
// /*#__PURE__*/ watchEnv(process.env.NODE_ENV)
const bodyDom = document.getElementsByTagName('body')[0]
const divDom = document.createElement('div');
divDom.setAttribute('class', 'wrap-box');
divDom.innerText = 'wrap-box';
bodyDom.appendChild(divDom);
console.log(_.last(['Maic', 'Web 技术学苑']));
main.js
中将 loadsh
打包进去了, 体积也十分之大72kb
咱们当初利用 entry
进行分包
const config = {
entry: {main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
'loadsh-vendors': ['loadsh']
},
}
此时咱们再次运行 npm run build:dev
此时 main.js
的大小 1kb
,然而loadsh
曾经被分离出来了
生成的 loadsh-vendors.js
会被独自引入
能够看下打包后的index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
<script defer src="main.js"></script>
<script defer src="loadsh-vendors.js"></script>
<link href="main.css" rel="stylesheet" />
</head>
<body>
<h1>Hello world!</h1>
<h2>Tip: Check your console</h2>
</body>
<script>
if ('serviceWorker' in navigator) {window.addEventListener('load', () => {
navigator.serviceWorker
.register('service-worker.js')
.then((registration) => {console.log('Service Worker registered:', registration);
})
.catch((registrationError) => {console.error('Service Worker registration failed:', registrationError);
});
});
}
</script>
</html>
-
splitChunks
次要是在optimzation.splitChunks
对于动静导入模块,在webpack4+
就默认采取分块策略const config = { // entry: {// main: { import: ['./src/index'], dependOn: 'loadsh-vendors' }, // 'loadsh-vendors': ['loadsh'] // }, entry: './src/index.js', ... } module.exports = () => {if (isProduction) { config.mode = "production"; config.plugins.push(new WorkboxWebpackPlugin.GenerateSW()); } else { config.mode = "development"; config.devtool = 'source-map', config.optimization = { splitChunks: {chunks: 'all' // 反对异步和非异步共享 chunk}, sideEffects: true, usedExports: true, minimize: true, // 开启 terser minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})] } } return config; };
当
optimization.splitChunks.chunks:'all'
, 此时能够把loash
分包进去了
对于 optimization.splitChunks
的设置十分之多,有对缓存的设置,有对 chunk
大小的限度,最罕用的还是设置chunks:all
,倡议 SplitChunksPlugin 多读几遍, 肯定会找到不少播种。
-
runtimeChunk
次要缩小依赖入口文件打包体积,当咱们设置optimization.runtimeChunk
时,运行时依赖的代码会独立打包成一个runtime.xxx.js
... config.optimization = { runtimeChunk: true, // 缩小入口文件打包的体积,运行时代码会独立抽离成一个 runtime 的文件 splitChunks: { minChunks: 1, // 默认是 1,能够不设置 chunks: 'all', // 反对异步和非异步共享 chunk }, sideEffects: true, usedExports: true, minimize: true, // 开启 terser minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})] }
main.js
有一部分代码移除到一个独立的runtime.js
中 - Externals 内部扩大
第三种计划就是,webpack
提供了一个内部扩大,将输入的bundle.js
排除第三方的依赖,参考 Externals
const config = {
// entry: {// main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
// 'loadsh-vendors': ['loadsh']
// },
entry: './src/index.js',
...,
externals: /^(loadsh)$/i,
/* or
externals: {loadsh: '_'}
*/
};
module.exports = () => {if (isProduction) {
config.mode = "production";
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = "development";
config.devtool = 'source-map',
config.optimization = {
runtimeChunk: true, // 缩小入口文件打包的体积,运行时代码会独立抽离成一个 runtime 的文件
// splitChunks: {
// minChunks: 1,
// chunks: 'all', // 反对异步和非异步共享 chunk
// },
sideEffects: true,
usedExports: true,
minimize: true, // 开启 terser
minimizer: [new TerserPlugin({extractComments: false, // 是否将正文剥离到独自文件,默认是 true})]
}
}
return config;
};
然而此时 loash
曾经被咱们移除了,咱们还需在 HtmlWebpackPlugin
中退出引入的 cdn
地址
...
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
inject: 'body', // 插入到 body 中
cdn: {
basePath: 'https://cdn.bootcdn.net/ajax/libs',
js: ['/lodash.js/4.17.21/lodash.min.js']
}
}),
]
批改模版, 因为模版内容是 ejs,所以咱们循环取出 js
数组中的数据
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Webpack App</title>
</head>
<body>
<h1>Hello world!</h1>
<h2>Tip: Check your console</h2>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.basePath %><%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
<script>
if ('serviceWorker' in navigator) {window.addEventListener('load', () => {
navigator.serviceWorker
.register('service-worker.js')
.then((registration) => {console.log('Service Worker registered:', registration);
})
.catch((registrationError) => {console.error('Service Worker registration failed:', registrationError);
});
});
}
</script>
</html>
此时你运行命令npm run build:dev
, 而后关上 html 页面
然而咱们发现当咱们运行 npm run serve
启动本地服务,此时页面还是会引入 loadsh
,在开发环境,其实并不需要引入,自身生成的bundle.js
就是在内存中加载的,很显然不是咱们须要的
此时我须要做几件事
1、开发环境我不容许引入externals
2、模版 html
中须要依据环境判断是否须要插入cdn
const isProduction = process.env.NODE_ENV == "production";
const stylesHandler = MiniCssExtractPlugin.loader;
const PATH = {src: path.resolve(__dirname, 'src')
}
const config = {
// entry: {// main: { import: ['./src/index'], dependOn: 'loadsh-vendors' },
// 'loadsh-vendors': ['loadsh']
// },
entry: './src/index.js',
output: {path: path.resolve(__dirname, "dist"),
},
devServer: {
open: true,
host: "localhost",
compress: true
},
plugins: [
new HtmlWebpackPlugin({
env: process.env.NODE_ENV, // 传入模版中的环境
template: "index.html",
inject: 'body', // 插入到 body 中
cdn: {
basePath: 'https://cdn.bootcdn.net/ajax/libs',
js: ['/lodash.js/4.17.21/lodash.min.js']
}
}),
new MiniCssExtractPlugin(),
new CompressionWebpackPlugin({exclude: /.(html|map)$/i // 排除 html,map 文件不做 gizp 压缩
}),
new PurgeCSSPlugin({paths: glob.sync(`${PATH.src}/**/*`, {nodir: true}),
})
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
...
// externals: /^(loadsh)$/i,
externals: isProduction ? {loadsh: '_'} : {}};
依据传入模版的 env
判断是否须要插入 cdn
...
<% if (htmlWebpackPlugin.options.env === 'production') { %>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.basePath %><%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<% } %>
图片资源压缩
次要是有抉择的压缩图片资源,咱们能够看下module.rules.parser
-
module.rules.parser.dataUrlCondition
对应的资源文件能够限度图片的输入, 比方动态资源模块类型module: { rules: [ {test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', parser: { dataUrlCondition: {maxSize: 4 * 1024 // 小于 4kb 将会 base64 输入} } }, ], },
官网提供了一个 ImageMinimizerWebpackPlugin
咱们须要装置npm i image-minimizer-webpack-plugin imagemin --save-dev
在
webpack.config.js
中引入image-minimizer-webpack-plugin
, 并且在plugins
中引入这个插件, 留神webpack5
官网那份文档很旧,参考npm
上 npm-image-minimizer-webpack-plugin
依照官网的,就间接报错一些配置参数不存在,我预计文档没及时更新
...
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const config = {
plugins: [
...
new ImageMinimizerPlugin({
minimizer: {
// Implementation
implementation: ImageMinimizerPlugin.squooshMinify,
},
})
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
}
未压缩前
压缩后
应用压缩后,图片无损压缩体积大小压缩大小放大一半,并且网络加载图片工夫从 18.87ms
缩小到4.81ms
, 工夫加载上靠近 5 倍的差距,因而能够用这个插件来优化图片加载。
这个插件能够将图片转成 webp
格局,具体参考官网文档成果测试一下
总结
1、webpack
如何做treeShaking
,次要是两种
- optimization 中设置
usedExports:true
,然而要配合terser
压缩插件才会失效 - optimization 中设置
sideEffects: true
, 在package.json
中设置sideEffects:false
去除无副作用的代码,然而留神css
引入会当成无副作用的代码,此时须要在 rules 的 css 规定中标记sideEffects: true
, 这样就不会删除 css 了
2、webpack
的 gizp 压缩
次要是利用 CompressionWebpackPlugin
官网提供的这个插件
3、css
如何做 treeShaking
,
次要是利用 PurgeCSSPlugin
这个插件,会将没有援用 css 删除
4、入口依赖文件拆包
- 第一种是在入口文件
entry
中分包解决,将依赖的第三方库独立打包成一个专用的bundle.js
, 入口文件不会把第三方包打包到外面去 - 第二种利用
optimization.splitChunks
设置chunks:'all'
将同步或者异步的esModule
形式的代码进行分包解决,会独自打成一个专用的 js - 利用外置扩大
externals
将第三方包拆散进来,此时第三方包不会打包到入口文件中去,不过留神要在ejs
模版中进行独自引入
5、图片资源
加载优化
- 次要是利用动态资源模块对文件体积小的能够进行 base64
- 利用社区插件
image-minimizer-webpack-plugin
做图片压缩解决
6、本文示例 code-example
欢送关注公众号:Web 技术学苑
好好学习,天天向上!