深夜的急诊室,一位值班医生坐在护士站前,面前的大屏上跳动着每个病房的生命体征曲线。他不可能24小时守在每位患者床边,却能透过这套监护系统,第一时间知道谁的心率异常、谁的血压骤降、谁需要马上抢救。你的线上应用就像那位被监护的患者——用户分布在天南海北,设备与网络千差万别,你无法时刻站在他们身边。前端监控,就是给每个用户戴上一支永不休息的“生命体征监护仪”。 这套监护系统并不神秘:它由若干台感应设备组成——心电贴片负责捕捉异常的心电波形(错误捕获),病历上的折叠索引帮你把撕碎的记录还原成原始内容(Source Map),血压心率血氧三大项用来快速分诊(Core Web Vitals),护士站的抽血与传呼规则决定了信息如何轻量上报,值班医生的报警阈值决定了哪些异常值得被唤醒,而凌晨的会诊复盘则让整场抢救沉淀为下一次改进的经验。 今天,我们就以这家“线上应用急诊室”为起点,一步步搭建属于你自己的前端监控体系。

岗位关联

在公司里,能从“能上线”进阶到“能保障线上”的工程师,才真正握住了稳定性的方向盘。前端监控与错误上报是从业者从“写代码”走向“为线上体验负责”的必修课。

目录
  1. 可视化演示
  2. 监控体系总览
  3. JS错误捕获
  4. Source Map还原
  5. 性能指标采集
  6. 数据上报策略
  7. 错误聚合告警
  8. 数据分析排查
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示前端监控体系:JS错误捕获与Source Map还原、Core Web Vitals性能指标采集,以及错误聚合采样与告警阈值策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

JS错误捕获与上报流程 window.onerror和unhandledrejection全局捕获JS运行时错误 Sentry SDK采集错误堆栈,Source Map还原压缩代码位置 错误数据上报至服务端,聚合分析后触发告警通知 Error→onerror→SDK→SourceMap→Server→Alert 全链路监控保障线上质量

监控体系总览

每一位值班医生面前都有一套完整的监护网络:病房里的感应设备负责采集数据和血压,走廊里的护士站负责把数据汇总成一张趋势图,而值班室里的医生根据这些图决定是否行动。前端监控体系正是这样一座三层“急诊室”——采集层、传输层、消费层各司其职,共同构成从发现问题到解决问题的闭环链路。

本地开发时,我们习惯用 console.log 与 debugger 自说自话,因为一切都在掌握之中。可一旦代码上线,用户设备千奇百怪,网络环境瞬息万变,本地无法复现的问题在线上可能大范围爆发。这时前端监控的价值便显现出来:它把“用户反馈驱动”的被动模式,升级为“数据驱动”的主动观测。可观测性的三根支柱——日志、指标、链路追踪,就像急诊室的心电波形、体温单和病程记录,从不同维度还原系统现场。

然而,监控链路的每一层都会产生损耗:有的事件未被 SDK 捕获,有的事件捕获了却无法还原堆栈,有的事件上报途中丢失。我们用一条“漏斗公式”衡量监控体系的完备程度——三层损耗相乘,才是最终真正进入分析平台的事件比例。

初学监控的人最容易陷入“埋头搭采集、不管链路通不通”的误区。但正如急诊室的价值在于最后那个被及时抢救的患者,监控的价值也体现在闭环的终点——只有当数据的最后一公里打通,采集才真正有意义。

Obs=EcaptureEtotal×SmapEcapture×DarriveSmap\displaystyle Obs = \frac{E_{capture}}{E_{total}} \times \frac{S_{map}}{E_{capture}} \times \frac{D_{arrive}}{S_{map}}
这条公式说明:监控的完备度不是某一层做得极致,而是采集、还原、上报三层效率的乘积。任何一层出现短板,整体都会被大幅拉低。
  1. 捕获率被监控 SDK 捕获到的错误数占全部实际错误数的比例,对应心电贴片能够记录到的异常波形范围。
    EcaptureEtotal\displaystyle \frac{E_{capture}}{E_{total}}
  2. 还原率在捕获到的错误中,堆栈能被 Source Map 成功还原为源码位置的比例,对应病历索引查找的成功率。
    SmapEcapture\displaystyle \frac{S_{map}}{E_{capture}}
  3. 到达率还原后的数据最终成功抵达监控平台的比例,对应护士站是不是把每一份记录都交到了医生手上。
    DarriveSmap\displaystyle \frac{D_{arrive}}{S_{map}}

