- 做优化应该是一个有指标、有比较、有数据的过程
- 一个完整的解决方案应该是说清楚标准,讲清楚缘由,算清楚结果 ,最后用数据与收益来说明工作成果
一、建立衡量标准
在确认指标之后,还要有量化基础,有数据积累。
理论基础 - 以『用户为核心』的 RAIL 性能模型
- Response(响应):应该尽可能快速的响应给客户,应该在 100ms 或者 100ms 以内响应用户输入;
- Animation(动画):在展示动画的时候,每一帧应该以 16ms 进行渲染,这样可以保持动画效果的一次性,并且避免卡顿;
- Idle(浏览器空闲时间):当使用 JavaScript 主线程时,应该把任务划分到执行时间小于 50ms 的片段中,这样可以释放线程以进行用户交互;
- Load(加载):应该在 1s 的时间内加载完你的网站,并可以进行用户交互。
衡量工具
- Chrome DevTools
- Performance:可查看性能指标,并有网页快照。
- Network:可以查看各个资源的加载时间。
- Lighthouse :非常流行的第三方性能评测工具,支持移动端和 PC。
在报告中会对诸如初次内容渲染、可交互时间、加载等进行具体的数值量化打分。
需要注意:
- Lighthouse 并不能真实的反应出每个用户的设备的实际性能数据;
- Lighthouse 的分数反应的是业界的标准,而非项目实际需求的标准。
配置:Chrome ➡️ 开发者工具 ➡️ Lighthouse ➡️ generate report。

- WebPageTest:在线测量和分析网页性能。

- PageSpeed Insights:在线评估网站性能和加载速度。

性能指标
- Navigation Timing API:浏览器提供的 JS API,用于在客户端测量网页加载的性能。
- Lighthouse Performance
- FCP(First Contentful Paint):记录首次加载并绘制内容的时间。
- TTI(Time to Interact):页面可交互的时间。通常通过记录
window.performance.timing
中的loadEventStart
与fetchStart
的时间差来完成。 - LCP(Lagest Contentful Paint):最大可见元素绘制。
- TBT(Total Blocking Time):指从一个请求发送到接收到响应所花费的时间,通常以毫秒为单位。
- CLS(Cumulative Layout Shift):衡量在网页的整个生命周期内发生的所有意外布局偏移的得分总和。得分是零到任意正数,其中 0 表示无偏移,且数字越大,网页的布局偏移越大。为了提供良好的用户体验,网站应努力使 CLS 得分不超过0.1。

