前端优化
# 前端性能优化指标
FP 和 FCP 可能是相同的时间,也可能是先 FP 后 FCP。
# 白屏时间 FP
白屏时间(First Paint) FP :从页面开始加载到浏览器开始显示内容的时间。
中间过程包括 DNS 查询、建立 TCP 链接、发送首个 HTTP 请求、返回 HTML 文档、HTML 文档 head 解析完毕。
影响白屏时间的因素:网络、服务端性能、前端页面结构设计。
白屏时间 = 页面开始展示的时间点 - 开始请求的时间点
我们通常认为浏览器开始渲染 <body>
标签或者解析完 <head>
标签的时刻就是页面白屏结束的时间点。
计算方法
window.performance.timing.domLoading - window.performance.timing.navigationStart
可使用 Performance API 时:白屏时间 = firstPaint - performance.timing.navigationStart;
不可使用 Performance API 时:白屏时间 = firstPaint - pageStartTime;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>白屏时间计算-常规方法</title>
<script>
window.pageStartTime = Date.now();
</script>
<link
rel="stylesheet"
href="https://b-gold-cdn.xitu.io/ionicons/2.0.1/css/ionicons.min.css"
/>
<link
rel="stylesheet"
href="https://b-gold-cdn.xitu.io/asset/fw-icon/1.0.9/iconfont.css"
/>
<script>
window.firstPaint = Date.now();
console.log(`白屏时间:${window.firstPaint - window.pageStartTime}`);
</script>
</head>
<body>
<div>这是常规计算白屏时间的示例页面</div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 首屏时间 FCP
首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点
计算方法:
window.performance.timing.domInteractive - window.performace.timing.navigationStart
首屏模块标签标记法
适用场景:通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况
在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳
首屏时间 = window.firstScreen - window.pageStartTime
适用场景:
(1)首屏内不需要拉取数据,否则可能拿到首屏线获取时间的时候,首屏还是空白
(2)不需要考虑图片加载,只考虑首屏主要模块
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>首屏时间计算-标记首屏标签模块</title>
<script>
window.pageStartTime = Date.now();
</script>
<link
rel="stylesheet"
href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image"
/>
<link
rel="stylesheet"
href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image"
/>
</head>
<body>
<div>首屏时间计算-标记首屏标签模块</div>
<div class="module-1"></div>
<div class="module-2"></div>
<script type="text/javascript">
window.firstScreen = Date.now();
console.log(`首屏时间:${window.firstScreen - window.pageStartTime}`);
</script>
<div class="module-3"></div>
<div class="module-4"></div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- 统计首屏内加载最慢的图片的时间
在一个页面中,图片资源往往是比较后加载完的,因此可以统计首屏加载最慢的图片是否加载完成,加载完了,记录结束时间。
首屏时间 = window.firstScreen - window.pageStartTime
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>首屏时间计算-统计首屏最慢图片加载时间</title>
<script>
window.pageStartTime = Date.now();
</script>
</head>
<body>
<img
src="https://gitee.com/HanpengChen/blog-images/raw/master/blogImages/2021/spring/20210107155629.png"
alt="img"
onload="load()"
/>
<img
src="https://gitee.com/HanpengChen/blog-images/raw/master/blogImages/2020/autumn/article-gzh-qrcode.png"
alt="img"
onload="load()"
/>
<script>
function load() {
window.firstScreen = Date.now();
}
window.onload = function() {
// 首屏时间
console.log(window.firstScreen - window.pageStartTime);
};
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
如何知道首屏内哪张图片加载最慢?
我们可以拿到首屏内所有的图片,遍历它们,逐个监听图片标签的 onload 事件,并收集到它们的加载时间,最后比较得到加载时间的最大值。
适用场景:
首屏元素数量固定的页面,比如移动端首屏不论屏幕大小都展示相同数量的内容。
但是对于首屏元素不固定的页面,这种方案并不适用,最典型的就是 PC 端页面,不同屏幕尺寸下展示的首屏内容不同。上述方案便不适用于此场景。
- 自定义首屏内容计算法,就是根据首屏内接口计算比较得出最迟的时间
# 首次可交互时间 TTI
首次可交互时间(Time to Interactive,TTI)是指网页需要多长时间可提供完整交互能力
# 最大内容绘制 LCP
最大内容绘制(Largest Contentful Paint,LCP) 标记了渲染最大文本或图片的时间
# 速度指数 Speed Index
速度指数 Speed Index 表明了网页内容的可见填充速度
# 阻塞总时间 TBT
阻塞总时间,TBT(Total Blocking Time)首次内容渲染(FCP)和首次可交互时间(TTI)之间的所有时间段的总和
# 累计位移偏移 CLS
累计位移偏移,CLS(Cumulative Layout Shift)旨在衡量可见元素在视口内的移动情况
# DOMContentLoaded
DCL 表示 DOMContentLoaded 事件触发的时间,就是 dom 内容加载完毕,不包括样式表,图片,flash。
# onload
L 表示 onLoad 事件触发的时间。页面上所有的 DOM,样式表,脚本,图片,flash 都已经加载完成了。
# 三大核心指标
Google 提出了网站用户体验的三大核心指标,分别为:
LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但是 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。
CLS 代表了页面的稳定指标,尤其在手机上这个指标更为重要。因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。
# 前端性能优化
# 网络优化
# 1. DNS 预解析
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。
<link rel="dns-prefetch" href="//yuchengkai.cn" />
# 2. 缓存优化
选择合适的缓存策略
浏览器缓存策略分为两种:强缓存和协商缓存
# 3. 使用 http2.0
# 4. CDN 优化
静态资源使用 CDN
CDN 就是,内容分发网络,它是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
# 代码文件优化
# 1. 图片优化
图片懒加载
雪碧图
使用正确的图片格式,小图使用 base64 格式
使用 css3、svg、字体图标代替可替代图片
避免图片使用空 src
# 2. CSS 优化
文件顺序:CSS 文件放在 head 中
减少使用 CSS 表达式
主要 CSS 属性书写顺序,减少重绘回流,css 语义合并
对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
# 3. JS 优化
文件顺序:将 script 标签放在 body 底部
减少 JS 阻塞,利用 async 和 defer
减少 dom 节点的操作,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
避免频繁操作样式,最好一次性重写 style 属性或者将样式列表定义为 class 并一次性更改 class 属性。
避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
事件委托,避免批量绑定事件
使用 requestAnimationFrame 代替 setInterval 实现动画
# 4. 其它
webpack 开启 Gzip 压缩结合服务器端 GZIP 压缩
资源压缩,js 压缩、css 压缩、图片压缩
交互优化:防抖节流
长列表优化:上拉加载,虚拟列表
开启 GPU 渲染加速
# webpack 优化
# 构建优化
- 合并 CSS 和 JS 文件,减少请求数
# 包体积优化
webpack Gzip 压缩
打包压缩,清除无用代码以及 console.log
第三方库和工具的按需加载
选择更优的工具 day.js 代替 moment
# 性能优化
- 小图标整合成雪碧图
- 图片懒加载:页面开始加载时放一张占位图,页面加载完毕,再去请求图片
- 小图标全部换成 svg 图片
- 所有大图,全部优化缩小
- css:减少@import 导入式(同步加载),尽量使用 link 导入(异步加载)
- 减少回流重绘
- 减少 http 请求数量
- 减少资源大小
- 部分依赖包换成 CDN 静态资源(eg: vue, vue-router, vant 等)
- 使用 DNS 预解析
- 开启 gzip 压缩(nginx 配置)
- webpack 配置优化
- 减少对 DOM 的操作(eg: ref)
- 减少重定向的使用
- 缓存:强缓存 & 协商缓存
# 图片过多优化
- 1.将图片服务和应用服务分离(从架构师的角度思考)
部署单独的图片服务器,减少应用服务器 IO 请求
- 2.图片压缩
3.图片懒加载
4.css Sprites css 雪碧图
- 5.将小图片压缩成 base64 格式来节约请求
参考:https://www.cnblogs.com/zytrue/p/8567795.html (opens new window)
# 前端优化工具分析
打包产物的话,可以根据 webpack-bundle-analyze 之类的打包分析工具进行分析
网站的性能概要可以通过 lighthouse 查看
运行时一些关键时间的监控例如 DNS 查询耗时、白屏时间等可以使用 Performance Api 来计算
比较具体的性能卡点通过 Chrome Performance 火焰图分析
# 优化工具 webpack-bundle-analyzer(打包分析工具)分析
- 安装:
npm install webpack-bundle-analyzer -D
依赖(加判断时):
npm install cross-env -D
- 使用
vue.config.js
configureWebpack: (config) => {
if (process.env.use_analyzer) {
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin());
}
};
2
3
4
5
6
7
package.json 添加 analyzer 脚本
"scripts": {
"analyzer": "cross-env use_analyzer=true npm run build:prod",
},
2
3
运行:
npm run analyzer
属性
config.plugins.push(new BundleAnalyzerPlugin({
// 定义属性
analyzerMode:'server', // 以是server,static,json,disabled。在server模式下,分析器将启动HTTP服务器来显示软件包报告。在“静态”模式下,会生成带有报告的单个HTML文件。在disabled模式下,你可以使用这个插件来将generateStatsFile设置为true来生成Webpack Stats JSON文件。
analyzerHost: '127.0.0.1', // 将在“服务器”模式下使用的端口启动HTTP服务器。
analyzerPort: 8888, // 端口号。
reportFilename: 'report.html', // 路径捆绑,将在static模式下生成的报告文件。相对于捆绑输出目录。
defaultSizes: 'parsed',// 默认显示在报告中的模块大小匹配方式。应该是stat,parsed或者gzip中的一个。
openAnalyzer: true,// 在默认浏览器中自动打开报告。
generateStatsFile:false:, // 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成。
statsFilename: 'stats.json', // 相对于捆绑输出目录。
statsOptions: null, // stats.toJson()方法的选项。例如,您可以使用source:false选项排除统计文件中模块的来源。
logLevel: 'info', // 日志级别,可以是info, warn, error, silent。
excludeAssets:null // 用于排除分析一些文件
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lighthouse 评估性能报告
安装: npm install lighthouse -g
使用:lighthouse https://juejin.im/books
# vue 首屏优化
CDN 优化,公共库使用 CDN 资源引入
UI 组件库按需引入
路由懒加载:把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件
使用更轻量级的工具库,例如:moment 改成 dayjs
vue-cli3 关闭预加载,设置完毕后,首屏就只会加载当前页面路由的组件
Vue 项目前端配置 GZIP 压缩
# 前端优化详解
# 长列表优化-虚拟列表
原因:上拉加载就是一般常见的触底加载,滚动到页面底部加载更多数据,但是当数据量上来之后,达到千万数量级别,浏览器没法承载这么多的节点渲染,肯定会卡顿甚至崩溃,这个时候就可以使用虚拟列表,通过计算滚动视窗,每次只渲染可见屏幕部分节点,超出屏幕的不可见范围用内填充 padding 代替,对于浏览器来说无论你滚动到什么位置,渲染的都是屏幕范围内的节点,这样就不会有性能负担了。
概念:虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。
实现:虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
# GZIP 压缩
Gzip 是万能的吗
首先要承认 Gzip 是高效的,压缩后通常能帮我们减少响应 70% 左右的大小。
但它并非万能。Gzip 并不保证针对每一个文件的压缩都会使其变小。
Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大。反之亦然。
webpack 的 Gzip 和服务端的 Gzip
一般来说,Gzip 压缩是服务器的活儿:服务器了解到我们这边有一个 Gzip 压缩的需求,它会启动自己的 CPU 去为我们完成这个任务。而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们以服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。
既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU 性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。Webpack 中 Gzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。
因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该结合业务压力的实际强度情况,去做好这其中的权衡。
参考:性能优化作战手册 (opens new window)
# 项目优化(已用到)
# 网络优化
- CDN 优化
(1) 三方库使用 CDN 上
(2) 简单动静分离:静态资源(JavaScript,CSS,img 等文件)部署至 CDN 上,通过 jenkins 流水线来上传
# 代码文件优化
- 用 dayjs 替换 moment
moment 大小 200 多 k,dayjs 只有 2KB
webpack-bundle-analyzer 分析 GZIP 从 74.66KB 到 2.73KB
# webpack 性能优化
- 压缩
使用 terser-webpack-plugin 进行 js 的压缩
使用 css-minimizer-webpack-plugin 进行 CSS 的压缩,CSS 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等
使用 compression-webpack-plugin 开启 gzip 进行文件压缩
使用 image-webpack-loader 进行图片的压缩
通过 vuecli 内置的 terser-webpack-plugin 去除 console
代码分离,使用内置的 SplitChunksPlugin 进行代码分离,将代码分离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件
第三方库和工具的按需加载
# webpack 打包(构建)优化
主要可以从优化搜索时间、缩小文件搜索范围、减少不必要的编译等方面入手
优化 loader 配置,在使用 loader 时,可以通过配置准确目录及文件后缀减少 loader 搜索时间
使用 cache-loader,在一些性能开销较大的 loader 之前添加 cache-loader,以将结果缓存到磁盘里,显著提升二次构建速度
terser 启动多线程,使用多进程并行运行来提高构建速度
合理使用 sourceMap
打包生成 sourceMap 的时候,如果信息越详细,打包速度就会越慢