JS错误捕获

急诊室的监护设备各有分工:心电贴片贴在患者胸口,只能监测躺在病床上的人,这就是 try/catch 的工作方式;动态心电图机让患者背回家,24 小时记录回家后的异常,这是针对异步错误的额外防线;而 window.onerror 则像一位永远在走廊巡逻的值班护士,任何没被病房设备捕捉到的呼救声,都由她来兜底。

这些设备最大的区别在于“作用域”。try/catch 只能捕获它所在代码块执行期间同步抛出的异常;当代码执行已经离开了这个作用域,比如定时器的回调在数秒后才被触发,它抛出的异常早已不在 try 的监听范围之内。这就像监考老师只在考场内抓作弊,学生走出考场后的作弊行为,他已经没有管辖权了。

真正完备的客户端捕获方案,需要四道防线协同:try/catch 捕获同步局部异常;window.onerror 捕获全局未处理异常;unhandledrejection 捕获 Promise 中未被处理的拒绝;error 事件捕获资源加载失败(图片、脚本、样式表等)。要注意,资源加载错误的事件不冒泡,必须在捕获阶段监听才能拦住它们。

在 Sentry 等成熟的监控 SDK 中,这些监听器在初始化时一次性注册完成,正是因为这四类错误的捕获方式互相独立、缺一不可。回到急诊室的比喻:只有心电、动态心电、化验室和影像科同时运转,医生在午夜面对大屏时才不会漏掉真正的危机。

Dtotal=Dsync∪Dasync∪Dpromise∪Dresource\displaystyle D_{total} = D_{sync} \cup D_{async} \cup D_{promise} \cup D_{resource}
四个集合的并集,才是线上发生的全部错误。任何一道防线缺失,都会有一类错误成为监控盲区。
  1. 同步异常try/catch 直接包裹的同步代码中抛出的错误,例如函数传参错误、类型读取错误。
    Dsync\displaystyle D_{sync}
  2. 异步异常setTimeout、事件回调、requestAnimationFrame 中抛出的错误,需要全局监听器接住。
    Dasync\displaystyle D_{async}
  3. 拒绝异常Promise 链中被 reject 但无人处理的异常,只能由 unhandledrejection 事件拦截。
    Dpromise\displaystyle D_{promise}
  4. 资源异常加载脚本、图片、样式表等资源失败产生的 error 事件,不属于传统 JS 异常体系。
    Dresource\displaystyle D_{resource}

Source Map还原

线上代码为了体积和性能,会被打包工具压缩成一行行“天书”:变量名变成单个字母,换行被全部删除,函数被合并进看不见的结构里。这就像一份被揉成一团、又淋了水的病历,字迹几乎无法辨认。Source Map 就是藏在病历纸背面的折叠索引,它记录着纸上的每个单元格原来属于哪个章节、哪一行。

Webpack 在构建时通过 devtool 配置生成 .map 文件,里面用高效的编码记录了压缩产物与原始源码之间的映射关系。线上报错堆栈形如 app.9f3c.js:1:23457——文件、行号、列号全部指向压缩后的坐标。没有 Source Map,这个堆栈就像病历上的坐标指向了一堆墨迹;有了它,Sentry 的 SourceMapConsumer 可以反向解码,把压缩坐标翻译回源码文件的真实行列。

这背后的数学本质是一个坐标映射函数:给定压缩产物中的行列对,经过 source map 的 mappings 字段查找与 VLQ 解码,输出原始源码中的行列对。VLQ(可变长数量级编码)的核心贡献,是用尽量短的字符串压缩了这张庞大的坐标索引表,让 .map 文件不至于体积爆炸。

