12 种 webpack 优化手段。
感受到更快的性能体验。
使自己的 webpack 构建工具愈来愈趋向合理。
一步步带你更高效的优化自己的 webpack ,使得打包出来的产物更精简。
利用 webpack 开箱即用,插件化的特点,来实现 webpack 的优化能力。
先快速使用 webpack 搭建一个工程,只需具备基本的功能即可。具体可查看 Git 仓库的 feature/20210425/FruitJ/init_project
分支或者直接点我,然后克隆下来一步步跟着笔者走下去。
开发的时候难免有些代码会在开发环境和生产环境切换的时候会随之调节,对于这部分代码我们只能手动去更改,一处两处还好,如果是很多处。不仅这是一个机械活动很累很烦,也是一个风险极大的活动,万一哪个接口没有及时替换成线上的接口,就可能会引发一些问题。其中一种解决方案是我们在 webpack 注册一个 “全局变量” ,然后涉及到随着环境切换而变动的代码可以通过这个 webpack 注册的 “全局变量” 做一个开关。这样就可以很大程度避免上述问题。
'DEV'
他是不会当成字符串 DEV 来处理的而是直接去掉最外层的引号变成了变量 DEV,所以为了解决这个问题通常都在单引号外面再套一层双引号("'DEV'"
),这样就可以正确表示 'DEV'
啦,当然如果你想定义的值是个布尔类型的就不要再在外面套层双引号了直接这样写即可: 'true'
。当然这种书写方式肯定不是大家想要的,因为需要多套一层引号,我们其实可以使用 JSON.stringify(...)
来处理这种情况的(将 "'DEV'"
变成 JSON.stringify('DEV')
),在本小节中还是使用 "'DEV'"
的方式,在下一个小节中我们将使用 JSON.stringify('DEV')
方式。...
plugins: [
...
new Webpack.DefinePlugin({
ENV: "'DEV'",
}),
],
...
webpack.config.js
const path = require("path");
const Webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new Webpack.DefinePlugin({
ENV: "'DEV'",
}),
],
module: {
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
ENV
来做判断即可。import "./index.css";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
"'DEV'"
所以肯定会匹配 index.js 判断部分的 if(ENV === 'DEV')
,所以肯定会打印 访问开发环境 API【 http://localhost:3000/api/userInfo 】
。当我们将全局变量的值改为 "'PROD'"
后就会匹配 index.js 判断部分的 else if(ENV === 'PROD')
就会打印 访问生产环境 API【 http://fruitj:3005/api/userInfo 】
。虽然在上一节中我们通过 webpack 的 define-plugin 插件注册全局环境变量已经实现了环境改变无需切换具体的业务代码就能展现对应的行为的能力了。但是这还是不够因为我们现在的 webpack.config.js 中的配置还是耦合了开发环境和生产环境的全部配置。一方面虽然可以这么做但是不规范,另一方面就是随着项目愈来愈复杂、配置愈来愈多如果都耦合在一起的话会非常难维护。所以我们需要将开发环境和生产环境的配置严格区分开来,但是对于共同配置还是需要合并进来的,这就需要 webpack-merge 来帮助我们在分离开发环境与生产环境的配置后将公共配置 merge 进来。
webpack.config.js
改名为 webpack.base.js
,这个 base 配置文件就作为 webpack 的基本配置文件,就是这个文件是用来存放开发环境和生产环境公共配置。webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
webpack.dev.js
文件,这里面就可以使用 webpack-merge
将 base 的配置 merge 到当前文件中,并且在此文件中配置一些开发环境用到的专有配置。webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
],
});
webpack.prod.js
文件,这里面就可以使用 webpack-merge
将 base 的配置 merge 到当前文件中,并且在此文件中配置一些生产环境用到的专有配置。webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
},
});
--config
指定读取的具体配置文件。...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 " ,
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17"
}
}
这样我们执行 yarn build,webpack 就会去读取 webpack.prod.js 配置文件,随即进行打包。我们执行 yarn dev,webpack 就会去读取 webpack.dev.js 配置文件,随即启动静态服务打开浏览器渲染页面 …
运行 yarn build 命令,打包出来 dist 目录后我们运行 index.html 查看效果。
运行 yarn dev 命令,查看浏览器效果。
小结 :
这一节通过 webpack-merge 我们实现了开发环境和生产环境的分离,实现了 yarn build 和 yarn dev 分别走不同环境的配置文件。这样后期相对来说会比较好维护一些。
同时我们也可以不用手动切换我们在 webpack 全局注册的 ENV 变量了,因为在 webpack.dev.js 和 webpack.prod.js 都分别指定好了,当运行 yarn dev 和 yarn build 命令的时候执行的是对应的配置文件,在全局注册的 ENV 变量的值就是对应的值。
我们的项目中可能包含有大量的类似 “jQuery” 这种不依赖或者说很少依赖其它模块的库,因为这些库本身就是一个最简最小的三方库了。而 webpack 在打包之前是先会以 output 规定的入口开始对每一个依赖资源进行解析的,解析这些依赖资源里面有没有再依赖其它资源。由此像 “jQuery” 这种包本身就不用去解析它了,它里面也不会依赖其它的资源。所以我们可以在 webpack 中配置 noParse 属性来告诉 webpack 不需要解析哪些模块。
~$ yarn add jquery --save
...
module: {
noParse: /jquery/,
...
},
...
webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
}
],
},
};
小结 :
这一节我们通过配置 webpack.base.js 中 module 属性的 noParse 来指明 webpack 不需要解析依赖的模块。这样 webpack 解析到该模块的时候就不会进去检查其内部依赖的模块了,如果这种模块只有少数几个则优化的效果还是不太容易看出来,但是如果这种模块比较多的话,就比较容易看出来了。
比如我们在配置 babel 转换的时候,test 我们是这样写的 /\.js$/
这不仅匹配了我们 src 目录同时也会匹配 node_module 目录。但是 node_modules 里面都是三方模块基本不需要我们的 babel 去做什么事情,所以我们要通过配置 babel 转换规则中的 exclude 属性来排除掉 node_modules 目录,或者使用 include 属性来指明只处理 src 目录。
...
module: {
...
rules: [
...
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
...
webpack.base.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
小结 :
这一节我们通过配置 babel 规则中的 exclude 属性和 include 属性来指明 webpack 的处理区域,这对提升 webpack 通过 babel 做语法转换的时候也是一个不小的性能提升(因为少了匹配并处理 node_modules 的环节)。
有时候在使用三方包的时,三方包会导入一些其它的依赖资源,但是这些资源又不是必须被依赖的,或者说我们不需要这些依赖的包。这个时候我们就可以使用 webpack 自带的 ignore-plugin 插件来指定忽视三方包中的特定依赖模块,被忽视依赖模块将不会被打包。
举个 : 在使用 moment.js 的时候,moment 会将所有的本地化内容和核心功能一起打包。通俗说我们使用 moment.js 的时候他会自动去加载各国的多语言,如果我们的项目是一个国际化的项目那么也只能这样用,如果我们的项目只是一个国内的项目不涉及多语言那么就没有必要去加载世界各国的多语言了,只需要加载本国语言便好,这样最终的打包体积会小很多。
~$ yarn add moment --save
import moment from "moment";
...
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
index.js
import "./index.css";
import $ from "jquery";
import moment from "moment";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
console.log($);
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
运行 yarn dev 命令,查看打包的 bundle 体积大小与浏览器结果。
可以看出当前打包体积是 1.39M
,体积比较大了,这就是因为 moment 包把所有多语言加载进去导致的结果。浏览器看到效果确实已经是我们设定好的中国的语言的时间。
所以为了优化我们需要忽略 moment 去自动加载多语言改为我们自己手动引入它的本国的多语言。
通过观察安装好的 moment 包的 package.json 找到 moment 包的入口文件。
打开 ./moment.js 找到我们设定多语言的 locale
方法再找到里面调用的 getLocale
方法,发现 getLocale 方法里面调用了 loadLocale
方法,在 loadLocale 方法中我们可以清楚的看到这样的导入多语言的语句 ...require('./locale' + name)
。
笔者大概数了下,这货约是导入了 135 个国家的多语言 … 下一步我们就是需要将这个 ./locale
目录忽略掉。
修改 webpack.base.js 通过 webpack 的 ignore-plugin 来忽略 moment 中导入的 ./locale
目录
const webpack = require("webpack");
...
plugins: [
...
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
],
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
import moment from "moment";
import "moment/locale/zh-cn";
...
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
index.js
import "./index.css";
import $ from "jquery";
import moment from "moment";
import "moment/locale/zh-cn";
const getMockData = () => {
if(ENV === 'DEV') {
console.log("访问开发环境 API【 http://localhost:3000/api/userInfo 】");
}else if(ENV === 'PROD') {
console.log("访问生产环境 API【 http://fruitj:3005/api/userInfo 】");
}else {
console.log("错误");
}
};
getMockData();
console.log($);
moment.locale("zh-cn");
console.log(moment().endOf('day').fromNow());
1.39M
,优化到了 871Kb
。这中间优化了约 552Kb
。小结 :
这一节我们通过 webpack 的自带插件 ignore-plugin 实现了,忽视指定的三方包导入的模块,实现了一定程度的优化,用 moment 举例我们优化了约 552Kb
。这对我们的项目最终产出的 bundle 体积也是一次极大优化,552Kb
这个数字不小了 …
说起优化,不仅是针对最终打包上线运行,同时也是针对开发者在开发过程中的项目构建。如果开发者在每次调试的时候构建速度能够快一些,这将会大大节约开发者的时间从而提升开发效率。
举个:项目中依赖的一些变动不是特别频繁的模块,我们就可以通过 webpack 的 dll-plugin 插件将其打进 “动态链接库” 中,这样我们构建的时候对于这些不常变动的模块直接就去动态链接库中去查找这样就不会参与打包了,大大提升了项目的构建速度,同时最终产出的 bundle 也会减少部分体积。
原理就是,原来我们只有一个主构建过程,在这个主构建过程中凡是依赖的资源全部一股脑的都被打包。但是使用 webpack 的 dll-plugin 插件后我们除了主构建过程还拥有了一个 dll 构建过程,这个 dll 构建过程是先于主构建过程执行的。就是说我们会先通过 dll 构建过程先将那些不常变动的模块打包出来备用(这就是上面说的将不常变动的模块打进动态链接库),然后当我们进行主构建过程的时候让 webpack 先到动态链接库中去找,如果找到了就直接使用,没有找到再去打包。这样我们项目的构建速度会大大提升且最终产出的 bundle 由于不包含那部分被 dll 构建过程构建的不常变动的模块,所以体积比以前会小一些。但是这样虽然优化了 bundle 的体积,但并不会优化页面的访问速度,因为这种机制会先将 dll 构建出来的产物请求回来,然后才会去走 bundle。所以对于页面访问的速度并无明显的优化,但是对于项目的构建速度,优化就非常明显了。
~$ yarn add react react-dom --save
@babel/preset-react
预设中插件集进行转换。~$ yarn add @babel/preset-react -D --save
@babel/preset-react
配置进去{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
import "./index.css";
import React from "react";
import { render } from "react-dom";
render(<h1>webpack optimization ...</h1>, document.querySelector("#app"));
然后运行 yarn dev 命令,查看打包的大小与浏览器的效果。
可以看到,bundle 的体积是 1.35M
,这已经很大了。浏览器效果正常。
接下来就要开始优化啦 …
优化之前先讲一点前置知识,就是正常我们打包后我们最终在打包的结果中是无法获取到我们输出的内容的。我们先来做个试验。
在 src 目录下新建 test.js
文件,然后在里面输出一个普通值。
test/js
module.exports = "哈哈";
webpack.dll.js
作为 dll 构建过程的 webpack 配置文件。现在是为了验证所以里面的配置比较简单webpack.dll.js
const path = require("path");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
test: "./src/test.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new CleanWebpackPlugin(),
],
};
"dll_build": "webpack --config webpack.dll.js"
。...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js",
"dll_build": "webpack --config webpack.dll.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 " ,
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js",
"dll_build": "webpack --config webpack.dll.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17",
"jquery": "^3.6.0",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
library
属性实现自动创建变量并接收打包结果的返回值,这里我们将变量取名为 _dll
。...
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
...
webpack.dll.js
const path = require("path");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
test: "./src/test.js",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
plugins: [
new CleanWebpackPlugin(),
],
};
react
和 react-dom
而不是 ./src/test.js
并且我们要使用 webpack 的 dll-plugin 插件来生成动态链接库的映射,通俗说就是借助 webpack 的 dll-plugin 插件会通过 output 输出中的 library 指定的名字来构建一个资源映射,后面webpack 在找的时候就可以根据这个资源映射去找了(至于 dll-plugin 怎么与 output 匹配上的就是根据 dll-plugin 的 name 属性和 output 的 library 属性)。...
const webpack = require("webpack");
...
entry: {
dll: ["react", "react-dom"],
},
...
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: "_dll",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
webpack.dll.js
const path = require("path");
const webpack = require("webpack");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
dll: ["react", "react-dom"],
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
library: "_dll",
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: "_dll",
path: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
};
manifest.json
这个资源映射也成功的打包了出来。manifest.json
去动态链接库中去找资源呢 ? 这就需要借助 webpack 的另一个插件 dll-reference-plugin
了。而且这个插件是在我们主构建过程的 webpack.base.js
中配置的,我们只需要通过 manifest
这个属性指明我们的 manifest.json
资源映射所在位置即可。...
plugins: [
...
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dist", "manifest.json"),
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
index.html
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack 优化title>
head>
<body>
<div id="app">div>
<script src="./dll.js">script>
body>
html>
clean-webpack-plugin
插件,所以我们每次构建都会将上一次的结果清楚掉,这里我们先临时将我们从 dll 构建过程中打包出来的 2 个文件先放在根目录下备份一下。1.21Kb
,相比一开始优化之前的 1.35M
,bundle 大小优化了约 1381Kb
。小结 :
这一节我们通过配置使用 webpack 的 dll-plugin 插件和 dll-reference-plugin 插件借助动态链接库的机制实现了优化构建速度与 bundle 的体积。因为动态链接库我们一旦通过 dll 构建过程构建完毕后你的话只要不遇到模块升级或者变动基本上是不用管它了,这样我们主的构建过程会提速一些的。
但是还需介绍后面的内容这里我们就先将 index.html 和 webpack.base.js 的相关代码注释掉啦。
index.html
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack 优化title>
head>
<body>
<div id="app">div>
body>
html>
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: ["babel-loader"],
include: /src/,
exclude: /node_modules/,
}
],
},
};
简单介绍下多线程的意思,就是一个线程占用 cpu,达到时间片后会主动让出 cpu 使用权,众多线程们再次去争夺时间片,这样循环往复,直到大家都把活干完。
当然如果项目工程较小的话,使用这个多线程效率不一定会上升,只有当项目工程比较大的时候这个多线程打包的效果才会变得明显。
这里我们以多线程打包 js 为例 :
~$ yarn add happypack -D --save
...
plugins: [
...
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
...
rules: [
...
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const Happypack = require("happypack");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "js/bundle.[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
};
test.js
const eat = () => {
console.log("吃");
};
const sleep = () => {
console.log("睡");
};
export default {
eat,
sleep,
};
index.js
import action from './test';
action.eat();
运行命令 yarn build 查看打包的结果,发现 webpack 只是将 eat 方法的内容导了出来,并没有看见 sleep 方法。
运行命令 yarn dev,发现 eat 和 sleep 方法全部被打进 bundle 了。
由此可见在生产环境打包的时候 webpack 会帮助我们对代码进行 tree shaking。
当我们的项目是多入口的时候,不同入口引入了相同文件,webpack 默认会将这个文件分别导进各自引它的入口中。这样会增加最终打包出来的 bundle 体积。所以我们需要将这个公共的部分代码给单独打包出来,然后各个入口直接引入就好了,这样会减少 bundle 体积。
优化之前先看下 webpack 在多入口情况下默认的导入机制
test.js
console.log("呵呵");
other.js
。other.js
import "./test";
console.log("other");
index.js
import "./test";
console.log("index");
...
entry: {
index: "./src/index.js",
other: "./src/other.js",
},
output: {
filename: "js/[name].[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
...
webpack.base.js
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
const Happypack = require("happypack");
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// entry: "./src/index.js",
entry: {
index: "./src/index.js",
other: "./src/other.js",
},
output: {
filename: "js/[name].[hash:8].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
hash: true,
filename: "index.html",
template: "./src/index.html",
}),
new CleanWebpackPlugin(),
new MiniCSSExtractPlugin({
filename: "css/bundle.css",
}),
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// new webpack.DllReferencePlugin({
// manifest: path.resolve(__dirname, "dist", "manifest.json"),
// }),
new Happypack({
id: "js",
use: ["babel-loader"],
}),
],
module: {
noParse: /jquery/,
rules: [{
test: /\.css$/,
use: [MiniCSSExtractPlugin.loader, "css-loader"],
},
{
test: /\.js$/,
use: "Happypack/loader?id=js",
include: /src/,
exclude: /node_modules/,
}
],
},
};
...
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
},
},
},
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
},
},
},
});
common~index~other.de1df520.js
。这个里面存放的就是 index.js
和 test.js
两个入口引入的公共代码,并且将其挂载到了 window.webpackJsonp
上了,而打包出来的 index.js 和 test.js 也是直接用的 window.webpackJsonp 上挂载的公共代码。vendor
属性来进行配置。...
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
vendor: {
test: /node_modules/, // 指定匹配的目录
chunks: "all",
minSize: 0,
minChunks: 2,
},
},
},
},
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
splitChunks: {
cacheGroups: {
common: { // 处理非三方模块
chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
minSize: 0, // 公共部分超过 0 字节就执行
minChunks: 2, // 公共部分被引用超过 2 次就执行
},
vendor: {
test: /node_modules/, // 指定匹配的目录
chunks: "all",
minSize: 0,
minChunks: 2,
},
},
},
},
});
import $ from "jquery";
console.log($, "index");
import $ from "jquery";
console.log($, "other");
window.webpackJsonp
上,然后打包的 index.js 和 test.js 引用的 jquery 都是从 window.webpackJsonp 上拿的。我们都知道凡是我们使用 import 语法导入的资源 webpack 都会在开始的时候全部打包进去,但是比如有个需求,我点击按钮出现一个弹框,弹框用到了 moment 要显示时间,对于完成这个业务需求来说可谓比较简单。但是我们在完成需求之后还可以进一步优化的,比如这个弹框是在我点击按钮之后才会使用的,那么为啥我不在它点击的时候去请求资源呢(moment) ? 在 webpack 中我们可以借助 import( ... )
来实现这个功能,这样就实现了拆分主 bundle,也就是我们常说的分包。
下面我们写个案例对比一下 : 案例的内容就是点击按钮打印 jquery。
未进行代码分割
index.js
import $ from "jquery";
const onClickDynamicImport = () => {
console.log($);
};
const btn = document.createElement("button");
btn.innerText = "Dynamic Import";
btn.addEventListener('click', onClickDynamicImport, false);
document.querySelector("#app").appendChild(btn);
进行代码分割
/* webpackChunkName: "jquery-chunk" */
这个写在 import( … ) 括号里面的注释是有用的不能删掉,比如这个 webpackChunkName
属性就是为当前的 dynamic import 的模块起个名字的意思。还有一些其它的注释index.js
const onClickDynamicImport = () => {
const result = import(/* webpackChunkName: "jquery-chunk" */ "jquery");
result.then(module => module.default).then($ => {
console.log($);
}).catch(error => {
console.log(error);
})
};
const btn = document.createElement("button");
btn.innerText = "Dynamic Import";
btn.addEventListener('click', onClickDynamicImport, false);
document.querySelector("#app").appendChild(btn);
vendors~jquery-chunk.e0954a3f.js
当页面按钮被点击时就会去请求这个文件。88.4Kb
。而打包后的大小是 2.31Kb
,优化了约 86Kb
左右。这个代码分割产生的优化效果是直接可以作用到页面的首屏访问速度上去的,因为被分割的代码不会在页面一开始渲染的时候一起请求过来,而是等需要的时候才会请求过来,所以对于项目中一起不常用且非必须在首屏就要加载的模块我们最好使用代码分割将他们分割出去,来提升页面的访问速度。vendors~jquery-chunk.e0954a3f.js
)是点击按钮之后才请求过来的。test
小结 :
这一节我们借助 webpack 的能力使用 ES6的 import 函数(Dynamic Import) 实现了代码分割,这种代码分割是一种比较实用的优化手段的,因为优化的结果可以直接作用于页面的访问速度上。
HRM (Hot Module Replacement) 就是 热更新/热替换 的意思,通俗说就是在开发阶段,代码改变浏览器无需重载页面,代码变动的地方页面会自动发生相应变化。试想一下如果页面的内容比较多,涉及到的逻辑也比较复杂,需要拉到许多资源,这个时候我们在开发阶段如果可以使用热更新功能,这会节省一部分的开发时间,提升我们的开发效率。因为不用重载整个网页了呀 ~ 。
配置热更新之前
index.js
console.log("index");
test
配置热更新
HotModuleReplacementPlugin
和 NamedModulesPlugin
插件,HotModuleReplacementPlugin 的作用是提供热更新支持,而 NamedModulesPlugin 插件的作用是打印更新模块的路径。webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
hot: true,
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
new Webpack.NamedModulesPlugin(),
new Webpack.HotModuleReplacementPlugin(),
],
});
index.js
if(module.hot) module.hot.accept();
console.log("index");
test
HotModuleReplacementPlugin
和 NamedModulesPlugin
插件,我们可以配置一下 package.json 中的 yarn dev 命令,在其后添加一个 --hot
参数,添加这个参数 webpack 就会帮助我们启用 HotModuleReplacementPlugin
和 NamedModulesPlugin
插件。HotModuleReplacementPlugin
和 NamedModulesPlugin
插件。webpack.dev.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
module.exports = merge(webpackBaseConfig, {
mode: "development",
devServer: {
hot: true,
port: 3005,
open: true,
compress: true,
progress: true,
contentBase: "./dist",
},
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('DEV'),
}),
// new Webpack.NamedModulesPlugin(),
// new Webpack.HotModuleReplacementPlugin(),
],
});
--hot
参数...
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js --hot",
"dll_build": "webpack --config webpack.dll.js"
},
...
package.json
{
"name": "test-webpack-optimization",
"version": "1.0.0",
"main": "index.js",
"repository": "https://gitee.com/LJ_PGSY/test-webpack-optimization.git",
"author": "wb-lj789114 " ,
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.prod.js",
"dev": "webpack-dev-server --config webpack.dev.js --hot",
"dll_build": "webpack --config webpack.dll.js"
},
"devDependencies": {
"@babel/core": "^7.13.16",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-react": "^7.13.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"css-loader": "^5.2.4",
"happypack": "^5.0.1",
"html-webpack-plugin": "4.5.2",
"mini-css-extract-plugin": "^1.5.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.46.0",
"webpack-cli": "3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.17",
"jquery": "^3.6.0",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
如果项目比较小,像我们这个测试的小项目其实是完全用不上这个插件的,因为使用的 三方包也不多,心里都知道最终会将哪个三方包打进 bundle 中,但是如果项目比较大,引的三方包比较多,或者自己写的模块比较多。这个时候就需要对最终产出的 bundle 进行分析了,但是我们直接看压缩过的源码那简直是为难人。所以可以借助 webpack-bundle-analyzer
插件生成的依赖图谱来进行分析。
webpack-bundle-plugin
插件~$ yarn add webpack-bundle-plugin -D --save
...
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
...
plugins: [
...
new BundleAnalyzerPlugin(),
],
...
webpack.prod.js
const Webpack = require("webpack");
const { merge } = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base");
const UglifyJSWebpackPlugin = require("uglifyjs-webpack-plugin");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const OptimizeCSSAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = merge(webpackBaseConfig, {
mode: "production",
plugins: [
new Webpack.DefinePlugin({
ENV: JSON.stringify('PROD'),
}),
new BundleAnalyzerPlugin(),
],
optimization: {
minimizer: [
new UglifyJSWebpackPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
new OptimizeCSSAssetsWebpackPlugin(),
],
// splitChunks: {
// cacheGroups: {
// common: { // 处理非三方模块
// chunks: "all", // 【initial(代表对于直接引入的模块采取该优化策略)、async(代表对按需引入的模块采取该优化策略)、all(不管是直接引入还是按需引入都采取该优化策略)】
// minSize: 0, // 公共部分超过 0 字节就执行
// minChunks: 2, // 公共部分被引用超过 2 次就执行
// },
// vendor: {
// test: /node_modules/, // 指定匹配的目录
// chunks: "all",
// minSize: 0,
// minChunks: 2,
// },
// },
// },
},
});
8888
的本地服务,并跳转导浏览器,此时就可以看到自己项目打包出来的 bundle 中的模块依赖图谱了。小结 :
这一节通过配置使用 webpack-bundle-analyzer 插件实现了 bundle 模块依赖的可视化,极大便利我们分析 bundle 中的模块依赖。是优化 bundle 最直观的一件利器。
本文是基于上一篇文章的基础上,介绍了 12 种 webpack 的优化手段。相信通过这些优化手段可以使得我们通过 webpack 构建的项目更加的健壮。
我们来回忆一下都介绍了哪些优化手段 ?
文章中虽然经过审阅但是难免会有错字、代码片段错误、贴图错误、甚至 npm 包不断升级可能跟到某处就卡了,如果出现这些问题请在博文下方留言,笔者发现第一时间进行处理。
本文是笔者一点点构建出来的绝对可以放心食用 。
git 仓库