公司用到了APM对前端应用进行性能监控,为了了解其原理和数值指标,我对源码进行了一番研究。
APM的指标
APM JS客户端主要使用Navigation_timing_API和Resource_Timing_API对界面加载性能和静态资源加载时间进行监控。
他们提供了一系列指标对页面的加载性能进行评估。
例如 Navigation_timing_API:1
performance.getEntriesByType('navigation')[0]
输出:1
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{
unloadEventStart: 0
unloadEventEnd: 0
domInteractive: 2981.1850000114646
domContentLoadedEventStart: 3019.0850000071805
domContentLoadedEventEnd: 3019.6350000042003
domComplete: 78957.03499999945
loadEventStart: 78957.06000001519
loadEventEnd: 78957.12999999523
type: "navigate"
redirectCount: 0
initiatorType: "navigation"
nextHopProtocol: "h2"
workerStart: 0
redirectStart: 0
redirectEnd: 0
fetchStart: 4.27000000490807
domainLookupStart: 113.70499999611638
domainLookupEnd: 246.34000001242384
connectStart: 246.34000001242384
connectEnd: 337.3050000227522
secureConnectionStart: 271.5100000204984
requestStart: 337.5800000212621
responseStart: 1154.450000001816
responseEnd: 2945.3750000102445
transferSize: 22851
encodedBodySize: 22299
decodedBodySize: 219855
serverTiming: []
name: "https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API"
entryType: "navigation"
startTime: 0
duration: 78957.12999999523
}
1 |
|
APM的管理页面上对上述参数进行整理,page-load
事务中包含以下指标:
timeToFirstByte
对应PerformanceTiming的responseStart
。
domInteractive
对应PerformanceTiming的domInteractive
。指浏览器解析html文档的状态为interactive时的时间节点,此时主文档的解析器结束工作,即 Document.readyState
改变为 ‘interactive’,但是内嵌资源(比如图片、css、js等)还未加载完成。
domComplete
顾名思义。
firstContentfulPaint
用来指示一部分内容元素能够在可视窗口显示出来,需要的渲染时间。1
2
3
4
5
6const unloadDiff = timing.fetchStart - timing.navigationStart
const firstContentfulPaint = performance.getEntriesByType('paint').reduce((_fcp, entry) => {
if (entry.name === 'first-contentful-paint') {
return unloadDiff >= 0 ? entry.startTime - unloadDiff : entry.startTime
}
}, null)
lagestContentfulPaint
用来指示最大内容元素能够在可视窗口显示出来,需要的渲染时间。1
2
3
4
5
6
7
8const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
const lastEntry = entries[entries.length - 1]
lcp = lastEntry.renderTime || lastEntry.loadTime
})
po.observe({type: 'largest-contentful-paint', buffered: true})
由此我们可以根据上述指标来给自己项目的性能优化进行定位。
APM的事务
APM的监控数据以事务(Transaction)为单位,每个事务下有多个时间跨度(span),同一时间只能有一个正在活跃的事务。如果在存在正在活跃的事务的情况下再开始另一个事务,事务之间会按照优先级进行合并。
事务的类型的优先级如下:page-load
> route-change
> user-interaction
> http-request
> custom
> temporary
。例如用户点击了页面,使当前事务为user-interaction
,如果此时导致了页面路由切换,即开始了事务route-change
,由于route-change
的优先级比较大,当前事务就切换为route-change
,原事务的监控数据会并入到当前事务。
通过transaction.startSpan(name, type, { blocking: true })
添加的span
会延长事务的结束时间,APM默认所有http请求都为blocking: true
,所有事务都会因为http请求延长时间。
page-load
这类事务监控页面加载过程的性能数据。最短到domComplete
就结束,但如果结束前有其他blocking
的任务,则会等到所有任务完成后才结束。这就是为什么有些事务duration
非常大,因为在domComplete
之前添加了耗时的请求。
route-change
这类事务监控页面路由切换的性能数据。APM会拦截history.pushState
来开启route-change
事务。
注意,route-change
的事务开始后并没有主动去检测结束状态,它不是像page-load
那样在domComplete
主动检测状态并结束,如果route-change
过程中没有其他任务,则这个事务可能会一直处于活跃状态,直到加了一些任务并结束才会有机会结束这个事务,所以才会经常出现事务长达几十秒的情况。所以这里推荐使用@elastic/apm-rum-react或者@elastic/apm-rum-vue等特定的框架,这样才能有效反映route-change
的耗时。
packages/rum-react/src/get-with-transaction.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const [transaction] = React.useState(() => {
const tr = apm.startTransaction(name, type, {
managed: true,
canReuse: true
})
callback(tr, props)
return tr
})
React.useEffect(() => {
afterFrame(() => transaction && transaction.detectFinish())
return () => {
transaction && transaction.detectFinish()
}
}, [])
packages/rum-vue/src/route-hooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21router.beforeEach((to, from, next) => {
const matched = to.matched || []
let path = to.path
if (matched.length > 0) {
path = matched[matched.length - 1].path || path
}
transaction = apm.startTransaction(path, 'route-change', {
managed: true,
canReuse: true
})
next()
})
router.afterEach(() => {
afterFrame(() => transaction && transaction.detectFinish())
})
router.onError(() => {
transaction && transaction.end()
})
或者可以手动detectFinish
:1
2
3
4
5
6
7
8
9
10
11history.listen(() => {
const trans = apm.getCurrentTransaction();
if (trans) {
afterFrame(() => {
const trans = apm.getCurrentTransaction();
if (trans) {
(trans as any).detectFinish();
}
});
}
});
user-interaction
点击事件(eventType === 'click'
)会触发这个事务。复用间隔为100毫秒,即100毫秒后的点击事件会触发新的user-interaction
事务。
http-request
任何http请求都会触发这个事务。当然,前提是没有更高优先级的事务。
custom
自定义事务类型。apm.startTransaction(name, 'custom')
。
temporary
临时的事务。当前没有活跃的事务,且用户通过apm.startSpan(name, type)
的方式添加span
时,会开启类型为temporary
的事务。这类事务如果没有合并到优先级较高的事务,最终会被抛弃,不会上传到APM Server。