值得一提的是工程实践中的安全意识:.map 文件绝不能部署到公网 CDN,否则任何人打开控制台就能还原你的全部源码。正确做法是像保存病历原件一样,只把 .map 上传到 Sentry 等监控平台的私有仓库,用 access token 鉴权保护,既保留调试能力,又避免源码泄露。

(sline,scol)=M(cline,ccol)\displaystyle (s_{line}, s_{col}) = M(c_{line}, c_{col})
映射函数 M 就像一个翻译官,把“压缩产物坐标”这门陌生的外语,翻译成“源码坐标”这门开发者熟悉的本土语言。
  1. 压缩坐标错误堆栈中记录的别名文件行号与列号,对应揉皱病历上的格子编号。
    (cline,ccol)\displaystyle (c_{line}, c_{col})
  2. 映射函数通过 SourceMapConsumer 读取 .map 文件中的 mappings 字段并解码,实现坐标翻译。
    M\displaystyle M
  3. 原始坐标翻译后得到的真实源码文件中的行号与列号,让开发者在熟悉的代码中定位问题。
    (sline,scol)\displaystyle (s_{line}, s_{col})

性能指标采集

急诊科的分诊台通常会在 30 秒内完成三项快速检查:测血压、贴心率贴、探体温。这三项读数决定了患者被分到普通门诊还是抢救室。Core Web Vitals 便是前端体验的“分诊三项”:LCP 是血压——主内容有没有在可接受的时间内让用户看到;INP 是心率——用户点击后界面能不能及时给出响应;CLS 是体温——页面会不会在加载过程中突然抖动移位。

这三项指标并非人为拍脑袋,而是 Google 基于海量用户体验研究选定的:LCP(Largest Contentful Paint)衡量最大内容元素的渲染完成时间,良好线在 2.5 秒以内;INP(Interaction to Next Paint)衡量用户交互事件到下一次界面绘制之间的延迟,良好线在 200 毫秒以内;CLS(Cumulative Layout Shift)衡量整个页面生命周期中所有非预期布局偏移的累加分数,良好线在 0.1 以内。

采集性能指标与采集错误不同:错误是突发的离散事件,性能则是在整个页面生命周期内不断产生的连续过程。工程上通过 PerformanceObserver API 订阅 largest-contentful-paint、layout-shift、event 等条目,在页面进入后台或卸载前统一计算并上报。聚合评估时通常取 P75 分位数而非平均值,因为平均值会被极少数极端用户拉偏,而 P75 能代表“大部分用户的实际体验”。CLS 的计算涉及到对每一次布局偏移的精确度量。

对性能指标的比喻回扣:就像分诊台的三项读数最终写入患者病历成为后续医生的参考基线,每一次页面访问产生的性能指标也被采集、聚合、入库,为团队提供了持续追踪体验演化的数据基础。

CLS=∑i(wiW×diH)\displaystyle CLS = \sum_{i} \left( \frac{w_{i}}{W} \times \frac{d_{i}}{H} \right)
每一次布局偏移的“危害程度”等于它占视口的面积比例乘以移动的距离比例;CLS 就是整个访问过程中这些危害值的累加。
  1. 影响面积第 i 次偏移中,发生位移的元素在视口内占据的宽度比例,元素越大,视觉冲击越强。
    wiW\displaystyle \frac{w_{i}}{W}
  2. 偏移距离该元素竖直方向移动的距离与视口高度的比值,移动得越远,用户感觉页面越“跳”。
    diH\displaystyle \frac{d_{i}}{H}
  3. 累加求和把整个页面生命周期内所有非预期偏移的这两个分数相乘并累加,得到最终的 CLS 指标。
    ∑i\displaystyle \sum_{i}

数据上报策略

护士给患者抽血时绝不会抽干全身的血,而是按化验项目取最少的量;急诊室的传呼系统也不会让每个探视家属都直接闯入值班室。前端监控的数据上报遵循同样的轻量原则:用户越多的产品,越不能把监控请求变成事故本身。未经采样的全量上报,在高并发场景下会让监控服务器先于业务崩溃。

