面试回答思路
如果让我在前端 SDK 里做 Web 性能监控,我一般会分成四层来看:指标采集、数据校准、数据上报、归因分析。
先说指标选择,我一般优先采集 FCP、LCP、CLS、TTFB,必要时再补 INP。因为这几项基本能覆盖页面什么时候开始出内容、核心内容什么时候出来、页面稳不稳定,以及网络和服务端首包快不快。
采集层我会优先用 PerformanceObserver 配合 PerformanceEntry API。像 paint、largest-contentful-paint、layout-shift、navigation 这些 entry type 都能直接监听。这里一个关键点是 buffered: true,因为 SDK 不一定在页面最早阶段初始化,如果不加这个配置,很容易错过 FCP、LCP 这种早期指标。
真正有价值的地方不只是采到数据,而是保证数据可信。比如 CLS 不能简单把所有 layout shift 相加,我会过滤掉用户主动交互导致的位移,也就是 hadRecentInput 为 true 的情况;同时 CLS 要按 session window 去算,而不是全量累加。再比如 FCP、LCP 要考虑页面在后台打开、prerender、或者 bfcache 恢复这些场景,否则采到的值会失真,所以我会结合页面的 firstHiddenTime、activationStart、pageshow persisted 去修正指标。还有 navigation 数据本身也不能盲信,像 responseStart 有时候浏览器会给出异常值,我会先做范围校验,再决定是否上报。
数据处理这层,我一般会做三件事:第一是过滤无效值和异常值,比如 0、负数、明显超出合理区间的数据;第二是去重和增量上报,避免同一个指标被重复发多次;第三是补充归因信息,而不是只传一个数字。比如 LCP 我会拆成 TTFB、资源加载延迟、资源加载耗时、元素渲染延迟;CLS 我会带上发生偏移的 DOM selector、最大偏移源、发生时机。这样监控平台看到 LCP 很高时,不只是知道它慢,还能继续定位到底是后端慢、图片慢,还是前端渲染阻塞。
上报机制上,我一般不会每采到一条就立刻发请求,而是做一个轻量队列,按数量阈值或者时间窗口批量上报,比如 10 条或者 2 秒 flush 一次。页面隐藏、卸载时,再用 sendBeacon 或者 fetch keepalive 做兜底,尽量减少丢包。重试也会做,但通常只做有限次重试,避免网络抖动时把同一批脏数据无限放大。
如果结合项目经验来讲,比较推荐“采集和传输解耦”的思路。SDK 内部通过 integration 机制去挂载性能采集模块,采集侧基于 PerformanceObserver 监听 FCP、LCP、CLS、TTFB,传输侧单独走 transport 层。这样做的好处是后续你想把 fetch 切到批量队列、sendBeacon,或者接入不同后端协议,基本不用动采集逻辑。
高频追问 20 题版
1. 你在前端 SDK 里为什么要做性能监控?
答:核心目的有两个。第一是量化用户体验,知道页面到底慢在哪;第二是能定位问题,不只是看到一个慢的结果,而是能继续拆到网络、资源、渲染还是交互层。
2. 你优先采哪些指标?
答:我一般优先采 FCP、LCP、CLS、TTFB,必要时补 INP。因为这几个指标能覆盖内容出现速度、核心内容完成时间、页面稳定性,以及首包链路表现。
3. FCP、LCP、CLS 分别代表什么?
答:FCP 是页面第一次出现内容的时间,LCP 是最大可见内容渲染完成的时间,CLS 是页面布局抖动程度。一个偏快不快,一个偏主内容何时可见,一个偏稳不稳定。
4. 这些指标你怎么采集?
答:主要用 PerformanceObserver 和 PerformanceEntry。比如 FCP 监听 paint,LCP 监听 largest-contentful-paint,CLS 监听 layout-shift,TTFB 则通过 navigation entry 计算。
5. 为什么选 PerformanceObserver,不直接自己打点?
答:浏览器原生性能时间线更准确,也更标准。自己打点更适合业务流程耗时,但像 LCP、CLS 这种浏览器渲染层指标,手写打点拿不到,或者不够准。
6. PerformanceObserver 使用时最关键的细节是什么?
答:一个关键点是 buffered: true。因为 SDK 不一定在最早时机初始化,如果不加 buffered,FCP、LCP 这种早期指标很容易漏采。
7. CLS 为什么不能直接把所有 layout shift 相加?
答:因为 CLS 不是总和,而是 session window 里的最大累计位移,而且用户主动输入导致的位移不应该计入。所以要过滤 hadRecentInput,再按时间窗口分组计算。
8. LCP 为什么不是拿到最后一个 entry 就结束?
答:因为 LCP 会持续变化,直到页面进入隐藏态,或者用户发生某些输入后才会稳定。所以实现里一般会持续监听,最后在合适时机收敛成最终值。
9. 怎么保证数据准确,而不是采到一堆脏数据?
答:我一般会做三层控制。第一层是浏览器支持性和 API 校验;第二层是过滤异常值,比如负数、0、超范围值;第三层是处理特殊场景,比如后台页、prerender、bfcache 恢复。
10. 后台打开页面为什么会影响指标准确性?
答:因为页面在后台时,渲染和可见性语义已经变了。比如用户根本没看到页面,但某些时间戳可能已经产生了,所以像 FCP、LCP 这类指标要结合 firstHiddenTime 去判断是否有效。
11. prerender 和 bfcache 为什么也要特殊处理?
答:因为这两种场景下,页面生命周期不是标准首次加载流程。比如 prerender 需要用 activationStart 修正时间基准,bfcache 恢复时也要重置或重新计算部分指标,否则同一页会被误判成一次普通新访问。
12. TTFB 是怎么采的?
答:通常基于 PerformanceNavigationTiming 的 responseStart 来算。这个值能反映从导航开始到收到首字节的时间,里面包含 DNS、建连、网络延迟和服务端处理耗时。
13. Navigation Timing 的数据能直接信吗?
答:不能完全直接信。有些浏览器场景下 responseStart 可能缺失、为负数,或者比当前时间还大,所以我会先做有效性校验,校验通过再上报。
14. 只上报一个数值够吗?
答:一般不够。我会尽量做归因。比如 LCP 我会拆成 TTFB、资源加载延迟、资源加载耗时、元素渲染延迟;CLS 我会带上最大偏移元素、偏移发生时机和对应节点信息,这样后续才好定位。
15. 数据上报层怎么设计更合理?
答:我一般不会采一条发一条,而是做队列化和批量上报。常见策略是按数量阈值或者按时间窗口 flush,尽量减少请求数和序列化开销。
16. 页面关闭时数据容易丢,怎么处理?
答:我会在 pagehide、visibilitychange 这些时机做兜底上报,优先考虑 sendBeacon,如果环境不支持,再考虑 fetch keepalive。这样能在页面退出前尽量把数据送出去。
17. 上报失败要不要重试?
答:要,但一般是有限重试。因为监控是辅助能力,不能为了保证监控成功反过来拖慢业务页面,也不能无限重试制造流量放大。
18. 性能监控会不会反过来影响页面性能?
答:会,所以 SDK 自己必须足够轻。我的做法是采集逻辑尽量简单,避免重计算;上报尽量批量;非关键处理延后执行;能复用浏览器原生能力的,就不自己做重逻辑。
19. 如果让你设计 SDK 架构,你会怎么拆?
答:我会拆成采集层、处理层、传输层。采集层负责监听指标,处理层负责过滤、清洗、归因,传输层负责批量上报、重试和宿主环境适配。这样扩展性和可维护性都会更好。
20. 你觉得性能监控最容易被面试官继续追问的点是什么?
答:一般是三类。第一类是指标定义,比如 CLS 为什么不能乱算;第二类是准确性,比如后台页、prerender、bfcache 怎么处理;第三类是工程落地,比如批量上报、丢包兜底、如何降低 SDK 本身开销。