# 1. 安装
cnpm i react react-dom -S
cnpm install webpack webpack-cli webpack-dev-server image-webpack-loader mini-css-extract-plugin purgecss-webpack-plugin babel-loader @babel/core @babel/preset-env @babel/preset-react terser-webpack-plugin html-webpack-plugin optimize-css-assets-webpack-plugin mini-css-extract-plugin qiniu webpack-bundle-analyzer -D
# 2.压缩JS
optimization: {
minimize: true,
minimizer: [
//压缩JS
+ new TerserPlugin({})
]
},
# 3. 压缩CSS
optimization: {
minimize: true,
minimizer: [
//压缩CSS
+ new OptimizeCSSAssetsPlugin({}),
]
},
# 4. 压缩图片
{
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,
+ }
+ }
+ }
]
}
# 5. 清除无用的CSS
- 单独提取CSS并清除用不到的CSS
const path = require("path");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const PurgecssPlugin = require("purgecss-webpack-plugin");
module.exports = {
module: {
rules: [
{
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 }),
+ })
]
devServer: {},
};
# 6. Tree Shaking
- 一个模块可以有多个方法,只要其中某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会uglify阶段擦除掉
- 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量
- webpack默认支持,在.babelrc里设置module:false即可在production mode下默认开启
module.exports = {
+ mode:'production',
+ devtool:false,
module: {
rules: [
{
test: /\.js/,
include: path.resolve(__dirname, "src"),
use: [
{
loader: "babel-loader",
options: {
+ presets: [["@babel/preset-env", { "modules": false }]],
},
},
],
}
}
}
# 7. Scope Hoisting
- Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 "作用域提升",是在 Webpack3 中新推出的功能。
- scope hoisting的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
- 这个功能在mode为
production下默认开启,开发环境要用webpack.optimize.ModuleConcatenationPlugin插件
hello.js
export default 'Hello';
index.js
import str from './hello.js';
console.log(str);
main.js
var hello = ('hello');
console.log(hello);
# 8. 代码分割
- 对于大的Web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被用到。
- webpack有一个功能就是将你的代码库分割成chunks语块,当代码运行到需要它们的时候再进行加载
# 8.1 入口点分割
- Entry Points:入口文件设置的时候可以配置
- 这种方法的问题
- 如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中
- 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
entry: {
index: "./src/index.js",
login: "./src/login.js"
}
# 8.2 动态导入和懒加载
用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时
- 一般采用以下原则:
- 对网站功能进行划分,每一类一个chunk
- 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
- 被分割出去的代码需要一个按需加载的时机
hello.js
module.exports = "hello";
index.js
document.querySelector('#clickBtn').addEventListener('click',() => {
import('./hello').then(result => {
console.log(result.default);
});
});
index.html
<button id="clickBtn">点我</button>
# 8.3 preload(预先加载)
- preload通常用于本页面要用到的关键资源,包括关键js、字体、css文件
- preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
- 在资源上添加预先加载的注释,你指明该模块需要立即被使用
- 一个资源的加载的优先级被分为五个级别,分别是
- Highest 最高
- High 高
- Medium 中等
- Low 低
- Lowest 最低
- 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是
Low
<link rel="preload" as="script" href="utils.js">
import(
`./utils.js`
/* webpackPreload: true */
/* webpackChunkName: "utils" */
)
# 8.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');
})
});
# 8.5 preload vs prefetch
- preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
- 而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源
- 所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch
# 8.6 提取公共代码
# 8.6.1 为什么需要提取公共代码
- 大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题
- 相同的资源被重复的加载,浪费用户的流量和服务器的成本;
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
- 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本
# 8.6.2 如何提取
- 基础类库,方便长期缓存
- 页面之间的公用代码
- 各个页面单独生成文件
# 8.6.3 splitChunks
# 8.6.3.1 module chunk bundle
- module:就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码
- chunk: chunk是webpack根据功能拆分出来的,包含三种情况
- 你的项目入口(entry)
- 通过import()动态引入的代码
- 通过splitChunks拆分出来的代码
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出
# 8.6.3.2 默认配置
webpack.config.js
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: 2, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
maxInitialRequests: 4, //限制入口的拆分数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: {
//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "all",
test: /node_modules/, //条件
priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
commons: {
chunks: "all",
minSize: 0, //最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20
}
}
}
src\page1.js
import utils1 from "./module1";
import utils2 from "./module2";
import $ from "jquery";
console.log(utils1, utils2, $);
import(/* webpackChunkName: "asyncModule1" */ "./asyncModule1");
src\page2.js
import utils1 from "./module1";
import utils2 from "./module2";
import $ from "jquery";
console.log(utils1, utils2, $);
src\page3.js
import utils1 from "./module1";
import utils3 from "./module3";
import $ from "jquery";
console.log(utils1, utils3, $);
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(_);
Asset Size Chunks Chunk Names
asyncModule1.chunk.js 740 bytes asyncModule1 [emitted] asyncModule1
index.html 498 bytes [emitted]
page1.js 10.6 KiB page1 [emitted] page1
page1~page2.chunk.js 302 bytes page1~page2 [emitted] page1~page2
page1~page2~page3.chunk.js 308 bytes page1~page2~page3 [emitted] page1~page2~page3
page2.js 7.52 KiB page2 [emitted] page2
page3.js 7.72 KiB page3 [emitted] page3
vendors~asyncModule1.chunk.js 532 KiB vendors~asyncModule1 [emitted] vendors~asyncModule1
vendors~page1~page2~page3.chunk.js 282 KiB vendors~page1~page2~page3 [emitted] vendors~page1~page2~page3
Entrypoint page1 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page1~page2.chunk.js page1.js
Entrypoint page2 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page1~page2.chunk.js page2.js
Entrypoint page3 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page3.js