缓解这种压力的第一层手段是采样。采样策略按数据类型分层:致命错误往往按用户维度全量采集,因为单条异常就有修复价值;性能指标这类高频数据则按用户 id 哈希取模随机采样,比如 10% 的用户上报;对于低优先级的日志,可以进一步压缩采样比例。采样率不是拍脑袋定的,它与希望达到的统计精度直接相关。统计学中估计总体比例所需的最小样本量公式是:

上报通道的设计同样关键。大量小请求会拖慢页面并占满连接池,因此通常先在内存中批量累积事件,按时间窗口(如 5 秒)合并打包、做 gzip 压缩后统一发出。页面卸载瞬间的数据最容易丢失——用户即将关闭标签页时,普通 fetch 请求可能来不及发出。navigator.sendBeacon() 是浏览器提供的专用接口,它不阻塞页面卸载,由浏览器保证数据尽量送达。每一次上报若失败,系统还要有兜底:把数据暂存到 localStorage,下次启动恢复时重试,并设置队列上限防止积压。

n=Z2×p×(1−p)E2\displaystyle n = \frac{Z^{2} \times p \times (1-p)}{E^{2}}
公式的含义是:在 95% 置信度下(Z=1.96),假设真实错误率为 p,允许误差为 E,理论上至少需要 n 个样本才能可靠估计真实错误率。
  1. 置信度系数Z 值对应置信水平,95% 置信度下取 1.96,它决定了结论的可靠程度。
    Z\displaystyle Z
  2. 预估比例p 是预估的错误率或采样比例,当 p 越接近 0.5 时,需要样本量越大。
    p×(1−p)\displaystyle p \times (1-p)
  3. 容许误差E 是允许的估计误差范围,误差要求越严格,需要的样本越多。
    E2\displaystyle E^{2}

错误聚合告警

流感季的急诊室里,医生不会把“张三咳嗽”和“李四发烧”当成两个不同的罕见病分别研究,而是根据相似症状归入同一个“流感”确诊组。前端监控每天可能收到数百万条错误记录,如果逐条查看,工程师很快会失去判断力。聚合的第一步就是生成错误指纹——把错误的 message、堆栈首帧、文件名、行列号等关键特征拼接后做哈希,得到一串稳定的指纹 ID。凡是指纹相同的错误,都归并为同一条 issue 来跟踪。

告警阈值的设计则像监护仪的报警线:调得太低,机器每五分钟响一次,护士很快麻木,真正危急时反而没人理会;调得太高,患者心跳停止了好一阵才会报警。静态固定阈值(如“错误数大于 100 就告警”)往往失效,因为业务流量有昼夜与周末的自然波动。成熟方案是动态基线:对比当前时间窗口的错误数与前 7 天同一时段的历史平均值,计算突增倍数,超过设定系数便触发告警。

告警发出后还要分级路由:P0 级影响核心交易链路的故障,直接拨打电话并同时短信通知值班负责人;P1 级通过即时通讯工具推送;P2 级归档到邮件日报。同时设置告警风暴抑制机制——同一指纹在短时间内反复触发时合并通知,避免深夜连环轰炸。

R=EnowEbase,R≥K\displaystyle R = \frac{E_{now}}{E_{base}}, \qquad R \ge K
K 通常取 3 到 5。只有在当前错误数达到历史同期基线 3 倍以上时,才认为这是一次值得惊动值班人的异常突增。
  1. 当前窗口计数最近 5 分钟或 10 分钟内新聚合的错误条数,对应监护仪此刻读到的瞬时心率。
    Enow\displaystyle E_{now}
  2. 历史基线均值前 7 天在同一时间段的平均错误条数,相当于患者过去的安静心率基线。
    Ebase\displaystyle E_{base}
  3. 倍数判定两者相除得到突增倍数,当它超过阈值 K 时触发告警,意味着“今天的异常需要醒来处理”。
    R≥K\displaystyle R \ge K

数据分析排查

深夜的急诊室在完成抢救后,医生团队会围坐在一起复盘:这场“流感”影响了多少患者?第一例是从哪个片区传来的?换药之后情况是否好转?前端监控的最后一公里同样不是收到告警就结束,而是把告警翻译成可执行的修复方案。第一步是影响面评估:通过受影响的用户数与活跃总用户数的比值判断问题优先级。