二、制定优化方案
我们可以从以下几方面进行优化。
- DNS 解析
- TCP 连接优化
- 请求优化
- 页面渲染优化
- JS 优化
- CSS 优化
- React 优化
- 图片优化
- Webpack 打包优化
DNS 解析
1. DNS 预解析
通过使用dns-prefetch
DNS 预解析,提前获取 IP 地址。
html
<link rel="dns-prefetch" href="xxx" />
2. 域名收敛
减少页面中域名的数量,从而减少 DNS 解析次数。
TCP 连接优化
1. 使用preconnect
提前和目标服务器进行连接
这个过程包括 DNS 查询得到 IP、TCP 三次握手、HTTP 或 HTTPS 连接。
html
<link rel="preconnect" href="xxx" />
请求优化
1. 可以使用 http/2 协议
依赖 http/2 的多路复用、头部压缩、二进制传输、服务端推送等特性,从而加快整体请求的响应速度,加快页面的渲染展示。
-
多路复用
多路复用允许同时通过单一的 http/2 连接发送多重请求-响应信息。这个功能相当于是长连接的增强,每个 request 请求可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里。
另外,多路复用也支持了流的优先级,允许客户端告诉服务器哪些内容是更高优先级的资源,可以优先传输。
改善了:在 http1.1 中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。
-
头部压缩
HTTP1.x 的 header 带有大量信息,而且每次都要重复发送。
HTTP2.0 使用 HPACK 算法对 header 的数据进行压缩,减少需要传输的 header 大小,通讯双方各自缓存一份 header fields 表,差量更新 HTTP 头,既避免了重复 header 的传输,又减小了需要传输的大小。
-
二进制传输
HTTP1.0 的解析是基于文本的,HTTP 2.0 会将所有的传输信息分割为更小的信息或者帧,并对它们进行二进制编码,基于二进制可以让协议有更多的扩展性,比如使用帧来传输数据和指令。
HTTP2.0 在应用层(HTTP2.0)和传输层(TCP/UDP)之间增加一个二进制分帧层。在不改动 HTTP1.x 的语义、方法、状态码、URI 以及首部字段的情况下,解决了 HTTP 1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。
在二进制分帧层中,HTTP2.0 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。
帧: HTTP 2.0 数据通信的最小单位信息(指 HTTP 2.0 中逻辑上的 HTTP 信息)。例如请求和响应等,消息由一个或多个帧组成。
流: 存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数 ID。 -
服务端推送
服务端推送是一种在客户端请求之前发送数据的机制。
服务端可以在发送 HTML 页面时主动推送其他资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如,服务端可以主动把 JS、CSS 文件推送给服务端,而不需要客户端解析 HTML 时再发送这些请求。
服务端推送的这些资源被保存在了客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然快很多。
2. 静态资源使用 CDN
CDN(内容分发网络,Content Delivery/Distribut Nectwork),是建立并覆盖在承载网之上,由分配在不同区域的边缘节点服务器群组成的分布式网络。
CDN 加速的本质是缓存加速 。将服务器上存储的静态内容缓存到 CDN 节点上,当访问这些静态内容时,无需访问服务器源站,就近访问 CDN 节点即可获取相同内容,从而达到加速的效果,同时减轻服务器源站的压力。
简单来说,CDN就是根据用户位置分配最近的资源。
CDN 工作原理
在没有 CDN 时,我们使用域名访问一个站点的路径为:用户提交域名 -> 浏览器对域名进行解析 -> DNS 解析得到目的主机的 IP 地址 -> 根据 IP 地址发出请求 -> 得到请求数据并回复。
使用 CDN 后,DNS 返回的不再是一个 IP 地址,而是一个 CName(Canonical Name)别名记录,指向 CDN 的全局负载均衡。

