TTFB(Time To First Byte)首字节时间, FP(First Paint)白屏时间, FCP(First Content Paint)首内容时间, LCP(Largest Content Paint)最大内容渲染时间
TTFB指的是从你的浏览器发起请求到服务端响应第一字节的时间,FP则指的是从浏览器开始解析html到首次渲染第一个像素点(背景什么)的时间,FCP指的是第一个实际内容渲染出来的时间(文本,img, video), LCP则是你的最大的内容渲染出来的时间, 这些时间都是从导航开始计算(从用户点击链接的那一刻开始,后面DNS查询,TCP握手,HTTP请求...),也就是从浏览器发起请求开始计算,所以,前面任何一个指标慢了都会影响后面的指标,比如浏览器所处的网速太慢了,TTFB特慢的话,就会直接拖累后面的FP, FCP, LCP的计算
什么场景下会影响这些指标?
-
TTFB: TTFB过高,我们可以知道是网络波动,服务器响应这些影响了这个指标
-
FP它是表示第一个像素渲染的时候,发生在body的背景渲染时,也可能是之前之后背景文字图片之类的首次渲染时,渲染body的时候,要不渲染默认白色背景要不就是用户自定义的背景色,这些都算开始了第一个像素点的渲染,所以FP过长,而TTFB正常的话,考虑是在head放了太多强阻塞性的资源或则是js执行时间过长了,可以通过async, defer, prefetch, preload这些解决
-
FCP表示在body阻塞了内容的渲染,FCP阶段的问包含了阻塞问题,这个阶段js代码开始执行了,也可能是代码质量影响了这个指标, 当然,像我们前面说的,前面几个阶段有问题也会影响后面的阶段评估,所以前面讨论的只限于FP正常的范畴
-
LCP只看大内容(文本,图片,视频),在ai平台的场景下,往往会使用流式输出实现打字机的这么一个效果,这个时候文本就是最可能影响LCP指标的,如果不用流式输出,一下子等待输出完整的文本块的话LCP指标肯定无法达标的,所以会利用li,div一些容器分隔文本块搞打字机,这样LCP就下去了,对于图片来说加载时间过长的话就会影响到这个指标, 很明显的,我们在图片中使用loading="lazy"而且图片原本就位于视口的话,这样图片的下载从解析标签的时候到浏览器时机布局完成后确认图片在视口内后才发起请求,这就无端引入了图片下载的延迟时间,LCP多了一段下载图片的时间,请看下面的代码
xml<!-- LCP 候选图片:位于视口内,但被 lazy 延迟 --> <img class="lcp-img" src="https://ts1.tc.mm.bing.net/th/id/OIP-C.vcU2WKSmEq4JRVTuldmaEQHaEK?w=193&h=135&c=8&rs=1&qlt=90&o=6&dpr=1.5&pid=3.1&rm=2" loading="lazy" alt="LCP 大图" style="width:800px; height:auto;"> <!-- 大量复杂DOM,增加布局计算成本 --> <div class="heavy-layout" id="heavy"> <!-- 动态生成5000个嵌套元素 --> </div> <script> // 1. 生成大量嵌套元素,增加布局复杂度 const container = document.getElementById('heavy'); let html = ''; for (let i = 0; i < 50000; i++) { // 创建深度嵌套的 div,每个都有 padding/border html += `<div class="deep-nested" style="padding-left: ${i % 10}px;">Item ${i}`; for (let j = 0; j < 5; j++) { html += `<span style="display:inline-block; width:20px;">·</span>`; } html += `</div>`; } container.innerHTML = html; // 2. 人为制造布局抖动 (layout thrashing) // 频繁读取 offsetHeight 再修改样式,强制浏览器反复计算布局 function jankLayout() { const elements = document.querySelectorAll('.deep-nested'); for (let i = 0; i < Math.min(elements.length, 200); i++) { const el = elements[i]; // 读取布局属性(强制同步布局) const h = el.offsetHeight; // 修改样式,使布局失效 el.style.backgroundColor = `hsl(${i % 360}, 70%, 80%)`; // 再次读取,触发重算 const h2 = el.offsetHeight; } requestAnimationFrame(() => setTimeout(jankLayout, 10)); } // 开始布局抖动,持续约 2 秒,严重阻塞主线程 setTimeout(() => { jankLayout(); }, 50); </script>我们打开浏览器性能分析板块,查看LCP4.59s,当我们去掉v-loading的时候,LCP为2.37s, 提升非常多,这里除了图片下载,还引入了img大小突变导致的重排,由于LCP计算依赖布局完成后元素的可见面积,于是LCP多了重排带来的额外开销,我们可以通过预设固定宽高比,防止容器突变带来的重排,当我们将height设置为400px后,使用loading时LCP为3.07s,当我们去掉的时候LCP为2.40s。差了。0.5s多, 这个差值取决于你图片的下载时间以及下载的图片是否会造成重排
其它性能指标
CLS(Cumulative Layout Shift)累计布局偏移
CLS就是计算每个元素发生的起始位置变化程度, 它是有一个计算公式的 score = impact fraction(影响分数) * distance fraction(距离分数), 很常见的,重排就会影响CLS分数, 但是这个也分情况,如果是用户交互后500ms左右的容器按照预期偏移,浏览器是不会算的,包括做一些动画transform,用户滚动scroll,但是非用户介入的,图片没有设置宽高,加载后挤下去了其它容器,这种是会被计入CLS的就像上面我提供的代码中height:auto的例子,在height:auto的时候,CLS为0.31,在给了固定高度后,CLS为0, 页面初始布局不算在里面的,这个时候都没有初始位置,没法计算布局,在SPA页面中,用户点击产生了路由状态的更迭,如果组件渲染耗时超出了500ms,则后续渲染也会被计入CLS, 这500ms称为一个渲染豁免区, 我利用AI完成了一个CLS测算页面,当超出500ms的豁免区时,就会被计入CLS