影响面明确之后,复现路径还原成为关键。Sentry 会把同一个用户会话中的路由跳转、点击事件、控制台日志和错误发生点按时间序列串成一条“行为链”,工程师借此还原用户是在哪一步操作后触发了异常。接下来是版本维度分析:把错误按 release 版本分组对比,如果新版发布后某指纹的错误量呈现陡增,而旧版没有,这通常是发布引入的回归——决策也清晰起来:要么快速热修复,要么直接回滚到上一个稳定版本。

复盘的终点是闭环迭代:定位根因后补充单元测试与场景覆盖,修复上线后持续观察监控曲线回落。正如急诊室的复盘不仅治好了今夜的患者,还改进了分诊流程,前端监控的真正价值也体现在每一次事故之后的体系优化——让下一场“流感”还没爆发就被识别。

I=UaffectedUtotal×100%\displaystyle I = \frac{U_{affected}}{U_{total}} \times 100\%
影响面越高,修复的紧迫性越大。工程师依据这个百分比决定是通宵修复、白班处理,还是仅记录跟踪。
  1. 受影响用户数错误上报中出现的去重用户 ID 数量,对应这次“疫情”波及的就诊人数。
    Uaffected\displaystyle U_{affected}
  2. 活跃用户总数时间段内的总活跃用户数,作为影响比的基准分母。
    Utotal\displaystyle U_{total}
  3. 百分比转换两者相除后乘以 100%,得到直观的影响比例。
    ×100%\displaystyle \times 100\%

AI 应用场景

新版本发布夜间值班监控

阶段:模型/应用上线后的线上效果监控与应急响应

解决的问题:传统发布流程依赖用户主动投诉才发现回归,反馈周期长达数小时甚至数天;接入监控后,异常在几分钟内通过告警通知到值班工程师,相当于监护仪的报警线第一时间捕捉到患者体征骤变。

具体案例:某电商团队发布新版首页后,LCP 性能指标从 1.8 秒陡增至 4.2 秒,动态基线告警立即触达值班群。工程师打开监控看板,按版本分组对比发现新引入的第三方统计脚本阻塞了首屏渲染,于是通过灰度开关快速降级,性能曲线在 15 分钟内回落到正常区间。

用户投诉白屏问题的快速定位

阶段:线上推理结果异常时的根因分析与定位

解决的问题:客服收到大量“页面白屏”投诉,传统排查方式难以直接复现用户环境,误判率高;通过错误上报拿到用户的真实堆栈与设备信息,用 Source Map 还原后精确锁定问题代码。

具体案例:某在线文档产品收到一批 iOS 低版本用户的白屏投诉。Sentry 堆栈显示压缩代码 app.1a2b.js:1:45672,Source Map 还原后定位到 utils.js 第 18 行使用了低版本 Safari 不支持的 replaceAll 方法。工程师用兼容写法替换后重新发布,同类错误归零。

大促前的性能容量评估

阶段:容量规划与体验优化策略制定

解决的问题:大促前夕无法预估页面的性能瓶颈,单靠本地测试无法覆盖弱网、低端机等真实场景;利用性能指标采样数据,可以评估不同地区、不同设备的真实体验分布。

具体案例:某直播电商在大促前调取近 7 天的 LCP 与 CLS 指标,按省份聚合后发现华北地区 LCP P75 达到 3.5 秒,远超 2.5 秒良好线。进一步分析是当地 CDN 节点命中率偏低导致的资源回源延迟,调整调度策略后 P75 降至 1.9 秒,活动首日用户流失率同比下降 12%。

Python 演示

