5.3-性能指标、优化
Create by fall on 22 Nov 2020 Recently revised in 28 Feb 2025
性能
性能就是页面的表现:
包括响应(Response)动画(Animation)空白时间(Idle)
- FID:first input delay(首次输入延迟)
- FCP:首屏渲染时间,一般需要在一秒内
- LCP:Largest ContentFul Paint (最大内容绘制)
- TTI:Time to Interreactive(可以使用页面中的内容的时间)
- INP:Interaction to Next Paint(从输入到下一个画面的时间)
一次脚本占用的时间超过 50 ms 就表示存在堵塞,一段时间内,超过 50ms 超过的部分,累加起来称之为堵塞时间。 如果一次的执行时间超过 100ms 就表示存在用户可感知的卡顿。
性能评估工具
- 谷歌浏览器 Devtools(F12)中的 NetWork
- 使用 Lighthouse 来对应用性能进行评估
- Devtools 中的 Peformance
- webPageTest 模拟不同场景下访问的情况(浏览器,国家)
- webpack-bundle-analyzer 打包分析工具
JS 中的钩子
DOMContentLoaded
DOM 渲染完成Load
当前页面所有资源加载完成
网络性能
Queueing
浏览器将资源放入队列时间Stalled
因放入队列时间而发生的停滞时间DNS Lookup
DNS 解析时间Initial connection
建立 HTTP 连接的时间SSL
浏览器与服务器建立安全性连接的时间TTFB
等待服务端返回数据的时间Content Download
浏览器下载资源的时间
性能指标
性能监控指标
字段 | 描述 | 备注 | 解释 |
---|---|---|---|
FP | First Paint(首次绘制) | 包括了任何用户自定义的背景绘制,它是首先将像素绘制到屏幕的时刻 | 表示浏览器从开始请求网站到屏幕渲染第一个像素点的时间 |
FCP | First Content Paint(首次内容绘制) | 是浏览器将第一个 DOM 渲染到屏幕的时间,可能是文本、图像、SVG 等,这其实就是白屏时间 | 表示浏览器渲染出第一个内容的时间,这个内容可以是文本、图片或 SVG 元素等等,不包括 iframe 和白色背景的canvas 元素 |
FMP | First Meaningful Paint(首次有意义绘制) | 页面有意义的内容渲染的时间 | |
LCP | Largest Contentful Paint(最大内容渲染) | 代表在 viewport 中最大的页面元素加载的时间 | 标记了渲染出最大文本或图片的时间 |
DCL | DomContentLoaded(DOM加载完成) | 当 HTML 文档被完全加载和解析完成之后, DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 | 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载 |
L | onLoad | 当依赖的资源全部加载完毕之后才会触发 | 检测一个完全加载的页面,页面的 html、css、js、图片等资源都已经加载完之后才会触发 load 事件 |
TTI | Time to Interactive 可交互时间 | 用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点 | 页面从开始加载到主要子资源完成渲染,并能够快速、可靠的响应用户输入所需的时间 |
FID | First Input Delay(首次输入延迟) | 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 | 测量加载响应度的一个以用户为中心的重要指标 |
TBT | Total Blocking Time 总阻塞时间 | 测量 FCP 与 TTI 之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应 | |
SI | Speed Index 速度指数 | 表明了网页内容的可见填充速度 | |
CLS | Cumulative Layout Shift 累积布局偏移 | 测量的是整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数 |
PC 端
在前端开发中,此规则作为一种开发指导思路,针对浏览器页面的性能优化。
- 用户在 2 秒内得到响应,会感觉页面的响应速度很快 Fast
- 用户在 2~5 秒间得到响应,会感觉页面的响应速度还行 Medium
- 用户在 5~8 秒间得到响应,会感觉页面的响应速度很慢,但还可以接受 Slow
- 用户在 8 秒后仍然无法得到响应,会感觉页面的响应速度垃圾死了
移动端
- 单个文件小于 25kb
- 打包内容为分段 multipart 文档,Pack Components Into A Multipart Document
性能优化
- 加载优化、执行优化、渲染优化、样式优化、脚本优化
- 加载优化:减少 HTTP 请求、缓存资源、压缩代码、无阻塞、首屏加载、按需加载、预加载、压缩图像、减少Cookie、避免重定向、异步加载第三方资源
- 执行优化:CSS 写在头部,JS 写在尾部并异步、避免 img、iframe 等的 src 为空、尽量避免重置图像大小、图像尽量避免使用 DataURL
- 渲染优化:设置 viewport、减少 DOM 节点、优化动画、优化高频事件、GPU 加速
- 样式优化:避免在 HTML 中书写 style、避免 CSS 表达式、移除 CSS 空规则、正确使用 display、不滥用 float 等
- 脚本优化:减少重绘和回流、缓存 DOM 选择与计算、缓存
.length
的值、尽量使用事件代理、尽量使用 id 选择器、touch 事件优化,对无用数据进行释放
优化策略
JavaScript
- 把脚本放在页面底部
- 压缩 JavaScript 和 CSS
- 减少 DOM 操作
- 使用高效的事件处理
- 避免短时间内多次重复请求
CSS
- 把样式表放在
<head></head>
标签中 - 不要使用 CSS 表达式
- 使用
<link>
替代@import
- 优化 CSS sprite
- 不要使用
filter
页面内容
- 减少 HTTP 请求数
- 减少 DNS 查询
- 避免重定向
- 缓存 Ajax 请求
- 延迟加载
- 预加载
- 减少 DOM 元素数量
- 根据内容划分到不同的域名
- 减少
iframe
使用 - 尽量减少 404
服务器
- 使用 CDN
- 添加 Expires 或者 Cache-Control 响应头
- 启用 Gzip
- 配置 Etag
- Ajax 请求使用 get 方法
- 避免图片 src 为空
Cookie
- 减小 Cookie 大小(减小传输带宽)
- 静态资源使用无 Cookie 域名
加载优化
-
减少 HTTP 请求:尽量减少页面的请求数(首次加载同时请求数不能超过 4 个),移动设备浏览器同时响应请求为 4 个请求(Android 支持 4 个,iOS5+ 支持 6 个)
- 合并 CSS 和 JS
- 使用 CSS 精灵图
-
缓存资源:使用缓存可减少向服务器的请求数,节省加载时间,所有静态资源都要在服务器端设置缓存,并且尽量使用长缓存(使用时间戳更新缓存)
- 缓存一切可缓存的资源
- 使用长缓存
- 使用外联的样式和脚本
-
压缩代码:减少资源大小可加快网页显示速度,对代码进行压缩,并在服务器端设置 GZip
- 压缩代码(多余的缩进、空格和换行符)
- 启用 Gzip
-
无阻塞:头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载
-
首屏加载:首屏快速显示可大大提升用户对页面速度的感知,应尽量针对首屏的快速显示做优化
-
按需加载:将不影响首屏的资源和当前屏幕不用的资源放到用户需要时才加载,可大大提升显示速度和降低总体流量(按需加载会导致大量重绘,影响渲染性能)
- 懒加载
- 滚屏加载
- Media Query 加载
-
预加载:大型资源页面可使用 Loading,资源加载完成后再显示页面,但加载时间过长,会造成用户流失
- 可感知 Loading:进入页面时 Loading
- 不可感知 Loading:提前加载下一页
-
压缩图像:使用图像时选择最合适的格式和大小,然后使用工具压缩,同时在代码中用srcset来按需显示(过度压缩图像大小影响图像显示效果)
-
减少 Cookie:Cookie 会影响加载速度,静态资源域名不使用 Cookie
-
避免重定向:重定向会影响加载速度,在服务器正确设置避免重定向
-
异步加载第三方资源:第三方资源不可控会影响页面的加载和显示,要异步加载第三方资源
-
加载阶段:减少关键资源个数,降低关键资源大小,降低关键资源的 RTT 次数;
-
交互阶段:减少 JavaScript 脚本执行时间,避免强制同步布局:操作 DOM 的同时获取布局样式会引发,避免布局抖动:多次执行强制布局和抖动,合理利用 CSS 合成动画:标记 will-change,避免频繁的垃圾回收;
-
CSS 实现一些变形、渐变、动画等特效,这是由 CSS 触发的,并且是在合成线程中执行,这个过程称为合成,它不会触发重排或者重绘;
-
当有数据更新时, React 会生产一个新的虚拟 DOM,然会拿新的虚拟 DOM 和之前的虚拟 DOM 进行比较,这个过程找出变化的节点,然后将变化的节点应用到 DOM 上;
-
最开始的时候,比较两个 DOM 的过程是在一个递归函数里执行的,其核心算法是 reconciliation。通常情况,这个比较过程执行很快,不过虚拟 DOM 比较复杂时,执行比较函数可能占据主线程比较久的时间,这样会导致其他任务的等待,造成页面卡顿。React 团队重写了 reconciliation 算法,称为 Fiber reconciler,之前老的算法称为 Stack reconciler;
-
Use A Content Delivery Network:使用 CDN
-
Add An Expires Or A Cache-Control Header:响应头添加 Expires 或 Cache-Control
-
Gzip Components:Gzip 资源
-
Configure ETags:配置 ETags
-
Flush The Buffer Early:尽早输出缓冲
-
Use Get For AJAX Requests:AJAX 请求时使用 get
-
Avoid Empty Image Src:避免图片空链接
-
Make Fewer HTTP Requests:减少 HTTP 请求数
-
Reduce DNS Lookups:减少 DNS 查询
-
Avoid Redirects:避免重定向
-
Make Ajax Cacheable:缓存 AJAX 请求
-
Postload Components:延迟加载资源
-
Preload Components:预加载资源
-
Reduce The Number Of DOM Elements:减少 DOM 元素数量
-
Split Components Across Domains:跨域拆分资源
-
Minimize The Number Of Iframes:减少 iframe 数量
-
No 404s:消除 404 错误
-
Optimize Images:优化图片
-
Optimize CSS Sprites:优化 CSS 精灵图
-
Make Favicon.ico Small And Cacheable:使用小体积可缓存的 favicon
-
Reduce Cookie Size:减少 Cookie 大小
-
Use Cookie-Free Domains For Components:使用无 Cookie 域名的资源
执行优化
- CSS 写在头部,JS 写在尾部并异步
- 避免 img、iframe 等的 src 为空:空 src 会重新加载当前页面,影响速度和效率
- 尽量避免重置图像大小:多次重置图像大小会引发图像的多次重绘,影响性能
- 图像尽量避免使用DataURL:DataURL 图像没有使用图像的压缩算法,文件会变大,并且要解码后再渲染,加载慢耗时长。
渲染优化
- 设置 viewport:HTML 的 viewport 可加速页面的渲染
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
-
减少 DOM 节点:DOM 节点太多影响页面的渲染,尽量减少 DOM 节点
-
优化动画
- 尽量使用 CSS3 动画
- 合理使用 requestAnimationFrame 动画代替 setTimeout
- 适当使用 Canvas 动画:5 个元素以内使用 CSS 动画,5 个元素以上使用 Canvas 动画,iOS8+ 可使用 WebGL 动画
-
优化高频事件:scroll、touchmove 等事件可导致多次渲染
- 函数节流
- 函数防抖
- 使用 requestAnimationFrame 监听帧变化:使得在正确的时间进行渲染
- 增加响应变化的时间间隔:减少重绘次数
-
GPU 加速:使用某些 HTML5 标签和 CSS3 属性会触发 GPU 渲染,请合理使用(过渡使用会引发手机耗电量增加)
- HTML标签:video、canvas、webgl
- CSS属性:opacity、transform、transition
样式优化
- Put Stylesheets At The Top:置顶样式
- Avoid CSS Expressions:避免 CSS 表达式
- Choose Over @import:选择 `` 代替 @import
- Avoid Filters:避免滤镜
- 避免行内样式(在 HTML 中书写 style)
- 避免CSS表达式:CSS 表达式的执行需跳出CSS树的渲染
- 移除CSS空规则:CSS 空规则增加了 css 文件的大小,影响 CSS 树的执行
- 正确使用 display(影响页面的渲染)
- display:inline 后不应该再使用 float、margin、padding、width 和 height
- display:inline-block 后不应该再使用 float
- display:block 后不应该再使用 vertical-align
- display:table-* 后不应该再使用 float 和 margin
- 不滥用 float:float 在渲染时计算量比较大,尽量减少使用
- 不滥用 Web 字体:Web 字体需要下载、解析、重绘当前页面,尽量减少使用
- 不声明过多的 font-size:过多的 font-size 影响 CSS 树的效率
- 值为 0 时不需要任何单位:为了浏览器的兼容性和性能,值为 0 时不要带单位
- 标准化各种浏览器前缀
- 无前缀属性应放在最后
- CSS 动画属性只用 -webkit-、无前缀两种
- 其它前缀为 -webkit-、-moz-、-ms-
- 避免让选择符看起来像正则表达式:高级选择符执行耗时长且不易读懂,避免使用
脚本优化
-
Put Scripts At The Bottom:置底脚本
-
Make JavaScript And CSS External:使用外部 JS 和 CSS
-
Minify JavaScript And CSS:压缩 JS 和 CSS
-
Remove Duplicate Scripts:删除重复脚本
-
Minimize DOM Access:减少 DOM 操作
-
Develop Smart Event Handlers:开发高效的事件处理
-
减少重绘和回流
- 避免不必要的 DOM 操作
- 避免使用 document.write
- 减少 drawImage
- 尽量改变 class 而不是 style,使用 classList 代替 className
-
缓存DOM选择与计算:每次 DOM 选择都要计算和缓存
-
缓存 .length 的值:每次 .length 计算用一个变量保存值
-
尽量使用事件代理:避免批量绑定事件
-
尽量使用 id 选择器:id 选择器选择元素是最快的
-
touch事件优化:使用 tap(touchstart 和 touchend)代替 click(注意 touch 响应过快,易引发误操作)
优化方案
打包
除去空格,换行,注释,tree-shaking
的优化。并且做一些代码混淆,提高安全性。
- UglifyJS
- webpack-parallel-uglify-plugin
- terser:代码压缩
css 压缩主要是 mini-css-extract-plugin
html 压缩可以用 HtmlWebpackPlugin
DllPlugin 提升构建速度
DllPlugin 插件,将一些比较大的,基本很少升级的包拆分出来,生成 xx.dll.js
通过 mainfest.json
引用
// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
react: ["react", "react-dom"],
},
output: {
filename: "[name].dll.js",
path: path.resolve(__dirname, "dll"),
library: "[name]"
},
plugins: [
new webpack.DllPlugin({
name: "[name]",
path: path.resolve(__dirname, "dll/[name].manifest.json")
})
]
};
// package.json
"scripts": {
"dll-build": "NODE_ENV=production webpack --config webpack.dll.config.js",
},
骨架屏
为元素提供骨架(插件 react-placeholder
)
窗口化
只加载当前窗口能显示的 DOM 元素,视图变化时,删除多余的 DOM,添加要显示的 DOM 保证 DOM 元素数量不会影响性能
安装:npm i react-window
使用:
import {FixedSizeList as List} from 'react-window'
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
)
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
缓存
keep-alive:允许消息发送者暗示连接的状态,还可以用来设置超时时长和最大请求数。
强制缓存、协商缓存的使用
Service Worker
借助webpack插件 WorkboxWebpackPlugin
和 ManifestPlugin
,加载 serviceWorker.js,通过 serviceWorker.register()
注册
实现预加载
预加载
预加载允许浏览器在空闲时间下载或预取资源(例如图像、脚本、样式表等)以便在用户需要它们时可以立即使用,从而减少页面加载时间和提高用户体验。
- 预加载 (
preload
):preload
是一种声明式的获取请求,告诉浏览器尽快下载特定资源,因为它是当前页面立即需要的关键资源。它具有最高的优先级,应该用于对页面初始渲染至关重要的资源。例如,用于 above-the-fold 内容的图像、字体或关键脚本。 - 预获取 (
prefetch
):prefetch
是一种提示,告诉浏览器用户可能在将来需要某个资源(例如,在下一个页面上)。浏览器会在空闲时间下载这些资源,优先级较低。这通常用于导航到其他页面的链接,或预测用户行为。 - 预连接 (
preconnect
):preconnect
允许浏览器在用户请求资源之前建立与服务器的早期连接(包括 DNS 查找、TCP 握手和 TLS 协商)。这可以减少连接建立的延迟,尤其是在 HTTPS 连接上。
<link rel="preload">
<link rel="prefetch">
<link rel="preconnect">
<link rel="preload" href="styles.css" as="style">
<link rel="preload" href="image.jpg" as="image">
<link rel="preload" href="script.js" as="script">
<link rel="preconnect" href="https://example.com">
<link rel="prefetch" href="next-page.html" as="document">
预加载的优势:
- 减少页面加载时间: 通过预先加载关键资源,浏览器可以在需要时立即使用它们,从而减少渲染阻塞并加快页面加载速度。
- 改进用户体验: 更快的加载时间可以提高用户满意度和参与度。
- 优先级控制:
preload
允许开发人员指定资源的优先级,确保关键资源首先加载。 - 灵活性和控制:
preload
支持多种资源类型,并允许开发人员指定as
属性以指示资源类型,从而优化加载过程。
预加载的最佳实践:
- 仅预加载关键资源: 不要预加载太多资源,因为这可能会占用带宽并降低其他资源的加载速度。专注于对页面初始渲染至关重要的资源。
- 使用
as
属性: 始终使用as
属性指定资源类型,以便浏览器可以正确地优先处理和缓存资源。 - 设置正确的
type
属性 (可选): 配合as
使用type
属性可以启用提前 MIME 类型嗅探,进一步优化加载。 - 避免重复加载: 确保预加载的资源不会被浏览器再次请求。
- 监控和优化: 使用浏览器开发工具监控预加载的效果,并根据需要进行调整。
对于 js 来说,更常用 modulepreload
<link rel="modulepreload">
modulepreload 指出模组应该被拉取解析,并且编译,存储并用于之后的执行。
该语句表明,将会在之后用到该模块,需要提前加载该模块,并且解析编译(不执行)不同于 preload,将只处理 fetching 部分
当处理依赖时,会更好用,添加上 modulepreload 后,会自动拉取整个代码块中的依赖树,当你的主模块引入了其它模块,将会拉取并将其它模块一起准备好
modulepreload 擅长大量的依赖或者基于路由的代码分割,对于不常用的路由,通常的动态加载可能会更有效率
懒加载
滚动到最下面的时候加载
需要的时候再进行加载
Web API
监听窗口的激活状态
// document.visibilityState
let listenEvent = 'visibilitychange'
function visibilityChange(){
if(document.hidden){
document.title = '你离开了当前窗口'
}else{
document.title = '你回到了这个窗口'
}
}
document.addEventListener(listenEvent, visibilityChange);
观察长任务
PerformancceObserver 对象
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry)
}
})
observer.observe({entryTypes: ['longtask']})
监听网络变化
监听网络变化,在视频网站上,如果遇到网络卡顿会告知给用户
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
var type = connection.effectiveType;
function updateConnectionStatus() {
console.log("Connection type changed from " + type + " to " + connection.effectiveType);
type = connection.effectiveType;
}
connection.addEventListener('change', updateConnectionStatus);
DOM 渲染完成的 API
DOMContentLoaded
window.addEventListener('DOMContentLoaded', (event) => {
let timing = performance.getEntriesByType('navigation')[0];
console.log(timing.domInteractive);
console.log(timing.fetchStart);
let diff = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + diff);
})
Node 性能监控
-
日志监控:可以通过监控异常日志的变动,将新增的异常类型和数量反映出来 监控日志可以实现 pv 和 uv 的监控,通过 pv/uv 的监控,可以知道使用者们的使用习惯,预知访问高峰
-
响应时间 响应时间也是一个需要监控的点。一旦系统的某个子系统出现异常或者性能瓶颈将会导致系统的响应时间变长。响应时间可以在 nginx 一类的反向代理上监控,也可以通过应用自己产生访问日志来监控
-
进程监控 监控日志和响应时间都能较好地监控到系统的状态,但是它们的前提是系统是运行状态的,所以监控进程是比前两者更为紧要的任务。监控进程一般是检查操作系统中运行的应用进程数,比如对于采用多进程架构的web应用,就需要检查工作进程的数,如果低于低估值,就应当发出警报
-
磁盘监控 磁盘监控主要是监控磁盘的用量。由于写日志频繁的缘故,磁盘空间渐渐被用光。一旦磁盘不够用将会引发系统的各种问题,给磁盘的使用量设置一个上限,一旦磁盘用量超过警戒值,服务器的管理者应该整理日志或者清理磁盘
-
内存监控 对于 node 而言,一旦出现内存泄漏,不是那么容易排查的。监控服务器的内存使用情况。如果内存只升不降,那么铁定存在内存泄漏问题。符合正常的内存使用应该是有升有降,在访问量大的时候上升,在访问量回落的时候,占用量也随之回落。监控内存异常时间也是防止系统出现异常的好方法。如果突然出现内存异常,也能够追踪到近期的哪些代码改动导致的问题
-
cpu占用监控 服务器的 cpu 占用监控也是必不可少的项,cpu的使用分为用户态、内核态、IOWait 等。如果用户态cpu使用率较高,说明服务器上的应用需要大量的cpu开销;如果内核态cpu使用率较高,说明服务器需要花费大量时间进行进程调度或者系统调用;IOWait 使用率反映的是cpu等待磁盘I/O操作;cpu的使用率中,用户态小于70%,内核态小于35%且整体小于70%,处于正常范围。监控cpu占用情况,可以帮助分析应用程序在实际业务中的状况。合理设置监控阈值能够很好地预警
-
cpu load监控 cpu load又称cpu平均负载。它用来描述操作系统当前的繁忙程度,又简单地理解为cpu在单位时间内正在使用和等待使用cpu的平均任务数。它有3个指标,即1分钟的平均负载、5分钟的平均负载,15分钟的平均负载。cpu load过高说明进程数量过多,这在node中可能体现在用于进程模块反复启动新的进程。监控该值可以防止意外发生
-
I/O 负载 I/O 负载指的主要是磁盘 I/O。反应的是磁盘上的读写情况,对于 node 编写的应用,主要是面向网络业务,是不太可能出现 I/O 负载过高的情况,大多数的 I/O 压力来自于数据库。不管 node 进程是否与数据库或其他 I/O 密集的应用共同处理相同的服务器,我们都应该监控该值防止意外情况
-
网络监控:虽然网络流量监控的优先级没有前端要求那么高,但还是需要对流量进行监控并设置流量上限值。即便应用突然受到用户的青睐,流量暴涨的时候也可以通过数值感知到网站的宣传是否有效。一旦流量超过警戒值,开发者就应当找出流量增长的原因。对于正常增长,应当评估是否该增加硬件设备来为更多用户提供服务。网络流量监控的两个主要指标是流入流量和流出流量
-
应用状态监控 除了这些硬性需要检测的指标之外,应用还应该提供一种机制来反馈其自身的状态信息,外部监控将会持续性地调用应用地反馈接口来检查它地健康状态。
-
DNS 监控 dns 是网络应用的基础,在实际的对外服务产品中,多数都对域名有依赖。dns 故障导致产品出现大面积影响的事件并不少见。由于 dns 服务通常是稳定的,容易让人忽略,但是一旦出现故障,就可能是史无前例的故障。对于产品的稳定性,域名 dns 状态也需要加入监控。
-