网页链接为CLS 500ms 豁免机制演示
INP(Interaction to Next Paint)
INP指标表示的是当用户和页面交互后到浏览器视觉上的绘制更新变化的延迟时间,这里面有三个容器拖累指标,第一个是如果用户点击的时候,主进程被占用了,用户的请求就需要排队,第二个是js处理当前的时间回调发生的时间,也就是js执行时间,第三个是浏览器重排重绘的时间, 可是说是js执行和浏览器重绘重排带来的耗时, 在js方面可以利用setTimeout把长任务拆分出片,分片执行,必要可以上Web Worker, 浏览器重排上尽量避免重排,避免写了又读导致重复不断的重排重绘,批量读取,批量操作样式
xml
<style>
.active {
transform: scale(1.5);
}
</style>
</head>
<body>
<button data-action="click">点击</button>
<script>
const btn = document.querySelector("button[data-action='click']");
btn.addEventListener("click", () => {
// let i = 0;
// while (i < 10000) {
// i++
// document.body.append("str");
// };
tag = true;
let count = 0;
while (tag) {
count++;
if (count > 10000000000) tag = false;
}
btn.classList.add("active");
});
</script>

可以看到这里的INP延迟相当大,当去掉这段耗时的while后,INP为3.90ms
TTI(Time To Interactive)可交互时间
TTI这个指标是指的从FCP完成开始,找一段连续5s的时间窗口,这个时间端,没有出现长任务(规定为执行时间>=50ms的任务), 并且网络请求数量不超过2个
TBT(Total Block Time)总阻塞时间
计算所有任务时长超过50ms的长任务的执行时间总和
PerformanceObserver API
在上面CLS计算项目的代码中,可以看到我们计算CLS的时候使用了PerformanceObserver这个API,这个API呢是浏览器专门为了用户获取相关性能指标而提供的, 这个api使用方式如下
xml
<body>
<div>Hello, world!</div>
<script>
const observer = new PerformanceObserver(function (list, obj) {
const entries = list.getEntries();
console.log(JSON.stringify(entries));
});
observer.observe({ type: 'paint', buffered: true })
</script>
</body>
这么使用的,list是一个性能事件对象,允许你使用entries获取所有显示观察到的PerformanceEntry对象,PerformanceEntry这个对象呢,就是封装了单一的性能指标数据, 这里observer提供entryTypes和type和buffered这几个参数,type就是指定你要观测的性能条目,entryType要你传递一组性能条目,表示要追踪多个性能指标,这里是互斥的,给了type就不能给entryType了,buffered指示是否要将之前历史记录中缓存的历史性能条目也放入list中,这个buffered非常有必要给true,因为实际js执行都比较晚,如果不刻意放在head标签的话,它是要晚于LCP的,这个时候不使用buffered记录js执行之前的缓存性能条目,就没有办法得到LCP了,这里传递的paint是一个类目,表示页面渲染的关键时间点,是浏览器自动记录的