python51 行
import hashlibimport jsonfrom collections import defaultdict  def generate_fingerprint(error):    """生成错误指纹,供聚合使用"""    parts = [        error.get("message", "").strip(),        error.get("filename", "").split("/")[-1],        error.get("line", 0),        error.get("column", 0),    ]    key = "|".join([str(p) for p in parts])    return hashlib.md5(key.encode("utf-8")).hexdigest()  def load_errors(path):    with open(path, "r", encoding="utf-8") as f:        return [json.loads(line) for line in f if line.strip()]  def aggregate(errors):    groups = defaultdict(list)    for err in errors:        fp = generate_fingerprint(err)        groups[fp].append(            {                "user": err.get("user", "anonymous"),                "browser": err.get("browser", "unknown"),                "time": err.get("time", 0),            }        )    return groups  def main():    sample = [        {"message": "Cannot read properties of undefined", "filename": "app.9f3.js", "line": 23, "column": 5, "user": "u_1001", "browser": "chrome", "time": 1710000000},        {"message": "Cannot read properties of undefined", "filename": "app.9f3.js", "line": 23, "column": 5, "user": "u_1002", "browser": "safari", "time": 1710000010},        {"message": "NetworkError: Failed to fetch", "filename": "api.js", "line": 12, "column": 3, "user": "u_1003", "browser": "chrome", "time": 1710000100},    ]    groups = aggregate(sample)    for fp, items in sorted(groups.items(), key=lambda kv: -len(kv[1])):        print(f"指纹 {fp}: 影响 {len(items)} 名用户")        for item in items:            print("  ", item["browser"], "@", item["time"])  if __name__ == "__main__":    main()
目的:演示监控平台“错误聚合”的核心思路——用指纹把海量原始错误折叠成少量可跟踪的 issue,并按影响面排序。
  1. key = "|".join([str(p) for p in parts])用竖线拼接特征串,避免字段边界黏连导致不同错误生成同一指纹。
  2. return hashlib.md5(key.encode("utf-8")).hexdigest()将特征串哈希为 32 位定长指纹,作为分组的唯一 ID。
  3. groups = defaultdict(list)按指纹分组的字典,新指纹自动创建空列表,无需手动判空。
  4. if __name__ == "__main__":保证脚本被直接运行时才执行 main,被 import 时不产生副作用。
  5. sorted(groups.items(), key=lambda kv: -len(kv[1]))按影响用户数降序排列,让最严重的错误簇排在最前。

常见误区

误区初学 JavaScript 时,老师告诉我们“用 try/catch 包裹代码就能处理错误”,于是很多同学上线前把所有业务函数都包一层 try/catch,以为这样就万无一失,结果线上错误依然从监控里冒出来。

正解try/catch 只能捕获其所在作用域内同步执行的异常。异步回调(setTimeout、事件监听器、Promise.then)在作用域退出之后才执行,try 已经“下班”,管不到它们。就像监考老师只负责考场内的纪律,学生交卷后在外面作弊他根本无法察觉。完整的错误捕获依赖 window.onerror、unhandledrejection 等全局监听协同工作。

为何常见:try/catch 是大多数人接触的第一个错误处理工具,语法直观、包裹即安心,容易让人把它想象成覆盖一切的魔法盾牌,忽略了作用域和执行时机的约束。

误区有人觉得 source map 只是调试辅助文件,部署到公网 CDN 也没关系,反正用户又不会主动打开控制台去看——这种想法掩盖了源码完全暴露的风险。

正解source map 包含完整的原始源码内容和目录结构,只要部署到公网,任何用户都能通过控制台反向还原,等于把病历原件贴在了病房门口。正确做法是像保存医疗档案一样单独管理:将 .map 上传到 Sentry 等监控平台的私有仓库,访问需要密钥鉴权;公网产物不携带 sourceMappingURL 注释。

为何常见:本地构建时 map 文件默认与 dist 产物放在一起,习惯成自然后容易直接全量上传 CDN,没有意识到它和源码在保密性上等价。

误区刚接手监控平台时,看到监控上报的请求量占了业务流量的好几个百分点,心疼服务器开销,于是一口气把采样率调到 1%,想着“能看个大概就行”。

正解采样率过低会让统计失去意义——真实错误率如果是 0.5%,1% 的采样下每天可能只采到个位数样本,任何聚合、告警都会因为样本量不足而失真。就像抽血只抽一滴,化验室连血常规都做不出来。正确的做法是按数据用途分层:致命错误全量采集保证不漏,高频指标用统计学样本量公式估算所需采样比例,在成本与精度之间取平衡点。