# 6. CDN
- 最影响用户体验的是网页首次打开时的加载等待。 导致这个问题的根本是网络传输过程耗时大,CDN的作用就是加速网络传输。
- CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度
- 缓存配置
- HTML文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的URL变成指向CDN服务器的地址
- 静态的JavaScript、CSS、图片等文件开启CDN和缓存,并且文件名带上HASH值
- 为了并行加载不阻塞,把不同的静态资源分配到不同的CDN服务器上
- 域名限制
- 同一时刻针对同一个域名的资源并行请求是有限制
- 可以把这些静态资源分散到不同的 CDN 服务上去
- 多个域名后会增加域名解析时间
- 可以通过在 HTML HEAD 标签中 加入去预解析域名,以降低域名解析带来的延迟
# 6.1 webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const UploadPlugin = require("./plugins/UploadPlugin");
const glob = require("glob");
const PATHS = {
src: path.join(__dirname, "src"),
};
module.exports = {
mode: "development",
devtool: false,
context: process.cwd(),
entry: {
main: "./src/index.js",
},
output: {
path: path.resolve(__dirname, "dist"),
+ filename: "[name].[hash].js",
+ chunkFilename: "[name].[hash].chunk.js",
+ publicPath: "http://img.zhufengpeixun.cn/",
},
optimization: {
minimize: true,
minimizer: [
//压缩JS
/* new TerserPlugin({
sourceMap: false,
extractComments: false,
}),
//压缩CSS
new OptimizeCSSAssetsPlugin({}), */
],
//自动分割第三方模块和公共模块
splitChunks: {
chunks: "all", //默认作用于异步chunk,值为all/initial/async
minSize: 0, //默认值是30kb,代码块的最小尺寸
minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 2, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
maxInitialRequests: 4, //限制入口的拆分数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: {
//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "all",
test: /node_modules/, //条件
priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
commons: {
chunks: "all",
minSize: 0, //最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20,
reuseExistingChunk: true, //如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
},
},
},
//为了长期缓存保持运行时代码块是单独的文件
/* runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
}, */
},
module: {
rules: [
{
test: /\.js/,
include: path.resolve(__dirname, "src"),
use: [
{
loader: "babel-loader",
options: {
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",
],
},
{
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,
},
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: "./src/index.html",
}),
new MiniCssExtractPlugin({
+ filename: "[name].[hash].css",
}),
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
new UploadPlugin({}),
],
devServer: {},
};
# 6.2 UploadPlugin.js
const qiniu = require("qiniu");
const path = require("path");
//https://developer.qiniu.com/kodo/sdk/1289/nodejs
require("dotenv").config();
const defaultAccessKey = process.env.accessKey;
const defaultSecretKey = process.env.secretKey;
class UploadPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
//在webpack把产出的文件写入硬盘后
compiler.hooks.afterEmit.tap("UploadPlugin", (compilation) => {
//把本次编译产出的文件上传到七牛上
let assets = compilation.assets;
//html不需要强缓存的,是需要放在自己的服务器上,不需要缓存,
let promises = Object.keys(assets).filter(item=>!item.includes('.html')).map(this.upload.bind(this));
Promise.all(promises).then((err, data) => console.log(err, data));
});
}
upload(filename) {
return new Promise((resolve, reject) => {
let {
bucket = "cnpmjs",
accessKey = defaultAccessKey,
secretKey = defaultSecretKey,
} = this.options;
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
let options = {
scope: bucket,
};
let putPolicy = new qiniu.rs.PutPolicy(options);
let uploadToken = putPolicy.uploadToken(mac);
let config = new qiniu.conf.Config();
let localFile = path.resolve(__dirname, "../dist", filename);
let formUploader = new qiniu.form_up.FormUploader(config);
let putExtra = new qiniu.form_up.PutExtra();
formUploader.putFile(
uploadToken,
filename,
localFile,
putExtra,
(err, body, info) => {
err ? reject(err) : resolve(body);
}
);
});
}
}
module.exports = UploadPlugin;