1.压缩代码
压缩css和压缩js是自带的,只有生产环境才会使用
压缩js 
压缩css
压缩图片:
{test: /\.(png|svg|jpg|gif|jpeg|ico)$/,use: ["file-loader",{+ loader: "image-webpack-loader",+ options: {+ mozjpeg: {+ progressive: true,+ quality: 65,+ },+ optipng: {+ enabled: false,+ },+ pngquant: {+ quality: "65-90",+ speed: 4,+ },+ gifsicle: {+ interlaced: false,+ },+ webp: {+ quality: 75,+ }+ }+ }]}
2.purgecss-webpack-plugin
可以去除未使用的css,一般与glob和glob-cli配合使用
必须和mini-css-extract-plugin(不只是压缩作用,可以实现提取css文件内容到单独的文件中去)配合使用
paths路径是绝对路径
npm i purgecss-webpack-plugin mini-css-extract-plugin css-loader glob -D
webpack.config.js
const path = require("path");+const glob = require("glob");// 按照文件匹配模式匹配文件列表+const PurgecssPlugin = require("purgecss-webpack-plugin");+const MiniCssExtractPlugin = require('mini-css-extract-plugin');const PATHS = {src: path.join(__dirname, 'src')}module.exports = {mode: "development",entry: "./src/index.js",module: {rules: [{test: /\.js/,include: path.resolve(__dirname, "src"),use: [{loader: "babel-loader",options: {presets: ["@babel/preset-env", "@babel/preset-react"],},},],},+ {+ test: /\.css$/,+ include: path.resolve(__dirname, "src"),+ exclude: /node_modules/,+ use: [+ {+ loader: MiniCssExtractPlugin.loader, // 可以提取css样式+ },+ "css-loader",+ ],+ },],},plugins: [+ new MiniCssExtractPlugin({+ filename: "[name].css",+ }),+ new PurgecssPlugin({+ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),+ })],};
3.多进程处理
把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行
thread-loader
npm i thread-loader- D
const path = require("path");const glob = require("glob");const PurgecssPlugin = require("purgecss-webpack-plugin");const MiniCssExtractPlugin = require('mini-css-extract-plugin');const DllReferencePlugin = require("webpack/lib/DllReferencePlugin.js");const PATHS = {src: path.join(__dirname, 'src')}module.exports = {mode: "development",entry: "./src/index.js",module: {rules: [{test: /\.js/,include: path.resolve(__dirname, "src"),use: [+ {+ loader:'thread-loader',+ options:{+ workers:3+ }+ },{loader: "babel-loader",options: {presets: ["@babel/preset-env", "@babel/preset-react"],},},],},{test: /\.css$/,include: path.resolve(__dirname, "src"),exclude: /node_modules/,use: [{loader: MiniCssExtractPlugin.loader,},"css-loader",],},],},plugins: [new MiniCssExtractPlugin({filename: "[name].css",}),new PurgecssPlugin({paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),}),new DllReferencePlugin({manifest: require("./dist/react.manifest.json"),}),],};
4.Tree Shaking
- 一个模块可以有多个方法,只要其中某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会uglify阶段擦除掉
- 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量
开启:
- webpack默认支持,在.babelrc里设置module:false即可在production mode下默认开启
- 还要注意把devtool设置为null 在 package.json 中配置:
- “sideEffects”: false 所有的代码都没有副作用(都可以进行 tree shaking)
- 可能会把 css / @babel/polyfill文件干掉 可以设置”sideEffects”:[“*.css”]
- tree-sharking是在代码压缩合并阶段
⚠️
module:false:不会转化import、export语法,把这个语法给webpack,webpack才能去做进一步的处理
module:true: 转换import 、export变成commonjs 的require 、module.exports,那么webpack就实现不了Tree Shaking
import().then不能实现Tree Shaking
总结:实现Tree Shaking的前提是只能是esmodule,而不是commonjs
为什么commonjs不支持Tree Shaking?
es 静态模块
require (a)require (b)/////////////////let name =await fetch ('./apixxx/xx/')require(name)// 打包的时候不知道最终返回的是还是b,动态加载,运行时解析// import from '不能是变量,固定的一个字符串'
const path = require("path");const glob = require("glob");const PurgecssPlugin = require("purgecss-webpack-plugin");const MiniCssExtractPlugin = require('mini-css-extract-plugin');const PATHS = {src: path.join(__dirname, 'src')}module.exports = {+ mode: "production",+ devtool:false,entry: {main: './src/index.js'},output:{path:path.resolve(__dirname,'dist'),filename:'[name].[hash].js'},module: {rules: [{test: /\.js/,include: path.resolve(__dirname, "src"),use: [{loader:'thread-loader',options:{workers:3}},{loader: "babel-loader",options: {//modules":false 不要转化import export+ presets: [["@babel/preset-env",{"modules":false}], "@babel/preset-react"],},},],},{test: /\.css$/,include: path.resolve(__dirname, "src"),exclude: /node_modules/,use: [{loader: MiniCssExtractPlugin.loader,},"css-loader",],},],},plugins: [new MiniCssExtractPlugin({filename: "[name].[contenthash].css"}),new PurgecssPlugin({paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),}),],};
functions.js
function func1(){ return 'func1'; }function func2(){ return 'func2'; }export { func1, func2 }
import {func2} from './functions';var result2 = func2();console.log(result2);// func1没有导入,则打包的时候func1会被干掉
// 代码执行结果永远不可达,也会被tree sharkingif(false){console.log('false')}
var aabbcc='aabbcc';aabbcc='eeffgg';//代码中只写不读的变量
告诉 Webpack 你的代码无副作用
// 所有文件都有副作用,全都不可 tree-shaking{"sideEffects": true}// 没有文件有副作用,全都可以 tree-shaking{"sideEffects": false}// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件{"sideEffects": ["./src/file1.js","./src/file2.js"]}
5.代码分割
在最开始使用Webpack的时候, 都是将所有的js文件全部打包到一个build.js文件中(文件名取决与在webpack.config.js文件中output.filename)
但是在大型项目中, build.js可能过大, 导致页面加载时间过长. 这个时候就需要代码分割
代码分割就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载
5.1 入口点分割
Entry Points:入口文件设置的时候可以配置
这种方法存在的问题
- 如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中,对策:提取公共代码
不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
entry: {index: "./src/index.js",login: "./src/login.js"}
以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用
防止重复
针对以上第一点:配置 dependOn选项,这样可以在多个 chunk 之间共享模块:
const path = require('path');module.exports = {mode: 'development',entry: {index: {import: './src/index.js',dependOn: 'shared',},another: {import: './src/another-module.js',dependOn: 'shared',},shared: 'lodash',},output: {filename: '[name].bundle.js',path: path.resolve(__dirname, 'dist'),},};
如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: ‘single’
5.2 动态导入和懒加载
- 用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时
- 一般采用以下原则:
- 对网站功能进行划分,每一类一个chunk
- 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
- 被分割出去的代码需要一个按需加载的时机
import()语句是天然的代码分割点,如果遇到import()就会分割出一个单独的代码块,单独加载
但是动态加载有个问题就是,如果一个模块比较大,当时真正需要的时候加载比较慢;这个时候用到preload、 prefetch
5.2.1 hello.js
hello.js
module.exports = "hello";
index.js
document.querySelector('#clickBtn').addEventListener('click',() => {import('./hello').then(result => {// import()语句是天然的代码分割点,如果遇到import()就会分割出一个单独的代码块,单独加载console.log(result.default);});});
index.html
<button id="clickBtn">点我</button>
5.2.2 按需加载
- 如何在react项目中实现按需加载?
5.2.2.1 index.js
index.js ```javascript import React, { Component, Suspense } from “react”; import ReactDOM from “react-dom”; import Loading from “./components/Loading”; /* function lazy(loadFunction) { return class LazyComponent extends React.Component { state = { Comp: null }; componentDidMount() {
} render() {loadFunction().then((result) => {this.setState({ Comp: result.default });});
} }; } / const AppTitle = React.lazy(() => import(/ webpackChunkName: “title” */ “./components/Title”) );let Comp = this.state.Comp; return Comp ? <Comp {...this.props} /> : null;
class App extends Component {
constructor(){
super();
this.state = {visible:false};
}
show(){
this.setState({ visible: true });
};
render() {
return (
<>
{this.state.visible && (
<a name="ORlr9"></a>
#### 5.2.2.2 Loading.js
src\components\Loading.js
```javascript
import React, { Component, Suspense } from "react";
export default (props) => {
return <p>Loading</p>;
};
5.2.2.3 Title.js
src\components\Title.js
import React, { Component, Suspense } from "react";
export default props=>{
return <p>Title</p>;
}
5.2.3 preload 预先加载
- preload通常用于本页面用到的关键资源,包括js、字体、css文件
- preload将会把资源的下载权重提高,使得关键数据提前下载好,优化页面打开速度
- 在资源上添加预先加载的注释,你指明该模块需要立即被使用
- 一个资源的加载的优先级分为五个级别:
- Highest 最高
- High 高
- Medium 中等
- Low 低
- Lowest 最低
- 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是Low
- preload-webpack-plugin

<link rel="preload" as="script" href="utils.js">
import(
`./utils.js`
/* webpackPreload: true */
/* webpackChunkName: "utils" */
)
5.2.4 prefetch 预先拉取
prefetch跟preload不同,它的作用是告诉浏览器未来可能使用的某个资源,浏览器将会在闲时取加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" href="utils.js" as="script">button.addEventListener('click', () => { import( `./utils.js` /* webpackPrefetch: true */ /* webpackChunkName: "utils" */ ).then(result => { result.default.log('hello'); }) });5.2.5 preload vs prefetch
preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源,浏览器会提升加载优先级为hight
- prefetch是告诉浏览器未来可能需要用到某个资源,浏览器不一定会加载这些资源,浏览器会以一个非常低的优先级lowest,在自己闲着没事干加载这个资源;不需要webpack插件,默认支持
- 建议:对于页面当前很有必要的资源使用preload(不要轻易用,会阻塞我们浏览器渲染),对于可能在将来页面中的资源使用prefetch ```javascript <!DOCTYPE html>
<a name="x0OA4"></a>
## 5.3 提取公共组件
- [common-chunk-and-vendor-chunk](https://github.com/webpack/webpack/tree/master/examples/common-chunk-and-vendor-chunk)
- [split-chunks-plugin](https://www.webpackjs.com/plugins/split-chunks-plugin)
- 怎么配置单页应用?怎么配置多页应用?
<a name="loCgc"></a>
### 5.3.1 为什么需要提取公共代码
大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共的代码。如果都包含进来会有问题:
- 相同资源被重复加载,浪费用户的流量和服务器成本
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验
- 如果能把公共代码抽离成单独文件进行加载,可以减少网络传输流量,降低服务器成本
<a name="xlBpU"></a>
### 5.3.2 如何提取
- 基础类库,方便长期缓存
- 页面之间的共用代码
- 各个页面单独生成文件
<a name="EFJZA"></a>
### 5.3.3 splitChunks
<a name="d1ShP"></a>
#### 5.3.3.1 module chunk bundle
- module(引入的文件):就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码
- chunk: chunk是webpack根据功能拆分出来的,包含三种情况(代码分割原则)
- 你的项目入口(entry),被多个入口共享,或者来自node_module都会被分割
- 通过import()动态引入的代码
- 通过splitChunks拆分出来的代码
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出
一个文件对应一个代码块(chunk),一个代码块对应一个bundle
<a name="NIO1F"></a>
#### 5.3.3.2 默认配置
```javascript
optimization: {
splitChunks: {
chunks: "all",//表示要分割哪些代码块,值为all=异步+同步/initial同步/async异步
minSize: 30000, //默认值是30kb,代码块的最小尺寸
minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 5, //按需加载最大并行请求数量
maxInitialRequests: 3, //一个入口的最大并行请求数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: '~',//默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "initial",
test: /node_modules/,//条件
priority: -10 ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
commons: {
chunks: "initial",
minSize: 0,//最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20,
reuseExistingChunk: true// 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
}
}
},
}
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
{
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js",
page3: "./src/page3.js",
},
optimization: {
splitChunks: {
chunks: "all", //默认作用于异步chunk,值为all/initial/async
minSize: 0, //默认值是30kb,代码块的最小尺寸
minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 3, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
maxInitialRequests: 5, //限制入口的拆分数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: {
//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "all",
test: /node_modules/, //条件
priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
default: {
chunks: "all",
minSize: 0, //最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20,
reuseExistingChunk: false
}
},
runtimeChunk:true // 运行时代码块,把运行时提取出去,变成一个chanck
},
plugins:[
new HtmlWebpackPlugin({
template:'./src/index.html',
chunks:["page1"],
filename:'page1.html'
}),
new HtmlWebpackPlugin({
template:'./src/index.html',
chunks:["page2"],
filename:'page2.html'
}),
new HtmlWebpackPlugin({
template:'./src/index.html',
chunks:["page3"],
filename:'page3.html'
})
]
}
src\page1.js
import module1 from "./module1";
import module2 from "./module2";
import $ from "jquery";
console.log(module1, module2, $);
import(/* webpackChunkName: "asyncModule1" */ "./asyncModule1");
src\page2.js
import module1 from "./module1";
import module2 from "./module2";
import $ from "jquery";
console.log(module1, module2, $);
src\page3.js
import module1 from "./module1";
import module3 from "./module3";
import $ from "jquery";
console.log(module1, module3, $);
src\module1.js
console.log(“module1”);
src\module2.js
console.log(“module2”);
src\module3.js
console.log(“module3”);
src\asyncModule1.js
import from ‘lodash’; console.log();

- 提取第三方模块,jquery,lodash
- 提取公共模块
多入口才需要分包,单入口懒加载就够了。不需要走分包逻辑了
6.开启Scope Hoisting 作用域提升,自带的
- Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。
- 初webpack转换后的模块会包裹上一层函数,import会转换成require
- 代码体积更小,因为函数申明语句会产生大量代码
- 代码在运行时因为创建的函数作用域更少了,内存开销也随之变小
- 大量作用域包裹代码会导致体积增大
- 运行时创建的函数作用域变多,内存开销增大
- scope hoisting的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
- 这个功能在mode为production下默认开启,开发环境要用 webpack.optimize.ModuleConcatenationPlugin插件
- 也要使用ES6 Module,CJS不支持
export default 'Hello';import str from './hello.js'; console.log(str);"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) { __webpack_require__.r(__webpack_exports__); var hello = ('hello'); console.log(hello); })函数由两个变成了一个,hello.js 中定义的内容被直接注入到了 main.js 中
7.利用缓存
webpack中利用缓存一般有以下几种思路:
Babel在转义js文件过程中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗
{ test: /\.js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", options: { cacheDirectory: true // 启用缓存 } }] },7.2 cache-loader
在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里
存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader
npm i cache-loader -Dconst loaders = ['babel-loader']; module.exports = { module: { rules: [ { test: /\.js$/, use: [ 'cache-loader', ...loaders ], include: path.resolve('src') } ] } }cache选项是默认开启的,而且是放在内存里的
- 有了这个cache,其他缓存都不需要了,一个足够。
cache-loader是webpack4中配置的,webpack5中不需要配置了
module.exports = { module: { mode:'prodution', entry:'./src/index.js', cache:false, ..... } }8. oneOf
每个文件对于rules中的所有规则都会遍历一遍,如果使用oneOf就可以解决该问题,只要能匹配一个即可退出。(注意:在oneOf中不能两个配置处理同一种类型文件)
module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, //优先执行 enforce: 'pre', loader: 'eslint-loader', options: { fix: true } }, { // 以下 loader 只会匹配一个 oneOf: [ ..., {}, {} ] } ] } }