为何常见:省成本的心理驱动下,人容易只看到采样的开销而忽略数据的用途,低估了“数据量不够”对决策质量的破坏力。

误区第一次被线上故障教育之后,有人会走向另一个极端:把所有告警阈值都下调到最低,任何风吹草动都通知全组,误以为告警越频繁说明监控越负责。

正解阈值过低会引发告警风暴,值班者一晚上被几百条通知轰炸后,就会形成对告警的心理麻木——等真正的 P0 出现时,反而没人重视了,这就是“狼来了”效应。好阈值应该与历史基线对比并设定动态倍数(通常是 3-5 倍),配合分级路由:P0 电话、P1 即时消息、P2 邮件归档,让每一条告警都有明确的行动价值。

为何常见:人对“安全”的心理是宁可多做也不可漏做,却没有意识到过度告警会污染注意力这种有限资源,让真正关键的信号淹没在噪声中。

误区有经验的工程师也常说“页面能打开就行,性能优化是锦上添花”,对 LCP、INP、CLS 这类指标漫不经心,直到大促时看到用户流失数据才开始慌。

正解性能直接作用于业务的转化率与留存率——首屏多一秒,用户流失率可能上升好几个百分点;CLS 从 0.1 恶化到 0.3,用户对页面“不稳定”的感受会显著下降。Core Web Vitals 是 Google 搜索排名的影响因子之一,也是用户体验团队最核心的量化依据。监测这些指标不是给老板看的美观报表,而是像跟踪患者体温一样早期发现恶化的苗头。

为何常见:优化效果不像功能开发那样“看得见摸得着”,性能问题的后果是渐进式的,缺少立竿见影的反馈就容易让人低估其重要性。

面试题

Q1. 以下哪种错误 try/catch 无法捕获?

单选题 · easy

  • A.同步函数中的类型错误
  • B.setTimeout 回调抛出的异常← 正确答案
  • C.JSON.parse 解析时的语法错误
  • D.普通函数调用时的引用错误

答案:B。try/catch 只对同步作用域内的异常生效。setTimeout 回调在函数执行完后才进入事件循环执行,此时 try 的作用域已经退出,异常无人接管。这就像监考老师只在考场内抓作弊,考生离场后发生的事情他无权管辖。A、C、D 都是同步代码块内抛出的错误,try/catch 可以正常捕获。

Q2. window.onerror 无法捕获下列哪类错误?

单选题 · easy

  • A.运行时类型错误
  • B.图片资源加载失败← 正确答案
  • C.未捕获的引用错误
  • D.全局的数组越界

答案:B。资源加载错误(如 <img> 的 404)虽然也产生 error 事件,但不冒泡到 window,因此 window.onerror 捕获不到,需要用捕获阶段的事件监听来处理。这就像影像科的胶片丢失不会自动传达到护士站的传呼系统,需要科室自己的设备来发现。A、C、D 都是 JS 运行时异常,会被 window.onerror 兜底接住。

Q3. Source Map 中 VLQ 编码的主要作用是什么?

单选题 · easy

  • A.将压缩代码还原为原始源码
  • B.对映射表进行高效的压缩编码← 正确答案
  • C.对线上源码进行加密与保护
  • D.将多个错误堆栈合并为一条

答案:B。VLQ(可变长数量级编码)用于压缩 source map 中庞大的 mappings 映射表,让坐标索引文件的体积可接受。它本身不还原源码(那是 SourceMapConsumer 的工作),也不负责加密。这就好比病历背面的索引用缩写符号记录位置,符号本身不是病历内容,只是高效查找的工具。

Q4. LCP(Largest Contentful Paint)反映的是什么?

单选题 · mid

  • A.页面布局的视觉稳定性指标
  • B.用户交互后的响应延迟指标
  • C.最大内容元素的渲染完成时间← 正确答案
  • D.页面从加载到可交互的时间指标

答案:C。LCP 衡量视口内最大内容元素(文字块、图片、视频)完成渲染的时间,代表用户“看到主要页面内容”的快慢,良好线是 2.5 秒。A 对应 CLS,B 对应 INP,D 对应 TTI(Time to Interactive),三者都是性能指标但含义完全不同,注意区分。

