图片来自 bing.com
背景
想实现一个 Tampermonkey 插件,当发现某个接口报错则使用替代接口。比如某个 AI 网站接口挂了,我们想替换成 kimi 或 deepseek 的接口,但是我们没法修改这个网站的源码。
或者我们出于其他目的比如前端监控,看到开发者工具 Network 面板内接口报错 404 或 500,如何监听这类错误呢?
如:GET https://example.com/api/completion?query=3 500 (Internal Server Error)
如何实现?
PerformanceObserver to the Rescue 💉
我们可以使用 PerformanceObserver 这个观察器,过滤出 initiatorType 是 fetch 且状态码是我们想要监控的即可。
代码如下,大家可在 F12 或控制台执行,然后随意发出一个报错的 fetch 请求。
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log('entry', entry)
// @ts-expect-error `entry` 类型必定为 `PerformanceResourceTiming` https://github.com/microsoft/TypeScript/issues/58644
if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
if (entry.responseStatus >= 400) {
console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
}
}
});
});
observer.observe({ type: 'resource', buffered: true });
代码说明
- 一般异常码都是 故
entry.responseStatus >= 400 - 注意:
entry.initiatorType会报 TS 类型错误,因为 entry 的类型被认为是PerformanceEntry没有该字段,原因是 TS 无法做到如此精确的推导问题 github.com/microsoft/T… ,但实际上我们监听的是type为resource那么entry类型必定为PerformanceResourceTiming则自然有initiatorType,故这里使用@ts-expect-error忽略即可。
classDiagram
class PerformanceEntry {
+String name
+String entryType
+DOMHighResTimeStamp startTime
+DOMHighResTimeStamp duration
+toJSON()
}
class PerformanceResourceTiming {
+String initiatorType
+DOMHighResTimeStamp fetchStart
+DOMHighResTimeStamp domainLookupStart
+DOMHighResTimeStamp domainLookupEnd
+DOMHighResTimeStamp connectStart
+DOMHighResTimeStamp connectEnd
+DOMHighResTimeStamp secureConnectionStart
+DOMHighResTimeStamp requestStart
+DOMHighResTimeStamp responseStart
+DOMHighResTimeStamp responseEnd
+Number transferSize
+Number encodedBodySize
+Number decodedBodySize
+String nextHopProtocol
+String serverTiming
+Number workerStart
+Number redirectStart
+Number redirectEnd
}
PerformanceEntry <|-- PerformanceResourceTiming
note for PerformanceResourceTiming "resource timing API\n监控网络资源加载性能"
note for PerformanceEntry "基础性能条目接口\n所有性能监控的基类"
使用示例
PerformanceResourceTiming 继承自 PerformanceEntry
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
// entry 是 PerformanceResourceTiming 实例
// 既有父类 PerformanceEntry 的属性
console.log('名称:', entry.name);
console.log('类型:', entry.entryType); // 固定为 'resource'
// 也有子类特有的属性
console.log('发起类型:', entry.initiatorType); // script/img/css等
console.log('DNS查询时间:', entry.domainLookupEnd - entry.domainLookupStart);
console.log('TCP连接时间:', entry.connectEnd - entry.connectStart);
console.log('响应时间:', entry.responseEnd - entry.responseStart);
});
});
封装
我们封装下“检测网络请求异常”的函数。
首先我们封装一个对任何 resource 的监听器。
type IProps = {
when: (entry: PerformanceResourceTiming) => boolean;
callback: (entry: PerformanceResourceTiming) => unknown;
buffered?: boolean;
}
function observeNetwork({ when, callback, buffered = true }: IProps) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((perfEntry) => {
const entry = perfEntry as PerformanceResourceTiming;
if (when(entry)) {
callback(entry)
}
});
});
// `type resource` 可以监听这些类型的资源:
// `"link","script","img","xmlhttprequest","fetch","beacon","css"`
observer.observe({ type: 'resource', buffered });
return () => {
observer.disconnect()
}
}
classDiagram
class PerformanceEntry {
<<abstract base class>>
+String name
+String entryType
+DOMHighResTimeStamp startTime
+DOMHighResTimeStamp duration
+toJSON() Object
}
class PerformanceResourceTiming {
<<重点:网络资源性能监控>>
+String initiatorType
+DOMHighResTimeStamp fetchStart
+DOMHighResTimeStamp domainLookupStart
+DOMHighResTimeStamp domainLookupEnd
+DOMHighResTimeStamp connectStart
+DOMHighResTimeStamp connectEnd
+DOMHighResTimeStamp secureConnectionStart
+DOMHighResTimeStamp requestStart
+DOMHighResTimeStamp responseStart
+DOMHighResTimeStamp responseEnd
+Number transferSize
+Number encodedBodySize
+Number decodedBodySize
+String nextHopProtocol
+PerformanceServerTiming[] serverTiming
+DOMHighResTimeStamp workerStart
+DOMHighResTimeStamp redirectStart
+DOMHighResTimeStamp redirectEnd
+DOMHighResTimeStamp workerStart
+DOMHighResTimeStamp redirectStart
+DOMHighResTimeStamp redirectEnd
+deliveryType() String
+renderBlockingStatus() String
}
PerformanceEntry <|-- PerformanceResourceTiming
%% 时间线可视化
class ResourceTimingPhases {
<<时间线阶段>>
1. 重定向阶段
2. DNS查询阶段
3. TCP连接阶段
4. TLS协商阶段
5. 请求阶段
6. 响应阶段
}
%% 资源类型分类
class ResourceTypes {
<<可监控的资源类型>>
- script
- link (CSS)
- img
- xmlhttprequest
- fetch
- iframe
- audio/video
- beacon
- other
}
PerformanceResourceTiming --* ResourceTimingPhases : 包含
PerformanceResourceTiming --* ResourceTypes : 分类依据
然后封装一个监听所有 API 请求且报错的:
function observeErrorRequest(onError: IProps['callback']) {
const disconnect = observeNetwork({
when: ({ initiatorType, responseStatus }) => {
return responseStatus >= 400 && (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest')
},
callback: (entry: PerformanceResourceTiming) => {
onError(entry)
}
})
return disconnect
}
用法就比较简单了:
observeErrorRequest((entry) => {
console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
})
// 当不需要监听的时候
// disconnect()
代码解释
创建一个 PerformanceObserver 来监控资源加载性能指标,重点解释下 type、buffered 和 initiatorType 参数的作用:
type: 'resource'
- 含义:指定要观察的性能条目类型。
type resource可以监听这些类型的资源"link","script","img","xmlhttprequest","fetch","beacon","css" - 作用:这里设置为
'resource'表示观察所有资源加载的性能数据 - 其他常见类型,每一种类型都对应一个 interface
PerformanceXxxTiming,更多见 PerformanceEntry: entryType property - MDN :'largest-contentful-paint'- interfaceLargestContentfulPaint'navigation'- 页面导航性能PerformanceNavigationTiming'paint'- 绘制性能(如 FP、FCP)。描述从 render tree 到绘画在屏幕上的一个个像素PerformancePaintTiming'longtask'- 长任务,阻塞 UI 进程超过 50ms 的任务PerformanceLongTaskTiming'element'- 特定元素的性能PerformanceElementTiming
buffered: true
- 含义:是否处理缓冲区中的历史性能条目
- 作用:
- 当设置为
true时,会立即返回在调用observe()之前已经存在的性能条目 - 当设置为
false时,只观察调用observe()之后新产生的性能条目
- 当设置为
- 使用场景:
- 如果你希望在代码初始化时就能获取到页面加载早期的性能数据,应该设为
true - 如果只关心后续发生的性能事件,可以设为
false
- 如果你希望在代码初始化时就能获取到页面加载早期的性能数据,应该设为
initiatorType: fetch | xmlhttprequest
- 含义:
PerformanceResourceTiming对象中的一个属性,用于表示 资源是由谁/什么发起的请求(即资源的初始化器类型)。它可以帮助开发者分析页面资源加载的来源和依赖关系。 - 作用:
- 筛选出通过 fetch 或 xmlhttprequest 发起的请求
- 使用场景:
- 性能优化分析
- 可以统计哪些类型的资源加载最慢(例如
script、css、img)。 - 检测是否有意外的
fetch/xmlhttprequest请求影响性能。
- 可以统计哪些类型的资源加载最慢(例如
- 错误监控
- 结合
responseStatus(如我们代码所示),可以监控 AJAX/Fetch 请求是否失败(>= 400)。
- 结合
- 性能优化分析
initiatorType 的可能值及含义
以下是常见的 initiatorType 值及其对应的含义:
| 值 | 说明 |
|---|---|
"img" | 资源由 <img> 标签加载(例如 <img src="image.jpg">) |
"script" | 资源由 <script> 标签加载(例如 <script src="app.js">) |
"link" | 资源由 <link> 标签加载(例如 <link rel="stylesheet" href="style.css">) |
"css" | 资源由 CSS 规则加载(例如 @import "theme.css" 或 background: url(...)) |
"xmlhttprequest" | 资源由 XMLHttpRequest 请求加载(AJAX) |
"fetch" | 资源由 fetch() 请求加载 |
"iframe" | 资源由 <iframe> 的 src 加载(例如 <iframe src="page.html">) |
"navigation" | 资源是页面本身(即 HTML 文档的加载) |
"audio" / "video" | 资源由 <audio> 或 <video> 标签加载 |
"beacon" | 资源由 navigator.sendBeacon() 发送 |
"other" | 其他未明确分类的请求 |
组合效果
{ type: 'resource', buffered: true } 表示:
- 观察所有资源加载的性能数据
- 立即获取已经存在的资源加载性能数据(而不仅仅是后续新发生的)
实际应用示例
buffered 应用:
// 这样设置可以获取到页面加载初期所有资源的性能数据
observer.observe({
type: 'resource',
buffered: true
});
// 如果只想观察后续发生的资源加载
observer.observe({
type: 'resource',
buffered: false
});
前端监控:
// 1. 监控关键资源
const observer = new PerformanceObserver((list) => {
list.getEntries()
.filter(entry => entry.initiatorType === 'script')
.forEach(script => {
if (script.duration > 1000) {
console.warn(`脚本加载过慢: ${script.name}`, script);
}
});
});
// 2. 分析图片性能
const imageTimings = performance.getEntriesByType('resource')
.filter(entry => entry.initiatorType === 'img')
.map(img => ({
url: img.name,
size: img.decodedBodySize,
loadTime: img.duration,
isLCPCandidate: img.renderBlockingStatus === 'blocking'
}));
// 3. API性能监控
const apiCalls = performance.getEntriesByType('resource')
.filter(entry =>
entry.initiatorType === 'xmlhttprequest' ||
entry.initiatorType === 'fetch'
)
.map(api => ({
endpoint: new URL(api.name).pathname,
duration: api.duration,
responseSize: api.encodedBodySize
}));
注意事项
buffered: true可能会一次性返回大量历史数据,要注意处理性能- 不是所有性能条目类型都支持
buffered选项 - 使用完毕后应调用
observer.disconnect()停止观察
—— 完 🎉 敬请关注 “JavaScript与编程艺术”——