- 当用户在浏览器上输入 URL 时,经过本地 DNS 系统解析,DNS 系统会最终将域名的解析权交给 CNAME 指向的 CDN 专用 DNS 服务器;
- CDN 专用 DNS 服务器将 CDN 全局负载均衡设备的 IP 地址返回给用户;
- 用户向 CDN 全局负载均衡设备发起内容 URL 访问请求;
- CDN 全局负载均衡设备根据用户 IP 地址以及用户请求的内容 URL,选择一台用户所属区域的 CDN 区域负载均衡设备,告诉用户向这台设备发起请求;
- CDN 区域负载均衡设备会为用户选择一台合适的缓存服务器提供服务,然后 CDN 区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的 IP 地址(选择的依据包括:根据用户 IP 地址,判断哪一台服务器距用户最近;根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需内容;查询各个服务器当前的负载情况,判断哪一台服务器尚有负载能力等等);
- 全局负载均衡设备把缓存服务器的 IP 地址返回给用户;
- 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容返回给用户。如果这台缓存服务器上没有用户想要的内容,而区域负载均衡设备依然将它分配给了用户,那么这台缓存服务器就需要向它的上一层缓存服务器请求内容,直至追溯到网站的源服务器将内容拉回到本地。
为什么动态内容不适合放在 CDN 上呢?
由于访问动态内容时,每次都需要访问服务器,由服务器动态生成实时的数据并返回。因此 CDN 的缓存加速不适用于加速动态内容,CDN 无法缓存实时变化的动态内容。对于动态内容请求,CDN 节点只能转发回服务器源站,没有加速效果。
3. 优化 http 缓存策略
4. 请求接口优化
页面渲染优化
1. 骨架屏、Loading 图标
2. 服务端渲染(SSR)、Next.js
JS 优化
1. 防抖和节流
-
节流
规定在一个单位时间内只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
适用场景: 节流适用于需要控制函数执行频率的场景,比如滚动、按钮点击、提交表单等。js// 手写简化版实现 // 1. 定时器实现(非立即执行版) const throttle = (fn, delay = 500) => { let flag = false; return (...args) => { if(flag) return; flag = true; setTimeout(() => { fn.apply(this, args); flag = false; }, delay); } } // 2. 时间戳实现(立即执行版) const throttle = (fn, delay = 500) => { let preTime = 0; return (...args) => { const nowTime = Date.now(); if(nowTime - preTime >= delay) { preTime = Date.now(); fn.apply(this, args); } } } // 3. 实现参数控制是立即执行还是非立即执行的节流 const throttle = (fn, delay = 500, immediate = false) => { let preTime = immediate ? 0 : Date.now(); return (...args) => { const nowTime = Date.now(); if(nowTime - preTime >= delay) { preTime = Date.now(); fn.apply(this, args); } } } // 4. lodash.throttle (https://www.lodashjs.com/docs/lodash.throttle) const throttle = lodash.throttle(() => fn(), delay, options)
-
防抖
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
适用场景: 防抖适用于需要等待用户停止某个操作后执行的场景,如搜索、输入框变化、联想词等。js// 手写简化版实现 // 1. 定时器实现(非立即执行) const debounce = (fn, delay) => { let timeId = null; return (...args) => { if(timeId) { clearTimeout(timeId); } timeId = setTimeout(() => { fn.apply(this, args); }, delay); } }; // 2. lodash.debounce(https://www.lodashjs.com/docs/lodash.debounce) const debounce = lodash.debounce(() => fun(), delay, options); // 3. 防抖如果需要立即执行,则需要第三个参数 const debounce = function(func, delay, immediate) { let timeId; return function () { let context = this; let args = arguments; if (timeId) clearTimeout(timeId); // timeId 不为null if (immediate) { let callNow = !timeId; // 第一次会立即执行,以后只有事件执行后才会再次触发 timeId = setTimeout(function () { timeId = null; }, delay) if (callNow) { func.apply(context, args); } } else { timeId = setTimeout(function () { func.apply(context, args); }, delay); } } }
2. 避免 script 标签阻塞页面渲染
CSS 优化
1. 减少回流和重绘
React 优化
-
尽量使用
shouldComponent
、React.PureComponent
、React.memo
,避免不必要的 render。 -
类组件中避免使用内联函数,避免每次调用 render 函数时都要重新创建一个新的函数实例。
-
使用
Fragement
标签减少层级,避免额外标记。 -
循环使用
key
,但注意key
值不要设置为index
。 -
Hook 组件使用
useMemo
缓存值,useCallback
缓存函数。 -
使用
React.lazy
和Suspense
组件来实现按需加载/懒加载。jsxconst MyComponent = React.lazy(() => import('./MyComponent')); export const johanAsyncComponent = props => ( <React.Suspense fallback={<Spinner />}> <johanComponent {...props} /> </React.Suspense> );
-
路由懒加载
在使用前端路由时,使用按需加载路由组件,以减少初始加载体积。jsxconst Home = () => import('./Home.vue');
图片优化
1. 选择合适的图片格式
类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
JPEG/JPG | 色彩丰富;文件小;无兼容问题 | 有损压缩,反复保存图片质量下降明显;不支持动画;不支持透明 | 色彩丰富的图片/渐变图像 |
PNG | 简单图片尺寸小;无损压缩;支持透明 | 不支持动画;色彩丰富的图片尺寸大 | logo、icon、透明图 |
GIF | 文件小;支持动画、透明;无兼容性问题 | 只支持 256 种颜色 | 色彩简单的 logo、icon、动图 |
WebP | 文件小;支持有损和无损压缩;支持动画、透明 | 浏览器兼容性不好 | 支持 webp 格式的 app 和 webview |
2. 图片压缩
-
webpack 压缩
bash# 安装依赖 npm install image-webpack-loader -D
js// 配置 webpack module.exports = { ... module: { rules: [ { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: [ { loader: 'file-loader', options: { name: '[name].[hash:8].[ext]' }, }, { loader: 'image-webpack-loader', options: { // 用于指定使用 MozJPEG 来压缩 JPEG 图片 mozjpeg: { progressive: true, // 表示是否使用渐进式的方式压缩图片,这意味着在图片加载过程中,用户可以逐步看到清晰度递增的图片 quality: 75, // 质量(1-100),质量越高体积越大,质量越低体积越小;如果使用数组的话,就可以设置 0 到 1 之间的小数 }, // 用于指定使用 OptiPNG 来压缩 PNG 图片 optipng: { enabled: true, // 表示是否启用图片压缩的设置 }, // 用于指定使用 PNGQuant 来压缩 PNG 图片 pngquant: { quality: [0.5, 0.65], speed: 4, // 指定压缩的速度(0-11),其中 0 为最快但质量最差,11 为最慢但质量最好 }, // 用于指定使用 Gifsicle 来压缩 GIF 图片 gifsicle: { interlaced: false, }, // 不支持WEBP就不要写这一项 webp: { quality: 75 }, }, }, ], }, ], }, }
-
工具压缩
- tinypng:免费、批量、速度快;
- 智图压缩:免费、批量、好用;
- squoosh:在线图片压缩工具;
- compressor:支持 JPG、PNG、SVG、GIF。
3. 使用雪碧图
雪碧图(CSS Sprites),国内也叫 CSS 精灵,是一种 CSS 图像合成技术,主要用于小图片显示。
把诸多小图合成一张大图,用background-position
属性来确定图片位置,可以有效的减少请求个数。
适用于页面多且图片丰富的场景。
在 webpack 中webpack-spritesmith插件提供了自动生成雪碧图的功能,并且可以自动生成对应的样式文件。
4. 使用 iconfont
iconfont(字体图标),通过字体的方式展示图标,多用于渲染图标、简单图形、特殊字体等。
阿里字体图标库
5. 小图片转为 base64 编码字符串
js
// webpack.prod.js
module.exports = {
modules: {
rules: [
// 图片 --- 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
use: "url-loader",
options: {
limit: 5 * 1024, // 小于 5kb 的图片用 base64 格式产出;否则,依然延用 file-loader 的形式,产出 url
outputPath: "/img1/", // 打包到 img 目录下
},
},
},
]
}
}
6. 图片懒加载
-
lazy='loading'
jsx<img src="img.png" lazy="loading" />
-
使用 js 监听页面的滚动
使用 js 实现的主要原理是判断当前图片是否到了可视区域:- 拿到所有的图片 DOM;
- 遍历每个图片,判断当前图片是否到达可视区与范围;
- 如果到了,就设置
src
属性(页面初始化时,图片地址设置在data-src
属性上); - 绑定
window
的scroll
事件,对其进行事件监听。
html<img data-src="真实图片地址" src="初始化图片地址" /> <script> window.addEventListener('scroll', throttle(lazyLoad, 200)); function lazyLoad() { let viewHeight = document.body.clientHeight; // 获取可视区高度 let imgs = document.querySelectorAll('img[data-src]'); imgs.forEach((item) => { if (!item.dataset.src) { return; } // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置 let rect = item.getBoundingClientRect(); if (rect.bottom >= 0 && rect.top < viewHeight) { item.src = item.dataset.src; item.removeAttribute('data-src'); } }); } </script>
-
使用交叉观察器IntersectionObserver
IntersectionObserver
是浏览器原生提供的构造函数,可以自动"观察"元素是否可见。
IntersectionObserver
接收两个参数:callback
:可见性变化时的回调函数;option
:配置选项,可选。
目标元素的可见性变化时,会调用观察器的
callback
回调函数。callback
一般会触发两次,一次是目标元素刚刚进入视口(开始可见),一次是完全离开视口(开始不可见)。jsconst imgs = document.querySelectorAll('img[data-src]') const config = { rootMargin: '0px', threshold: 0, }; let observer = new IntersectionObserver((entries, self) => { entries.forEach((entry) => { if (entry.isIntersecting) { let img = entry.target; let src = img.dataset.src; if (src) { img.src = src; img.removeAttribute('data-src'); } // 解除观察 self.unobserve(entry.target); } }) }, config); imgs.forEach((image) => { // 开始观察 observer.observe(image); });
Webpack 打包优化
提高打包速度
1. 优化 babel loader
-
优化 loader 的文件搜索范围
jsmodule.exports = { module: { rules: [ { test: /\.js$/, // js 文件才使用 babel loader: ['babel-loader'], include: [resolve('src')], // 只在 src 文件下查找 exclude: /node_modules/ // 不会去查找的文件 } ] } }
-
将 babel 编译过的文件缓存起来
jsmodule.exports = { module: { rules: [ { test: /\.js$/, // js 文件才使用 babel use: { loader: 'babel-loader', options: { cacheDirectory: true } }, } ] } }
2. IgnorePlugin:避免引入无用模块
比如,使用 moment
库:import moment from 'moment'
,默认会引入所有语言 js 代码,代码过大,那么如何只引入中文?
js
// index.js
import 'moment/locale/zh-cn';
// webpack.prod.js
module.exports = {
plugin: [
new webpack.IgnorePlugin({ // 忽略 moment 下的 /locale 目录
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
]
}
3. noParse:避免重复打包
js
module.exports = {
module: {
noParse: [/react\/.min\.js$/]
}
}
IgnorePlugin 🆚 noParse:
- IgnorePlugin 直接不引入,代码中没有
- noParse 引入,但不打包
4. HappyPack:多进程打包
JS 是单线程的,开启多进程打包。
提高构建速度,特别是对于多核 CPU。
happypack 已经不维护了,可以使用 thread-loader代替
js
// webpack.prod.js
const HappyPack = require('happypack');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ["happyPack/loader?id=babel"], // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
include: srcPath,
},
]
},
plugins: [
// happyPack 开启多进程打包
new HappyPack({
id: "babel", // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
loaders: ["babel-loader?cacheDirectory"], // 如何处理 .js 文件,用法和 Loader 配置中一样
}),
]
}
5. ParallelUglifyPlugin:多进程压缩 JS
webpack 内置 Uglify 工具压缩 js。
JS 是单线程的,开启多进程压缩更快。
和 happyPack 同理。
关于开启多进程打包:
- 项目较大,打包较慢,开启多进程能提高速度
- 项目较小,打包很快,开启多进程会降低速度(进程开销)
js
// webpack.prod.js
module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 js 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// 还是使用 UglifyJS 压缩,只不过帮助开启了多进程
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
drop_console: true, // 删除所有的 console 语句,可以兼容 IE 浏览器
collapse_vars: true, // 内嵌定义了但是只用过一次的变量
reduce_vars: true, // 提取出出现多次,但是没有被定义成变量去引用的静态值
},
},
}),
]
}
/**
* var a = 10;
* var b = 20;
* var c = a + b;
* 会被编译成
* var c = 30;
*/
6. 使用 CDN 加速
-
配置 CDN 的公共路径
jsmodule.exports = { output: { filename: "[name].[contenthash:8].js", // name 即多入口时,entry 的 key path: distPath, publicPath: "https://cdn.abc.com", // 修改所有静态文件 url 的前缀(如 cdn 域名) }, module: { rules: [ { test: /\.(png|jpg|jpeg|gif)$/, use: { loader: "url-loader", options: { // 小于 5kb 的图片用 base64 格式产出 // 否则,依然延用 file-loader 的形式,产出 url limit: 5 * 1024, outputPath: "/img1/", // 打包到 img 目录下 publicPath: "http://cdn.abc.com", // 设置图片的 cdn 地址(也可以统一在外面的 ) }, }, } ] } }
-
将打包后的结果(dist 目录)上传到 CDN 服务器上。
缩小打包体积
1. 按需加载
使用 Webpack 等构建工具可以通过代码分割和动态导入来实现按需加载。通过import()
语法,可以在运行时动态加载模块。
js
const module = import('./module.js');
module.then((module) => {
// 使用加载的模块
});
2. 提取公共代码
js
// webpack.prod.js
module.exports = {
plugins: [
// 多入口 ------ 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面需要引用哪些 chunk(即上面的 index 和 other)
chunks: ['index', 'vendor', 'common'] // 考虑代码分割
}),
],
optimization: {
// 分割代码块
splitChunks: {
/**
* initial: 入口 chunks,对于异步导入的文件不处理
* async: 异步 chunk,只对异步导入的文件处理
* all: 全部 chunk
*/
chunks: 'all',
// 缓存分组
cacheGroups: {
// 第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!
test: /node_modules/,
minSize: 0, // 大小限制
minChunks: 1, // 最少复用几次
},
// 公共的模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
}
}
3. bundle 加 hash
hash 通常被作为前端静态资源实现增量更新的方案,通过在文件名上带上一串 hash 字符串,告诉浏览器该文件是否发生更新,从而决定是否要使用缓存机制。
Webpack 打包时的 hash 有三种:fullhash(Webpack4.x 之前的叫 hash,Webpack5.x 叫 fullhash 或 hash 都可)、chunkhash 和 contenthash。
在生产环境下,我们对 output 中打包的文件名一般采用 chunkhash,对于 css 等样式文件,采用 contenthash,这样可以使得每个模块最小范围的改变 hash 值。
一方面可以最大程度的利用浏览器缓存机制,提升用户体验;
另一方面,合理利用 hash 也减少了 webpack 再次打包所要处理的文件数量,提升了打包速度。
-
fullhash
fullhash 是全量的 hash,是整个项目级别的,只要项目中有任何的一个文件发生变动,打包后所有文件的 hash 值都会改变。
js// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[hash].js", clean: true, } }
执行
npx webpack
命令进行项目打包。文件改动前打包 文件改动后打包 -
chunkhash
chunkhash 根据不同的入口文件(entry)进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。当某个文件内容发生变动时,再次执行打包,只有该文件以及依赖该文件的文件的打包结果 hash 值会发生改变。
js// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[chunkhash].js", clean: true, } }
文件改动前打包 文件改动后打包 因为改动了
add.js
文件,所以依赖了这个文件的index.js
文件的 hash 值也会发生改变,但是除此之外index.css
文件的 hash 值也发生了改变,这是因为在index.js
中引用了index.css
,打包后它们属于一个模块。 -
contenthash
contenthash 是只有当文件自己的内容发生改变时,其打包的 hash 值才会发生变动。
js// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, "./dist"), filename: "[name].[conenthash].js", clean: true, } }
文件改动前打包 文件改动后打包 只有 add.js 和 index.js 文件的 hash 值发生了改变。
4. 使用production
模式
js
module.exports = {
mode: 'production'
}
- 自动开启压缩
- Vue、React 等会自动删掉测试代码(如开发环境的 waring)
- 启用 Tree Shaking
Tree Shaking
Tree Shaking 可以用来删除项目中未被引用的代码。
- Tree Shaking 的过程:
Tree shaking 的过程主要涉及静态分析、标记、和剔除未使用的代码,- 静态分析: Tree shaking 依赖于模块系统的静态性质,因此它可以在编译时进行静态分析。这表示在代码中通过 ES6 模块语法(import/export)明确声明了模块之间的依赖关系。
- 标记未使用的代码: Tree shaking 通过标记(标记为"无用"或"未使用")未被引用的模块、变量、函数等,以便后续的步骤能够识别并移除这些无用的部分。这一步通常是通过静态分析工具进行的。
- 依赖关系分析: Tree shaking 分析模块之间的依赖关系,以确定哪些模块是被引用的,哪些模块是没有被引用的。
- 剔除未使用的代码: 根据之前的标记和分析,tree shaking 将移除未被引用的代码。这个步骤通常由构建工具(如 Webpack)的优化阶段完成,通过配置启用压缩工具(如 UglifyJSPlugin 或 TerserPlugin)来实现。
- 在 JavaScript 中,这可能涉及到删除未被引用的函数、类、变量等。
- 在样式表中,未使用的样式规则可能会被移除。
- 输出优化后的代码: 最终,tree shaking 生成一个优化后的代码包,其中只包含实际被应用程序使用的代码。这有助于减小应用程序的文件大小,提高加载性能。
注意事项:
- Tree shaking 只能剔除通过 ES6 模块导入的未使用代码。CommonJS 等其他模块系统的导入方式不会触发 tree shaking。
- 某些情况下,例如动态导入(
import()
)和使用require.context
的代码,可能会影响 tree shaking 的效果,需要额外的注意。 - 在一些情况下,特别是使用第三方库时,可能需要手动标记未使用的代码或者使用专门设计用于 tree shaking 的库版本。
5. Scope Hoisting 作用域提升
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中。
js
const ModuleConcatenationPlugin = require('webpack/lib/ModuleConcatenationPlugin');
module.exports = {
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin()
]
}
『性能优化方案』实时更新中,有问题欢迎指正~