Q5. 以下哪项数值符合 CLS 指标的“良好”标准?

单选题 · mid

  • A.低于 2.5 秒
  • B.低于 0.1 分← 正确答案
  • C.低于 200 毫秒
  • D.低于 1.0 分

答案:B。CLS 是布局偏移的累加分数,无单位,良好线是 0.1 以下。A 的 2.5 秒是 LCP 的良好线,C 的 200 毫秒是 INP 的良好线,D 的 1.0 分是短视频平台的评分习惯,与 Web 指标无关。

Q6. 下列哪些场景适合用 window.onerror 做兜底捕获?

多选题 · mid

  • A.第三方脚本抛出的未捕获异常← 正确答案
  • B.未包裹 try-catch 的老代码运行时错误← 正确答案
  • C.Promise 中未处理的拒绝异常
  • D.框架异常边界之外的渲染错误← 正确答案

答案:ABD。window.onerror 是所有未被局部捕获的 JS 运行时异常的最后防线,包括第三方脚本和老代码。但 Promise 的拒绝不属于普通异常,必须由 unhandledrejection 事件捕获,所以 C 不选。这就像走廊巡逻护士能接住病房里传出的呼救声,但化验室传来的电子报告走的是另一条通信线路。

Q7. 关于 Core Web Vitals,下列说法正确的有?

多选题 · mid

  • A.LCP 衡量最大内容元素的渲染时间← 正确答案
  • B.INP 反映用户交互后的响应延迟← 正确答案
  • C.CLS 统计布局偏移的累加分数← 正确答案
  • D.上报时只需取平均值即可评估

答案:ABC。A、B、C 分别正确描述了 LCP、INP、CLS 的定义。D 错在聚合方式——平均值会被极端用户拉偏,行业标准是取 P75 分位数代表大多数用户体验,相当于用“大多数患者的体温分布”而非“个人极端值”来判断疫情。

Q8. 页面即将卸载时上报监控数据,最可靠的手段是?

单选题 · hard

  • A.XMLHttpRequest 同步发送请求
  • B.fetch 异步发送请求
  • C.navigator.sendBeacon 发送请求← 正确答案
  • D.script 标签注入发送请求

答案:C。页面卸载瞬间,普通异步请求可能来不及发出;同步 XHR 会阻塞页面且已被浏览器逐渐废弃。navigator.sendBeacon 是浏览器专为此场景设计的接口,不阻塞卸载流程且由浏览器保证尽力送达。这就像患者在电梯门关闭瞬间按下应急铃,这部专用电话和基站有特殊保障通道,普通电话可能刚拨出去就断了。

Q9. 关于前端监控采样策略,正确的做法有?

多选题 · hard

  • A.所有用户全量采集所有指标
  • B.高频性能指标按比例随机采样← 正确答案
  • C.致命错误按用户维度全量采集← 正确答案
  • D.用简单平均还原采样总体量级

答案:BC。全量采集会产生巨大的请求量,通常只在致命错误或核心链路场景使用;高频指标按比例采样可控制成本。C 正确:致命错误单条就有价值。D 错误:从样本还原总体需要用权重乘以采样比例的倒数,而非简单平均。这就像抽血按检验目的确定采血量,且化验报告换算时要用回稀释倍数。

Q10. 关于告警阈值设置的最佳实践是?

单选题 · extreme

  • A.阈值设得越低越能及时发现问题
  • B.与历史基线对比并设定动态倍数← 正确答案
  • C.所有错误类型统一使用相同阈值
  • D.关闭告警以避免打扰开发人员

答案:B。不同时段流量的自然波动会让固定阈值失真,最佳实践是当前窗口错误数与历史同期基线对比,以动态倍数(通常 3-5)判定突增,兼顾准确性与灵敏度。A 会造成告警风暴让人麻木;C 忽略错误类型差异,致命错误和低危日志不应同标准;D 直接使监控失去意义。就像监护仪的报警线要适配患者个人心率基线,统一阈值必然一边漏报一边打扰。