跳到主要内容

性能优化热门面试题

使用说明

本篇共 180 道题,覆盖指标与方法论、加载与网络、构建与 JavaScript、渲染与交互、图片与字体、内存与 Worker、SSR、传输方案、测试与性能治理,以及 SPA 导航、第三方脚本和音视频性能。

性能面试最看重“测量—定位—优化—验证”的闭环。回答不要先背优化清单,应先明确用户场景、指标和瓶颈证据。

Q1: 性能数据为什么要按页面、设备、网络和版本分群?

答案

全站平均值会把真正受影响的人群稀释掉。例如高端设备占比很大时,平均 INP 看起来正常,但低端 Android 用户可能持续卡顿;某个版本或路由的回归也可能被其他页面流量掩盖。

分群时选择能指导行动的低基数字段:页面模板、发布版本、设备档位、网络类型、地区和缓存状态等。先用整体 P75 观察趋势,再下钻定位,避免组合维度无限增长导致监控成本和噪声失控。

分群结果必须结合样本量、采样策略和业务指标解释。样本太少时不要轻易下结论,必要时延长观察窗口或做受控实验。

Q2: Core Web Vitals 主要衡量什么?

答案

Core Web Vitals 是 Google 提出的核心用户体验指标:

指标全称衡量维度良好标准
LCPLargest Contentful Paint加载性能≤ 2.5s
INPInteraction to Next Paint交互响应性≤ 200ms
CLSCumulative Layout Shift视觉稳定性≤ 0.1
// 使用 web-vitals 库测量
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(metric => console.log('LCP:', metric.value));
onINP(metric => console.log('INP:', metric.value));
onCLS(metric => console.log('CLS:', metric.value));

Q3: 实验室数据和 RUM 有什么区别?

答案

实验室数据在可控环境中复现,适合调试和回归;RUM 来自真实用户,能反映设备、网络和交互差异,但噪声更大。

两者互补:用 RUM 找受影响人群和页面,用实验室工具定位原因,再回到 RUM 验证收益。

  • 实验室数据在固定设备和网络下可重复,适合开发阶段定位与回归;RUM 来自真实用户,能反映设备、地域、缓存和交互差异。
  • 实践中用实验室工具解释“为什么慢”,用 RUM 的 P75 判断“用户是否真的慢”,并按页面、版本、设备分群。

Q4: LCP 慢应该怎么排查?

答案

先确认真正的 LCP 元素,再把时间拆成服务端响应、资源发现、下载和渲染延迟。

  • HTML 慢就优化后端、缓存和 CDN。
  • 主图发现晚就调整优先级、预加载和响应式资源。
  • 渲染晚则检查 CSS/JS 阻塞、字体、主线程长任务和客户端数据依赖。

Q5: INP 差的常见原因是什么?

答案

事件处理过长、主线程已有长任务、渲染更新范围过大和交互后的布局/绘制昂贵都可能拉高 INP。

用 Performance 找最慢交互的输入延迟、处理时间和呈现延迟,拆分长任务、缩小状态更新、减少同步布局,并为耗时工作提供及时反馈。

  • INP 包含输入延迟、事件处理时间和下一帧呈现延迟。长任务、同步渲染、大量事件回调和布局抖动都会拉高它。
  • 优化时先用 Performance 面板找到最长交互,再拆分长任务、减少同步工作,并用 scheduler.yield、Worker 或并发渲染让出主线程。

Q6: CLS 应该如何优化?

答案

为图片、视频、广告和异步组件预留稳定空间;字体设置合理回退并控制交换;避免在现有内容上方无提示插入元素。

用户触发的合理位移与意外位移要区分。通过真实用户元素归因找到主要来源,而不是只在一个页面刷新观察。

  • 给图片、视频、广告和异步组件预留稳定尺寸;字体使用接近的后备字体和正确加载策略,避免换字造成位移。
  • 不要在已有内容上方无提示插入模块。动画优先使用 transform,定位 CLS 来源时结合 Layout Shift 记录查看发生位移的节点。

Q7: 首屏白屏应该如何分层排查?

答案

按 HTML 是否返回、关键资源是否成功、脚本是否执行、框架是否挂载、接口是否阻塞渲染和错误降级是否可见逐层检查。

建立首屏阶段埋点和全局错误上报,并让壳层能在脚本失败时展示可恢复提示,不能只依赖业务应用渲染错误页。

  • 先区分 HTML 是否到达、关键 CSS/JS 是否阻塞、主线程是否长任务、应用是否等待接口,以及首屏元素本身是否加载慢。
  • 用 Network 看 TTFB 和资源瀑布,用 Performance 看首次绘制和主线程,用错误监控确认是否启动失败;不要一上来只做压缩包体。

Q8: 什么是关键渲染路径?

答案

关键渲染路径是浏览器将 HTML、CSS、JS 转换为屏幕像素的步骤。

优化策略

步骤优化方法
DOM减少 HTML 嵌套,精简标签
CSSOM内联关键 CSS,异步加载非关键 CSS
JavaScriptdefer/async,代码分割
Render Tree减少渲染阻塞资源
Layout/Paint避免强制同步布局

Q9: JavaScript 体积应该如何优化?

答案

先用 Bundle Analyzer 找大模块和重复依赖,再做按路由/功能拆分、Tree Shaking、替换重依赖、减少 polyfill 和延迟第三方脚本。

体积只是输入,解析、编译和执行成本同样重要。拆得过碎会增加请求和调度开销,需要结合缓存命中评估。

Q10: 代码分割和懒加载有哪些陷阱?

答案

按用户路径拆分能减少首屏包,但过度拆分会产生请求瀑布、加载闪烁和错误恢复问题。

对高概率下一步资源做适度预取,提供稳定 fallback 和重试,发布时保证旧 HTML 引用的 chunk 在缓存周期内仍可访问。

  • 拆分粒度过细会增加请求、模块初始化和调度成本;入口太多还可能让多个路由重复下载公共依赖。
  • 应按用户路径和变更边界拆分,预加载高概率下一步资源,并为懒加载失败提供重试和错误 UI,避免首屏只剩加载骨架。

Q11: 图片优化有哪些关键点?

答案

选择合适尺寸和 AVIF/WebP 等格式,使用 srcset/sizes,非首屏懒加载,首屏主图正确设置优先级并使用 CDN 转换。

同时设置宽高减少 CLS,控制解码和占位。不要为了体积把图片压到影响业务质量。

  • 格式上优先 AVIF/WebP 并保留回退,尺寸上用 srcset/sizes 让浏览器选择合适资源,首屏主图设置高优先级。
  • 同时压缩质量、移除元数据、使用 CDN 裁剪,并写明宽高减少 CLS;非首屏图片再使用懒加载。

Q12: 长列表为什么要虚拟化?

答案

大量 DOM 会增加创建、样式、布局、绘制和内存成本。虚拟列表只渲染视口附近项目,用占位尺寸保持滚动范围。

难点在动态高度、滚动定位、焦点、搜索和可访问性。数据分页与 DOM 虚拟化解决的是不同层问题,通常要配合。

  • 虚拟列表只渲染可视区和少量 overscan,把 DOM 数量从总数据量降到与视口相关。固定高度实现简单,动态高度需要测量缓存和滚动位置补偿。
  • 还要处理键盘导航、搜索定位、粘性元素和可访问性;数据量不大时普通分页往往更简单。

Q13: 如何优化频繁渲染和布局抖动?

答案

减少无关状态传播,批量 DOM 读写,避免循环中读布局后立刻写样式造成强制同步布局。

动画优先 transform/opacity,复杂计算移出渲染路径。框架项目先用 Profiler 找组件更新来源,不要全局盲目 memo。

  • 频繁交替读取布局属性和写样式会让浏览器反复强制布局。应先批量读取,再集中写入,并在一帧内合并更新。
  • 能用 transform/opacity 完成的动画尽量避免改变几何属性;复杂页面可用 contain、分层和虚拟化缩小布局影响范围。

Q14: Web Worker 能解决哪些性能问题?

答案

Worker 适合把纯计算或可独立处理的任务移出主线程,例如大文件 hash、数据解析、压缩、图像处理和复杂规则计算,从而降低长任务与 INP 风险。

但它不能直接操作 DOM,通信还会有启动、序列化和数据复制成本。大数据优先使用 Transferable 转移所有权;只有高频共享数据且能满足跨源隔离时才考虑 SharedArrayBuffer。网络请求本身是异步的,单纯把 fetch 放进 Worker 通常收益有限。

Q15: 字体加载如何优化?

答案

减少字体家族、字重和字符范围,使用现代压缩格式与子集化,设置合理缓存和跨域,选择合适 font-display

通过相似回退字体和 size-adjust 等方式降低切换偏移。关键字体是否预加载要以瀑布图证明,避免抢占主图带宽。

  • 预加载首屏必要字体并做子集化,使用 WOFF2、合理缓存和 font-display 控制文本可见策略。
  • 后备字体应通过 size-adjust 等指标接近 Web Font,降低 FOUT 带来的 CLS;不要预加载页面并未立即使用的所有字重。

Q16: SSR、SSG 和 CSR 对性能的影响怎么比较?

答案

不能只回答“SSG 最快、SSR 次之、CSR 最慢”。SSG 在构建时生成内容,缓存简单但数据新鲜度和构建规模受限;SSR 在请求时生成 HTML,能较早展示个性化内容,但 TTFB、服务容量和 Hydration 都可能成为瓶颈;CSR 的静态壳可缓存,复杂登录态应用也可能更合适,但首屏依赖更多 JavaScript。

实际项目往往按路由混用,并结合流式渲染、局部缓存、服务端组件或选择性 Hydration。选择时看 SEO、个性化、更新频率、首屏内容、交互成本和运维能力,而不是套一个全站模式。

Q17: 如何处理大文件上传和下载?

答案

上传采用分片、并发控制、断点续传、哈希/服务端校验和失败重试;下载用流式响应、Range 请求或对象存储直传,避免前端一次性把全部数据放内存。

还要处理鉴权、过期签名、限速、取消、进度和幂等合并,不能只实现前端切片。

  • 上传采用分片、并发上限、失败重试和断点续传,服务端用文件哈希或上传会话校验完整性;超大文件优先签名直传对象存储。
  • 下载使用流式响应、Range 和 CDN,前端避免把整文件一次性读进内存。进度展示要区分网络传输与服务端合并处理。

Q18: 什么是性能预算?

答案

性能预算把可接受的包体、关键请求、Web Vitals 或业务交互时延设成可监控门槛,并在 CI 和线上持续检查。

预算应按页面和设备制定,超过时要求说明或阻断发布。它的价值是防止性能在多次小改动中持续退化。

  • 预算应落到可自动检查的指标,例如首屏 JS、图片总量、LCP、INP 和关键接口延迟,并按页面类型制定。
  • CI 阻止明显回归,线上 RUM 检查真实用户分位数;预算不是一次性目标,业务增长时需要通过数据评审调整。

Q19: 为什么 Core Web Vitals 通常看第 75 百分位,而不是平均值?

答案

平均值会被大量高速设备“稀释”,看不出较慢用户的真实体验;P75 表示至少 75% 的访问达到这个值或更好,在代表性和尾部体验之间做了平衡。

统计时还要按移动端/桌面端、页面类型、地区和版本分群,并保证样本量与时间窗口足够。不能把不同路由混成一个全站数字,也不能用实验室一次测试代替字段 P75。

Q20: 如何区分“首屏慢”和“应用启动失败”?

答案

首屏慢最终会成功,只是 HTML、资源、接口或主线程耗时高;启动失败则是 chunk 404、脚本异常、配置错误、Service Worker 版本不一致或根组件没有挂载,等待再久也不会恢复。

我会记录 HTML 到达 -> 关键资源完成 -> bootstrap 开始 -> 框架挂载 -> 首屏可见 的阶段埋点,并关联资源错误、全局异常和发布版本。壳层要有独立于业务 bundle 的超时提示、刷新或回滚入口,不能只展示永远转动的 Loading。

Q21: 如何实现秒传?

答案

秒传是服务端已有同内容对象时复用。客户端提交 hash 和大小只用于查找候选,服务端仍要验证当前用户是否有权限引用,避免通过 hash 探测文件或跨租户越权。

MD5 可用于非安全去重兼容,但抗碰撞安全语义应选择 SHA-256 或存储服务支持的 checksum。是否允许跨用户去重还要考虑加密、隐私和删除语义。

Q22: 如何分析一个网站的性能?

答案

先问清页面、设备、地区和用户任务,再建立字段基线;工具只是定位证据的手段。

  1. 用 RUM 的 LCP、INP、CLS 和业务阶段找到慢页面与人群。
  2. 用 Network/Navigation Timing 拆 HTML、关键资源、缓存和请求瀑布。
  3. 用 Performance 分析长任务、交互三阶段、样式、布局和绘制。
  4. 用 Lighthouse/Insights 做可重复诊断,用 Coverage、bundle analyzer 找未使用与重复代码。
  5. 改完在同条件实验室复测,并回到线上 P75、错误率与业务指标验证。

Q23: 如何选择图片格式?

答案

场景推荐格式原因
照片AVIF / WebP / JPEG同等视觉质量下实测体积、编码与解码成本
透明图AVIF / WebP / PNG前两者通常更小,PNG 可做无损或兼容兜底
图标SVG矢量无损,可缩放
简单动画WebP > GIF更小,更多色彩
复杂动画MP4/WebM视频比 GIF 小很多
<!-- 最佳实践:格式降级 -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="图片">
</picture>

Q24: 如何优化首屏加载时间?

答案

// 1. 减少关键资源
// - 内联关键 CSS
// - defer/async JS
// - 代码分割

// 2. 减少资源大小
// - 压缩(Gzip/Brotli)
// - Tree Shaking
// - 图片优化

// 3. 提前加载
// - dns-prefetch
// - preconnect
// - preload 关键资源

// 4. 架构优化
// - SSR/SSG
// - 骨架屏
// - 流式渲染

回答时要先按 LCP 四阶段定位:HTML 慢先处理 TTFB,关键图发现晚再处理 HTML 可发现性和优先级,下载慢再做尺寸/格式/CDN,渲染晚则查主线程与客户端数据依赖。SSR、骨架屏和 preload 都是有条件的手段,不应一次全上。

Q25: Tree Shaking 的原理和条件?

答案

原理:基于 ESM 的静态分析,标记未使用的 export,在压缩阶段移除。

生效条件

  1. 使用 ESM(import/export)语法
  2. 生产模式构建
  3. 配置 sideEffects
  4. 使用支持的压缩工具
// ✅ 可 Tree Shaking
import { debounce } from 'lodash-es';

// ⚠️ CommonJS 的动态特性通常会限制精确 Tree Shaking
const _ = require('lodash');
import _ from 'lodash';

Q26: 什么是重排和重绘?如何避免?

答案

概念说明触发条件性能影响
重排重新计算布局尺寸、位置变化
重绘重新绘制外观颜色、背景变化
合成组合已有图层transform、opacity 常有机会只触发合成通常较低,但取决于图层和设备

避免方法

  • 批量修改 DOM
  • 使用 CSS 类
  • 避免读取触发重排的属性
  • 使用 transform/opacity 做动画
  • 只对已证实的热点短期使用 will-change,避免创建过多图层

Q27: 常见的内存泄漏场景有哪些?

答案

场景原因解决方案
全局变量忘记声明使用严格模式
定时器未清除clearInterval/clearTimeout
事件监听未移除removeEventListener
闭包持有大对象只保留必要数据
DOM 引用保留已删除 DOM清空引用
未完成请求fetch/XHR 未取消AbortController

Q28: 什么是虚拟滚动?原理是什么?

答案

虚拟滚动是一种只渲染可见区域 DOM 元素的技术。

原理

  1. 计算可视区域能显示多少条数据
  2. 根据滚动位置计算当前应该渲染哪些数据
  3. 使用占位元素撑起滚动条高度
  4. 用 CSS transform 定位实际渲染的列表
// 核心计算
const visibleCount = Math.ceil(containerHeight / itemHeight);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
const offsetY = startIndex * itemHeight;

Q29: 什么是 FOIT 和 FOUT?如何解决?

答案

问题描述解决方案
FOIT字体加载时文本不可见font-display: swap
FOUT后备字体闪烁为 Web 字体优化后备字体匹配
/* 解决 FOIT */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* 立即显示后备字体 */
}

/* 减少 FOUT 闪烁 */
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 105%; /* 调整后备字体大小 */
}

Q30: CSS 动画和 JS 动画的区别?

答案

特性CSS 动画JS 动画
调度方式浏览器管理时间线;是否只合成取决于属性脚本或 WAAPI 驱动;也可能走合成
性能简单状态过渡通常更省心,但不一定脱离主线程控制灵活,回调和布局读写不当会阻塞
控制性适合声明式 transition/keyframes适合手势、物理效果和复杂时间线
选择依据动画属性、可取消性和实测同左

使用建议

  • 简单动画 → CSS transition/animation
  • 复杂交互 → WAAPI 或 framer-motion
  • 游戏/高频更新 → Canvas/WebGL

Q31: Web Worker 有什么限制?

答案

限制说明
无法访问 DOM不能操作 document、window
无法访问 window 对象使用 self 代替
加载与安全限制受 CSP、脚本类型、CORS/同源策略影响
无法使用部分 API如 alert、confirm
文件限制不能访问本地文件系统
// Worker 中可用的 API
self.fetch()
self.indexedDB
self.caches
self.WebSocket
self.crypto
self.performance

Q32: 流式 SSR 能解决什么,不能解决什么?

答案

流式 SSR 可以先发送页面壳和已就绪内容,让慢数据边界稍后到达,从而改善“等所有接口完成才返回 HTML”的问题,也能与 Suspense 边界配合分阶段展示。

它不能自动减少客户端 JavaScript、Hydration 和主线程成本;边界切得过碎还会增加闪烁与状态复杂度。要同时设计错误边界、加载占位、缓存、代理缓冲和客户端接管顺序,并用 LCP、INP 与可恢复性验证。

Q33: Lighthouse 评估哪些维度?

答案

维度说明核心指标
Performance页面性能FCP、LCP、TBT、CLS、SI
Accessibility可访问性颜色对比度、ARIA 标签
Best Practices最佳实践HTTPS、无控制台错误
SEO搜索优化meta 标签、语义化
PWA(如当前版本提供独立审计)渐进式能力manifest、离线与安装相关检查

Lighthouse 的类别、审计、权重和评分曲线会随版本变化,PWA 也不应背成永远固定的第五个类别。面试时先说明所用版本与配置,并强调分数是实验室诊断入口,不是线上用户体验本身。

Q34: 性能预算为什么要同时有绝对阈值和相对回归阈值?

答案

绝对阈值守住体验底线,例如关键路由的资源上限;相对阈值能发现“还没越线但这次明显变差”的小回归。只有绝对值会让预算内持续变慢,只有相对值又可能让一个本来就很慢的页面一直维持现状。

CI 中还要固定设备、网络、缓存和旅程,重复运行后用中位数或稳健统计比较主分支。体积等确定性指标可以硬阻断,噪声较大的实验室指标先分级告警,并保留有 owner 和到期日的例外流程。

Q35: 如何实现一个简单的性能监控上报?

答案

import { onCLS, onINP, onLCP } from 'web-vitals/attribution';

function report(metric: unknown): void {
const body = JSON.stringify(metric);
if (!navigator.sendBeacon('/rum', body)) {
fetch('/rum', { method: 'POST', body, keepalive: true }).catch(() => {});
}
}

onLCP(report);
onINP(report);
onCLS(report);

SDK 之外还要记录路由、版本、navigation type 和必要的 attribution,并做采样、脱敏、限基数与去重。服务端按页面和设备聚合 P75,不能根据单个样本告警。

Q36: 骨架屏和 Loading 动画有什么区别?应该选择哪个?

答案

对比项骨架屏Loading 动画
视觉效果页面布局占位简单的转圈/进度条
用户感知页面"正在加载内容"页面"正在处理"
心理效果感知等待时间更短可能产生焦虑
实现复杂度较高简单
适用场景内容型页面简单操作、提交表单

选择建议

  • 首屏加载:使用骨架屏(给用户"即将看到内容"的预期)
  • 按钮提交:使用 Loading(短暂操作)
  • 列表加载更多:使用骨架屏(与已有内容风格统一)

骨架屏主要改善等待反馈,不会让数据真正更快。骨架尺寸必须接近最终内容以避免 CLS;未知时长操作应给阶段提示和取消能力,长任务最好展示真实进度。

Q37: 如何保证分片的顺序和完整性?

答案

  1. 顺序:每个分片带有 index 标识,合并时按顺序读取
  2. 完整性:对每个分片记录大小与 checksum,完成时验证完整分片集合和整体 checksum
  3. 幂等:同一 uploadId + part number 重试返回同一结果,完成接口也要幂等
  4. 安全:上传会话与用户绑定,过期分片可清理,不能信任客户端文件名和类型

Q38: 如何判断性能瓶颈在服务端、网络还是前端运行时?

答案

先用时间线把问题分段:Navigation Timing 和 Server-Timing 看 DNS、连接、TTFB 与后端阶段;Network 瀑布看资源发现、排队、缓存和下载;Performance 看解析、脚本、布局、绘制与长任务。

再做交叉验证:接口慢可看服务端 trace,下载慢按地区/CDN/协议分群,主线程忙则看调用栈和交互阶段。不要仅凭 TTFB 高就断言数据库慢,也不要看到大 bundle 就假设它是当前 LCP 的主因。

Q39: 图片懒加载的实现方式?

答案

// 方式1:原生 loading 属性(推荐)
<img src="image.jpg" loading="lazy" alt="">

// 方式2:Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
observer.unobserve(img);
}
});
});

document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});

原生 loading="lazy" 是默认选择;只有需要自定义提前距离、动画或兼容复杂容器时才用 IntersectionObserver。LCP/首屏关键图不能懒加载,并应写明宽高;Observer 方案还要更新 srcset、解绑观察和处理加载失败。

Q40: 浏览器预加载扫描器是什么,为什么资源仍可能发现得很晚?

答案

预加载扫描器会在主解析器受脚本等因素阻塞时,提前扫描 HTML 中可发现的图片、脚本和样式引用,尽早发起请求。

资源如果藏在客户端渲染结果、CSS 背景、运行时拼接 URL 或接口响应里,扫描器就无法早发现。关键 LCP 图应尽量出现在初始 HTML,并使用正确的 srcset/sizes 与优先级;必要时才 preload,避免与 CSS、字体等资源争抢带宽。

Q41: 如何分析和优化打包体积?

答案

// 1. 分析
// npx vite-bundle-visualizer
// npx webpack-bundle-analyzer

// 2. 常见优化
const optimizations = {
// 代码分割
splitChunks: '按路由/功能分割',

// 按需加载
lazyLoad: '动态 import',

// 依赖优化
dependencies: {
'lodash → lodash-es': '支持 Tree Shaking',
'moment → dayjs': '更小体积',
'antd': '按需加载组件'
},

// 外部化
externals: 'React/Vue 等用 CDN',

// 压缩
compress: 'Gzip/Brotli'
};

优化顺序是先看路由入口、重复版本、locale/polyfill、客户端边界和实际执行成本,再决定删除、替换或分包。不要只追求 chunk 小:过度分包会产生交互瀑布和缓存失效;外部 CDN 也会增加连接、可用性和安全治理成本。

Q42: 如何优化动画性能?

答案

/* 1. 使用 transform 而非 left/top */
.animate {
transform: translateX(100px);
}

/* 2. 使用 opacity 而非 visibility */
.fade {
opacity: 0;
}

/* 3. 只有分析确认后,短期提示即将变化的属性 */
.about-to-animate {
will-change: transform;
}
// 4. 使用 requestAnimationFrame
function animate(now: number) {
// 根据时间戳计算进度,兼容 60Hz/120Hz 等刷新率
updateByTimestamp(now);
requestAnimationFrame(animate);
}

// 5. 使用 Web Animations API
element.animate([...keyframes], options);

Q43: 如何检测内存泄漏?

答案

先定义可重复旅程,例如反复进入详情并返回。预热后记录基线,重复操作并回到相同状态,观察 JS heap、DOM Nodes 和总内存是否回落。

确认趋势后用 Heap Snapshot 的 Comparison、retained size 和 Retainers 找强引用链;Detached Elements 查游离 DOM,Allocation Timeline 查持续分配位置。单次 performance.memory 上涨可能只是尚未 GC,且它不是跨浏览器标准,不能直接判定泄漏。

Q44: 定高和不定高虚拟列表有什么区别?

答案

特性定高列表不定高列表
高度固定动态
索引查找O(1),直接计算O(log n),二分查找
实现复杂度简单复杂
性能更好较好
准确性精确需要测量修正

不定高列表需要:

  • 预估高度
  • 渲染后测量实际高度
  • 缓存高度信息
  • 更新后续项的位置

Q45: font-display 各值的区别?

答案

阻塞期行为适用场景
block较短阻塞期、较长交换期先隐藏,之后仍可交换特殊品牌场景,谨慎使用
swap很短或无阻塞期立即后备,完成后可交换强调正文立即可读
fallback很短阻塞期、有限交换期减少很晚的字体切换平衡可读与稳定性
optional很短阻塞期、无交换期浏览器可决定不使用 Web 字体弱网可放弃的装饰字体

具体时长由浏览器实现决定。swap 也可能因回退字体指标不同引发 CLS,应配合 size-adjust 等度量覆盖并实测。

Q46: 什么是 GPU 加速?哪些属性可以 GPU 加速?

答案

“GPU 加速”不是给元素加一个属性就免费变快。浏览器可能把元素提升为合成层,使 transform、opacity 等变化跳过部分布局和绘制,但层创建、纹理上传、显存和合成也有成本。

/* 这些属性常有机会只触发合成,但要以 DevTools 验证 */
transform: translate/scale/rotate
opacity

/* 只在即将动画且确有收益时短期使用 */
.element {
will-change: transform;
}

Q47: 如何实现主线程和 Worker 的通信?

答案

// 1. postMessage + onmessage
// 主线程 → Worker
worker.postMessage(data);
// Worker → 主线程
self.postMessage(result);

// 2. Transferable Objects 高效传输
const buffer = new ArrayBuffer(1000);
worker.postMessage(buffer, [buffer]); // 转移所有权

// 3. SharedArrayBuffer 共享内存
const sab = new SharedArrayBuffer(1024);
const arr = new Int32Array(sab);
worker.postMessage({ buffer: sab });
// 两边都可以读写 arr

结构化克隆不是简单“深拷贝”所有值,函数、DOM 节点等不能直接传。Transferable 转移后发送方的原 buffer 会分离;SharedArrayBuffer 需要跨源隔离,并必须用 Atomics 或明确协议避免竞态。

Q48: Hydration 为什么会成为 SSR 页面的性能瓶颈?

答案

服务端 HTML 让内容先可见,但客户端仍要下载模块、创建组件树、绑定事件并核对现有 DOM。大组件树、同步初始化、重复取数和第三方脚本会让 Hydration 形成长任务,出现“看得见但点不动”。

优化方向是减少客户端边界与 JavaScript、把非交互内容留在服务端、按区域或优先级 Hydrate、拆分长任务,并避免服务端/客户端输出不一致。最终用交互阶段的 INP 与主线程 trace 验证,而不是只看 HTML 到达。

Q49: 如何提高 Lighthouse Performance 分数?

答案

先固定 Lighthouse 版本、设备、网络和运行次数,再从具体审计与 trace 找证据:LCP 看四阶段,TBT 看启动期长任务,CLS 看位移节点。分数和权重会变化,不应围绕分数做无意义技巧;修复后还要看线上 RUM 和关键业务任务是否改善。

Q50: 如何在 CI 中稳定执行性能预算?

答案

先固定运行环境、页面数据、设备/网络配置与缓存状态,并对每条关键旅程重复运行;用中位数等稳健统计同时比较绝对上限和同环境主分支。

  • 产物体积、重复依赖和静态规则较确定,可直接阻断。
  • Lighthouse/LCP 等有噪声,设置波动带,先告警后升级为门禁。
  • 失败报告要展示本次、基线、阈值和构建差异。
  • 例外必须有工单、owner、补偿措施和到期日,避免永久放宽。

Q51: sendBeacon 和普通 AJAX 有什么区别?为什么性能上报要用它?

答案

特性sendBeaconAJAX (XMLHttpRequest/fetch)
页面隐藏时排队发送更适合小型遥测,但不保证服务端已接收普通请求可能被页面生命周期取消;keepalive 可改善
阻塞页面关闭❌ 异步,不阻塞同步会阻塞
返回值仅返回是否成功加入队列返回完整响应
请求方式仅 POST支持所有方法

为什么用 sendBeacon

  1. 页面隐藏时更适合把小型数据交给浏览器排队发送
  2. 不阻塞页面卸载流程,不影响用户体验
  3. 不需要等待响应,适合可丢失、可批量的小型遥测
// 页面隐藏时使用 sendBeacon
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
const data = new Blob([JSON.stringify(metrics)], { type: 'application/json' });
navigator.sendBeacon('/api/report', data);
}
});

Q52: 一个项目可以混用 CSR、SSR 和 SSG 吗?

答案

可以,而且通常应按路由甚至按组件边界选择。营销页和文档可静态生成,个性化详情可 SSR,登录后的重交互区域可 CSR;同一页面还可用服务端渲染静态内容,只让小块交互组件进入客户端。

需要统一处理缓存、鉴权、数据一致性、错误边界和监控,避免同一份数据在服务端和客户端重复请求。选型目标是让内容尽早到达,同时只下发必要的客户端 JavaScript。

Q53: 如果用户刷新页面,如何恢复上传?

答案

  1. 本地保存 uploadId、文件指纹、大小、修改时间和过期时间等轻量元数据。
  2. 刷新后网页通常不能自动重新获得本地文件,需用户重新选择并核对指纹。
  3. 向服务端查询已确认分片,以服务端为权威,只补传缺失部分。
  4. 会话过期或文件变化时重新创建会话,不能把本地记录当成最终状态。

Q54: srcset 和 sizes 属性的作用?

答案

  • srcset:提供不同尺寸/分辨率的图片候选
  • sizes:告诉浏览器图片在不同视口下的显示尺寸
<img
src="default.jpg"
srcset="
small.jpg 400w,
medium.jpg 800w,
large.jpg 1200w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
600px
"
alt="响应式图片"
>

浏览器会根据:

  1. 设备像素比(DPR)
  2. 视口宽度
  3. sizes 定义的显示尺寸

自动选择最合适的图片。

指标、监控与网络

Q55: Navigation Timing 和 Resource Timing 分别解决什么问题?

答案

Navigation Timing 描述一次文档导航,从跳转、DNS、连接、请求到 DOM 事件,适合拆 TTFB 和页面导航阶段;Resource Timing 描述脚本、样式、图片、字体等单个资源的发现、排队、连接、下载和缓存信息。

排查首屏时我会先用 Navigation Timing 判断 HTML 是否晚,再用 Resource Timing 找关键资源为什么晚。跨源资源若没有 Timing-Allow-Origin,很多详细字段会被隐藏,监控设计时要和 CDN 配置一起考虑。

Q56: TTFB 应该怎么计算,它高就一定是后端慢吗?

答案

页面导航中通常用 responseStart - startTime 表示从导航开始到收到首字节,不能只写 responseStart - requestStart,后者漏掉了重定向、DNS 和连接阶段。

TTFB 高可能来自重定向、DNS、TCP/TLS、CDN 未命中、网络 RTT、排队或后端处理。要结合 Navigation Timing、CDN 日志、Server-Timing 和服务端 trace 分段,不能直接归因数据库。

Q57: LCP 的四个阶段是什么?

答案

LCP 可拆为:TTFB、资源加载延迟、资源加载耗时、元素渲染延迟。

  • TTFB 高:查服务端、CDN 和缓存。
  • 加载延迟高:资源发现晚、优先级低或被懒加载。
  • 下载耗时高:尺寸、格式、网络或缓存有问题。
  • 渲染延迟高:主线程、CSS、字体、隐藏状态或客户端逻辑阻塞展示。

这个拆法比一句“压缩图片”更能说明你会定位问题。

Q58: 为什么页面加载过程中 LCP 元素会变化?

答案

浏览器会持续记录视口内最大的合格内容元素,后出现的标题、主图或海报可能超过之前候选,因此最终 LCP 往往不是最早看到的元素。

监控要在页面隐藏或交互等最终时机由成熟库上报,并保留元素归因。实验室里也要看完整 trace,避免只优化早期候选却没有改善最终 LCP。

Q59: INP 是不是所有交互延迟的平均值?

答案

不是。INP 观察页面生命周期中的点击、触摸和键盘等交互延迟,并以接近最慢交互、同时对异常值有一定处理的方式给出代表值;它不是简单平均数,也不是只看第一次输入。

因此优化不能只保证首个按钮快,要找最慢的交互类型、页面状态和设备分群。短会话交互样本少时,统计行为也要交给 web-vitals 等经过验证的实现。

Q60: 一次交互延迟可以拆成哪三部分?

答案

可以拆成输入延迟、处理时间和呈现延迟。

  • 输入延迟:事件发生后主线程何时有空开始处理。
  • 处理时间:相关事件回调执行多久。
  • 呈现延迟:回调结束到下一帧真正绘制多久。

输入延迟高要查前置长任务,处理慢要查业务回调,呈现慢则看框架提交、样式、布局和绘制。

Q61: Event Timing API 在性能排查中有什么价值?

答案

它提供交互事件的开始、处理和呈现相关时间,可用于把字段中的慢交互与具体事件、目标和页面状态关联。生产中通常通过 web-vitals/attribution 获得更稳定的 INP 归因。

要控制隐私与基数:不要直接上传完整 DOM、用户文本或动态 URL;应转换为稳定组件名、路由模板和受控事件类型。

Q62: Long Task 和 Long Animation Frame 有什么区别?

答案

Long Task 关注主线程单个任务是否长时间占用,适合发现脚本阻塞;Long Animation Frame 更贴近一帧为什么超过预算,能串起脚本、样式和布局等工作。

排查 INP 时两者互补:前者看谁长期占着主线程,后者看交互后的这一帧为什么晚。不能只删除一个红色长任务就假设呈现延迟也解决了。

Q63: TBT 和 INP 能互相替代吗?

答案

不能。TBT 是实验室中 FCP 到指定阶段之间长任务超出 50ms 部分的累计值,适合诊断启动期主线程阻塞;INP 是字段交互响应指标,覆盖用户真实交互。

降低 TBT 往往有助于响应性,但页面也可能启动很轻、某个后续操作却很慢。CI 可用 TBT 做代理门禁,最终仍要看 RUM INP 和具体交互。

Q64: CLS 为什么不能把所有 layout-shift 值无限累加?

答案

当前 CLS 使用会话窗口思想,把相邻且时间接近的位移归组,并取影响最大的窗口,避免长时间打开页面因零散小位移无限累加。

自己手写简单求和容易算错,还会漏掉用户输入排除等规则。生产应使用 web-vitals,排查时再看窗口内的位移来源和受影响节点。

Q65: SPA 的软导航性能应该怎么监控?

答案

先定义软导航边界:路由开始、数据就绪、主要内容可见和交互完成,并用 User Timing 或框架路由钩子记录。传统 Navigation Timing 只覆盖文档导航,不能天然代表每次客户端路由。

软导航相关浏览器能力仍在演进,生产方案要做特性检测,并保留业务级“路由内容可见”指标。不要把一次页面生命周期的 LCP 机械重置成每个路由的官方 LCP。

Q66: BFCache 对性能和代码生命周期有什么影响?

答案

BFCache 会把完整页面冻结并在前进后退时快速恢复,通常比重新加载快很多。恢复不是一次新导航,原有内存和状态可能继续存在。

代码要监听 pageshow/pagehide 并检查 persisted,避免恢复时重复注册监听、计时器或监控。依赖 unload 还可能让页面失去 BFCache 资格,应通过浏览器诊断工具查看阻塞原因。

Q67: Prerender 与普通 prefetch 有什么不同?

答案

prefetch 主要提前获取可能用到的资源;prerender 会在隔离环境中提前加载甚至渲染整个页面,命中后导航非常快,但资源、隐私和副作用成本更高。

只对高概率、安全且可撤销的导航使用,不能在预渲染阶段发送不可逆请求或误记曝光。需要处理激活时机、鉴权变化和未命中的浪费。

Q68: Resource Timing 中 transferSize 为 0 一定代表缓存命中吗?

答案

不一定。可能是缓存命中,也可能是跨源时序信息受限、Service Worker、资源协议或浏览器实现造成字段不可用。

判断缓存要结合 decodedBodySizeencodedBodySize、响应头、DevTools 的缓存来源和服务端日志。线上监控更适合统计趋势,不要根据一个字段给每个请求下绝对结论。

Q69: Server-Timing 有什么用?

答案

服务端可以通过响应头暴露受控阶段,例如网关、应用、缓存和数据库耗时,浏览器会把它关联到对应导航或资源时序。

它能把“TTFB 高”继续拆到后端内部,但字段必须稳定、低基数且不泄露敏感实现。最好与 trace ID 关联,让前端慢样本能跳转到服务端链路。

Q70: User Timing API 适合记录哪些业务性能?

答案

它适合浏览器没有标准指标的业务阶段,例如编辑器可用、搜索结果渲染、图表完成和路由内容稳定。用 performance.mark 标记边界,再用 performance.measure 得到时长。

命名要版本化且可聚合,起止点语义必须稳定。记录“Promise resolve”不一定等于用户看见结果,最好把终点放在 DOM 提交和下一次绘制之后验证。

Q71: RUM 应该全量上报吗?

答案

通常不必。核心 Web Vitals 数据很小,但资源明细、长任务和归因可能量大,应按流量、页面价值和问题状态分层采样。

核心指标可较高采样,详细诊断只对慢样本或特定版本采样,并设置每页上限。采样规则要随 payload 上报,聚合时才能校正偏差。

Q72: 性能监控为什么要控制字段基数?

答案

完整 URL、DOM 选择器、错误文本和用户 ID 会产生近乎无限维度,导致存储、查询和告警成本失控,也带来隐私风险。

应使用路由模板、组件 ID、资源类型和版本等有限枚举;动态参数做哈希或丢弃。任何可能包含用户输入的字段先脱敏,再配置长度和数量上限。

Q73: 线上性能告警应该怎样避免噪声?

答案

不要按单个慢样本报警。先保证最小样本量,再按页面、设备、地区和版本比较滚动窗口 P75、历史基线和发布前后变化。

告警要有持续时间、变化幅度和严重级别,并关联发布标记。流量突变或监控 SDK 版本变化也可能改变分布,需要在告警上下文展示。

Q74: 如何把线上慢样本带回本地复现?

答案

RUM 中保留可控的环境信息:路由模板、版本、设备档位、网络类型、navigation type、慢交互组件和 LCP 资源类型,再在实验室构造相近条件。

如果无法完全复现,用字段分群和服务端 trace 缩小范围,而不是伪造精确用户环境。严禁为了复现上传敏感页面内容或完整 DOM。

Q75: PerformanceObserverbuffered: true 有什么作用?

答案

监控脚本可能在某些性能条目已经产生后才加载,buffered: true 允许观察者获取缓冲区中已有的同类条目,减少漏报首屏指标。

它不代表浏览器会无限保存所有条目,资源缓冲区仍有容量限制。SDK 还要处理不支持的 entry type、重复注册和页面生命周期结束上报。

Q76: Resource Timing 缓冲区满了怎么办?

答案

资源很多时浏览器的 timing buffer 可能满,后续条目无法保留。可以监听缓冲区满事件、及时消费和聚合,或在确有需要时通过 setResourceTimingBufferSize 调整容量。

不要无上限增大并上报所有明细。更好的做法是只保留关键资源、慢资源和汇总统计,并对第三方 URL 做脱敏。

Q77: Timing-Allow-Origin 为什么重要?

答案

跨源资源默认不会向页面暴露完整 Resource Timing 细节,以防信息泄露。资源服务通过 Timing-Allow-Origin 明确允许后,页面才能看到更完整的下载与连接阶段。

CDN 字体、图片和脚本若要做字段诊断,需要正确配置该响应头;允许范围应按实际站点控制,不能为了监控无脑开放敏感资源。

Q78: CDN 命中率高,为什么用户仍可能觉得慢?

答案

CDN 命中只说明边缘有对象,不代表用户到边缘 RTT 低、HTML 快、资源优先级正确或主线程轻。大文件、错误缓存键、跨区路由和客户端执行仍会慢。

要同时看边缘 TTFB、传输、缓存层级、LCP 四阶段和 JS 执行。优化目标是用户任务,不是单独把 hit ratio 做高。

Q79: 什么资源适合 Cache-Control: immutable

答案

内容哈希文件名且内容永不在原 URL 上变化的 JS、CSS、图片和字体适合长期缓存并加 immutable。发布新内容时换 URL,而不是覆盖旧文件。

HTML、运行时配置和没有版本化的接口不适合无条件长期 immutable,否则发布与回滚会读到旧内容。还要保证旧 chunk 在 HTML 缓存周期内继续可访问。

Q80: stale-while-revalidate 的收益和风险是什么?

答案

它允许缓存先返回过期内容,同时后台重新验证,能降低等待时间和源站压力。适合允许短暂陈旧的公共内容。

风险是用户可能看到旧数据,多个缓存层的陈旧窗口也会叠加。价格、权限、库存等敏感数据要明确一致性要求,并配合版本、刷新提示或更严格缓存策略。

Q81: HTTP/2 和 HTTP/3 下还需要合并资源吗?

答案

多路复用降低了多个请求的连接成本,但每个资源仍有请求头、优先级、调度、解析和缓存成本,过度碎片化依然会产生瀑布与主线程开销。

不应再为了 HTTP/1.1 机械做巨型 vendor 包,也不能走到“每个模块一个生产请求”的另一极端。按用户旅程、缓存变化频率和执行成本分包,再在目标协议与网络下实测。

资源加载、构建、图片与字体

Q82: dns-prefetchpreconnectpreloadprefetch 怎么选?

答案

  • dns-prefetch 只提前解析域名,成本最低。
  • preconnect 还建立连接,适合很快会请求的关键跨源。
  • preload 以较高优先级获取当前页面确定需要的资源,必须写对 as、类型和 CORS。
  • prefetch 面向未来导航的低优先级候选,是否执行由浏览器决定。

它们都会消耗资源,先从瀑布图证明“发现晚或建连慢”,不要把所有域名和 chunk 都提前。

Q83: fetchpriority="high" 应该用在哪里?

答案

常见场景是初始 HTML 中的关键 LCP 图片,帮助浏览器在多个图片候选中提高它的调度优先级。它是提示,不是保证,也不能解决资源发现太晚或文件过大。

一个页面不应有很多 high;优先级过多等于没有优先级,还可能挤压 CSS、字体和其他关键请求。设置后要在 Network 和 LCP 四阶段中验证。

Q84: preload 用错会有什么后果?

答案

预加载了未使用资源会浪费高优先级带宽;URL、astype 或 CORS 不一致可能导致浏览器再次请求;响应式图片没配 imagesrcset/imagesizes 还可能下载错误候选。

preload 适合当前导航确定且发现偏晚的关键资源。上线后监控 unused preload 警告、重复请求和 LCP 是否真的改善。

Q85: asyncdefer 和 module script 有什么区别?

答案

普通 async 下载完成就执行,顺序不稳定,适合独立脚本;defer 并行下载,在文档解析后按顺序执行;module script 默认具有类似 defer 的行为,并遵循模块依赖图。

第三方脚本即使 async,也会在执行时占主线程。选择属性只是控制发现和执行时机,仍要关注依赖顺序、执行成本、CSP 和错误隔离。

Q86: 浏览器资源优先级只由标签决定吗?

答案

不是。浏览器综合资源类型、所在位置、可见性、发现时机、preload、fetchpriority 和内部调度策略决定优先级,而且策略可能随浏览器演进。

开发者应表达意图并验证实际瀑布,不能把某个 DevTools 优先级标签当协议承诺。优化重点仍是让关键资源早发现、体积合理且数量受控。

Q87: Gzip 和 Brotli 应该怎么选?

答案

Brotli 通常对文本资源压缩更好,现代浏览器支持广泛;Gzip 可作为兼容回退。静态资源适合在构建或 CDN 预压缩,避免每次请求消耗 CPU。

图片、视频等已经压缩的格式通常不值得再压。缓存键要正确区分 Accept-Encoding,并同时记录原始、gzip 和 Brotli 口径,防止预算比较混乱。

Q88: 103 Early Hints 能解决什么问题?

答案

当最终 HTML 还在服务端生成时,服务器或边缘可先发送 Early Hints,让浏览器提前连接或加载确定的关键资源,降低资源发现等待。

它只适合非常稳定的关键资源,且要验证 CDN、代理和浏览器链路支持。错误提示会浪费带宽,不能替代降低后端 TTFB 和正确缓存。

Q89: Service Worker 缓存最常见的性能坑是什么?

答案

常见问题是旧 HTML 引用已删除的新旧 chunk、缓存键漏掉关键维度、安装阶段缓存过多、网络优先导致离线慢,以及更新后新旧页面同时运行。

要设计版本化、原子发布、清理策略和失败回退;监控响应来自网络还是 SW。不要用“缓存优先”覆盖所有请求,API、导航和静态资源的策略应不同。

Q90: 如何发现一个依赖被打进多个版本?

答案

从 bundle analyzer、source map 和包管理器依赖树交叉检查,关注同一库不同主版本、ESM/CJS 双份构建和 monorepo 链接包。

解决方式包括统一版本范围、调整 peerDependencies、去掉深层重复依赖或在可验证时做 alias。不能为了只剩一份强行提升不兼容版本,正确性优先。

Q91: 为什么 ESM 更利于 Tree Shaking?

答案

ESM 的导入导出结构可静态分析,工具能建立模块图、标记未使用导出,再由压缩器删除不可达代码。CommonJS 允许动态 require 和运行时修改导出,分析更困难。

这不是“CommonJS 完全不能优化”。打包器可能识别部分模式,但稳定效果取决于包的发布格式、副作用和工具链。

Q92: sideEffects: false 为什么可能删错代码?

答案

它表示包内模块可以在导出未使用时整模块删除。如果模块顶层注册 polyfill、导入 CSS、修改全局或执行注册逻辑,却错误声明无副作用,生产构建可能把必要行为删掉。

库作者应准确列出有副作用文件,应用方不要为追求体积随意覆盖。通过生产模式测试和构建差异验证,而不是只看 analyzer 变小。

Q93: 代码分割为什么可能让性能更差?

答案

过细分包会增加请求、模块解析和运行时调度,一次点击还可能形成 A -> B -> C 串行瀑布;公共依赖归属变化也会让大量缓存失效。

按路由和明确重功能设边界,对高概率下一步适度预取,并比较首次加载、交互后延迟和重复访问缓存。chunk 数不是优化目标。

Q94: 动态 import 的 chunk 404 应该怎么处理?

答案

常见原因是用户保留旧 HTML,发布后对应旧 chunk 已删除。首先要让带 hash 的旧产物至少保留一个 HTML 缓存周期,并保证发布原子性。

客户端可识别加载失败,有限重试或提示刷新,但不能无限刷新循环。Service Worker 项目还要协调新旧版本激活和缓存清理。

Q95: 生产 source map 会不会泄露源码?

答案

如果公开部署可访问的 source map,确实可能暴露源码、路径和注释。更常见做法是生成 map 后上传到错误监控系统,不在公共 CDN 暴露,或限制访问。

source map 也会增加构建时间和存储,但对定位线上错误与长任务很有价值。要管理版本、上传成功校验和源码中的秘密,秘密本就不应进入前端包。

Q96: 提升构建速度应该先做什么?

答案

先分别测冷启动、HMR、完整生产构建和 CI 各阶段,找出转译、类型检查、压缩、source map、插件还是 I/O 最慢。

再选择持久化缓存、缩小处理范围、增量类型检查、替换转译器或并行。多线程有启动与序列化成本,迁移 Vite 也有插件和 SSR 成本,都不是无条件开关。

Q97: 开发服务器启动快,是否说明生产性能好?

答案

不说明。开发模式关注按需转换、依赖预构建和 HMR;生产性能取决于最终分包、压缩、缓存、网络、解析和执行。

工具选型要分别设开发反馈和生产用户指标。Vite 等工具改善开发体验很有价值,但仍必须分析生产产物和 RUM。

Q98: 服务端组件或客户端边界为什么影响包体?

答案

被标记为客户端执行的边界及其可达依赖通常需要下发到浏览器。边界放得过高,会把本可只在服务端渲染的展示组件和依赖一起带入客户端包。

把交互状态下沉到最小必要区域,服务端负责数据和静态结构,并检查序列化边界。优化后还要验证交互是否需要重复请求和 Hydration 是否一致。

Q99: Polyfill 应该如何治理?

答案

先基于真实浏览器支持矩阵确定目标,而不是默认兼容所有历史环境;检查是否重复注入、是否能用按需或差异化加载,并优先使用已广泛支持的平台能力。

减少 polyfill 要配合监控旧浏览器失败率。语法转译、API polyfill 和 Node.js 浏览器 shim 是不同问题,应分别审计。

Q100: 第三方脚本怎样做性能治理?

答案

每个第三方必须有业务价值、owner、加载时机、超时、失败降级和隐私评估。能服务端完成的不要下发浏览器,非首屏脚本延后到同意或交互后。

用 Resource Timing、Long Task attribution、主线程 trace 和 RUM 评估传输与执行成本。iframe 可做一定隔离,但仍占网络、CPU 和内存,不能当免费沙箱。

Q101: AVIF 一定比 WebP 更好吗?

答案

不一定。AVIF 对很多照片压缩优秀,但编码耗时、解码成本、透明/动画能力和具体素材表现都不同;WebP、JPEG 甚至 PNG 在某些场景更合适。

应在同等视觉质量下比较代表性素材,并由 <picture> 或图片 CDN 提供候选。不要背固定“AVIF 小 50%”作为所有图片结论。

Q102: srcsetsizes 写错会发生什么?

答案

使用宽度描述符时,srcset 提供候选宽度,sizes 告诉浏览器图片预计显示宽度。若 sizes 总写 100vw,实际只占半屏,浏览器可能下载过大的图片。

应让 sizes 与响应式布局一致,并在不同 DPR、视口和缓存状态下检查实际选择。CSS 改布局时也要同步更新。

Q103: 为什么 LCP 图片不能懒加载?

答案

懒加载会推迟资源请求,直接增加 LCP 的资源加载延迟。首屏候选应在初始 HTML 可发现,通常使用 eager 默认行为,并在确认是关键图时设置高 fetch priority。

首屏以外图片再懒加载。不要给所有图片 high 或 preload,否则会争抢关键带宽。

Q104: decoding="async" 能保证图片更快显示吗?

答案

不能。它只是解码行为提示,可能减少同步解码对其他工作的阻塞,也可能让具体图片展示稍后。浏览器仍会结合自身策略处理。

高分辨率图片的解码与内存成本应通过 Performance 和设备测试验证,不能用一个属性替代正确尺寸与压缩。

Q105: 图片写 widthheight 为什么不会限制响应式布局?

答案

HTML 宽高属性主要提供固有宽高比,让浏览器在图片下载前预留空间;CSS 仍可用 width: 100%; height: auto 控制响应式尺寸。

只要属性比例与资源一致,就能显著降低 CLS。艺术指导使用不同比例图片时,要为各 source 或容器设计准确比例。

Q106: font-display: swapoptional 怎么选?

答案

swap 强调立即可读,先用回退字体,Web 字体到达后可交换;optional 允许浏览器在条件不佳时继续使用回退字体,减少很晚切换与额外下载。

正文、品牌标题和装饰字体需求不同。无论选哪个,都要用相近回退字体和度量覆盖降低 CLS,并在弱网测试。

Q107: 中文字体子集化最大的坑是什么?

答案

只扫描静态页面或只保留“常用 3500 字”,会漏掉用户昵称、地名、生僻字、动态接口内容和标点,最终出现局部字体回退。

应基于生产语料分片,保留必要 OpenType 特性,使用 unicode-range 按需加载,并始终提供可靠系统字体兜底。分片过多也会增加请求与缓存成本。

Q108: 可变字体一定比多个静态字体小吗?

答案

不一定。页面使用多个连续字重时,可变字体可共享轮廓数据;只用一个字重时,完整可变字体可能更大。

比较实际使用的轴范围和字符子集,删除未用字重与斜体,并检查浏览器是否合成不存在的字形。最终看传输、渲染和视觉一致性。

渲染、交互、内存与 Worker

Q109: 浏览器从样式到显示大致经历哪些阶段?

答案

通常可以理解为样式计算、布局、绘制、分层与合成。并不是每次更新都会完整经历所有阶段:改变几何尺寸往往触发布局和绘制,改变颜色通常重绘,合适图层上的 transform/opacity 可能主要走合成。

具体路径由浏览器和页面状态决定,面试回答要强调用 Performance、Rendering 和 Layers 工具验证,而不是背一张永远不变的属性表。

Q110: 什么是强制同步布局?

答案

脚本刚修改了会影响布局的样式,又立即读取 offsetWidthgetBoundingClientRect 等几何信息时,浏览器为了返回准确值,可能被迫提前完成样式与布局。

一次不一定严重,循环里反复“写—读—写—读”会形成布局抖动。优化是批量读取、集中写入,缓存结果并缩小布局影响范围。

Q111: 如何定位 layout thrashing?

答案

在 Performance trace 中看 Recalculate Style/Layout 是否频繁出现,以及是否由脚本调用栈触发;结合源码找到循环中的几何读取和样式写入。

修复后用同一交互重录,比较布局次数、总耗时和 INP 三阶段。不能只把代码包进 rAF,如果 rAF 内仍交替读写,抖动依然存在。

Q112: content-visibility: auto 能替代虚拟列表吗?

答案

它可以跳过屏外内容的部分布局与绘制,保留较自然的 DOM 和查找语义,适合中等规模、内容已在 DOM 的长页面。

它不会减少数据下载、DOM 节点创建和全部内存;超大列表或复杂交互仍可能需要虚拟化。使用时提供合理 contain-intrinsic-size,避免滚动条和布局跳动。

Q113: CSS contain 有什么价值和风险?

答案

contain 告诉浏览器某个组件在布局、绘制、尺寸或样式方面与外部隔离,可缩小更新影响范围,适合独立卡片、列表项和复杂组件。

声明过强会改变尺寸计算、定位、溢出和粘性行为。应从明确边界开始,用真实布局与可访问性测试验证,不能全局一键开启。

Q114: will-change 为什么不能长期全局使用?

答案

它只是告诉浏览器某属性即将变化,浏览器可能提前分层或准备资源。大量、长期声明会增加图层、显存和合成管理成本,甚至更慢。

只在分析确认的动画热点上、开始前短期添加,结束后移除;如果没有可测收益,就不需要它。

Q115: requestAnimationFrame 在 120Hz 屏幕上要注意什么?

答案

不能假设每帧固定 16.7ms,也不能每帧固定移动 1px。应使用回调时间戳计算经过时间,让动画在 60Hz、90Hz、120Hz 和掉帧时保持相同速度。

每帧预算在高刷新率下更紧,仍要减少布局、绘制和脚本工作,并在目标设备实测。

Q116: scheduler.yield() 适合解决什么问题?

答案

它适合把一段可分割的长任务拆成多个任务,让浏览器有机会先处理输入和渲染,同时让当前函数的 continuation 以更合理的优先级继续。

需要特性检测和回退,也不要每个小操作都 yield,调度本身有成本。先完成用户立即可见的反馈,再分批处理低优先级工作。

Q117: await Promise.resolve() 能让浏览器渲染一帧吗?

答案

通常不能。它只把 continuation 放进微任务队列,而浏览器会在当前任务结束前清空微任务;大量微任务仍可能一直占着主线程,渲染和输入得不到机会。

要真正让出当前任务,可用 scheduler.yield() 或合适的 task 回退;动画帧工作则用 rAF,并明确不同调度 API 的时机语义。

Q118: 防抖和节流怎样影响 INP?

答案

节流限制持续事件的执行频率,防抖等用户停止后再执行,能减少重复工作;但把关键反馈防抖 500ms 会直接让交互显得迟钝。

输入框可立即更新本地 UI,再防抖网络搜索;滚动视觉更新可按帧合并。选择等待时间要根据用户反馈需求和实测,不是固定 300ms。

Q119: passive: true 为什么能改善滚动?

答案

对触摸和滚轮监听声明 passive,表示回调不会 preventDefault,浏览器无需等回调结束就能开始滚动。

只有确实不需要阻止默认行为时才能用。回调本身很重仍会占主线程,所以还要减少工作、合并更新;现代浏览器对部分监听有默认策略,也应显式表达意图。

Q120: 事件委托一定比每个节点绑定事件好吗?

答案

事件委托能减少大量相似监听,并自然覆盖动态节点,适合列表点击等冒泡事件。它也会增加祖先回调的目标判断,并不适合不冒泡事件或复杂边界。

现代浏览器处理监听并非一定昂贵,节点不多时直接绑定更清晰。选择依据是节点规模、生命周期、事件语义和维护性。

Q121: React 页面频繁重渲染应该怎么排查?

答案

先用 React Profiler 看哪个提交慢、哪些组件渲染、触发更新的 state/context/props 来源。再缩小状态作用域、拆分 context、稳定真正重要的引用,或把高成本计算移出关键渲染。

不要看到渲染次数就全局 memo;一次便宜渲染可能比比较 props 更划算。目标是减少用户可感知的提交成本和交互延迟。

Q122: useMemoReact.memo 为什么可能适得其反?

答案

它们会增加依赖比较、缓存内存和代码复杂度;依赖经常变化时命中率低,缓存的大结果还可能存活更久。错误依赖也会带来陈旧数据 bug。

只对 profiler 证明的昂贵计算或稳定 props 能跳过重渲染的边界使用,并在变更后重新测量。

Q123: 动态高度虚拟列表为什么容易跳动?

答案

初始只能估算高度,渲染、图片、字体或流式内容到达后真实高度改变,后续项偏移会重算;如果变化发生在视口上方,当前内容就会视觉跳动。

需要稳定 ID、合理估值、ResizeObserver/库测量缓存和滚动锚点补偿。恢复位置时保存业务锚点及相对偏移,比只存 scrollTop 更可靠。

Q124: 虚拟列表如何保证可访问性?

答案

保持正确列表或表格语义,必要时提供总量和当前位置信息;键盘移动到屏外项时先挂载再聚焦,不能让焦点节点因滚动突然消失。

还要提供应用内全量搜索、导出或打印路径,因为浏览器 Ctrl+F 和辅助技术只能看到已挂载内容。用真实屏幕阅读器和键盘旅程验证。

Q125: 聊天列表的滚动锚定怎么设计?

答案

只有用户原本接近底部时才随新消息或流式内容自动滚动;用户上滑读历史时保持位置,并显示“回到最新”。向顶部 prepend 历史后,用稳定消息 ID 和高度差保持原锚点。

不要简单使用 column-reverse 或每次强制 scrollToBottom,否则焦点、可访问性和用户阅读都会受影响。

Q126: 内存泄漏、内存膨胀和频繁 GC 有什么区别?

答案

泄漏是不用的对象仍被引用,重复旅程后基线持续上涨;膨胀是业务确实保留了过多数据,基线高但可能稳定;频繁 GC 是分配速率高,引擎不断回收导致停顿,内存曲线可能反复升降。

三者工具和修复不同:看 Retainers 找泄漏,审计缓存/DOM 找膨胀,用 Allocation profiling 找分配热点。

Q127: WeakMap 和 WeakRef 分别适合什么场景?

答案

WeakMap 适合把元数据关联到对象键,不阻止键被回收,例如 DOM 元数据;它不可枚举,不能当需要遍历和统计的普通缓存。

WeakRef 只能做“对象还在就复用,不在就重建”的可选优化,回收时机完全不确定。会话、权限、请求去重和必须命中的缓存不能依赖它。

Q128: 未取消 fetch 一定会内存泄漏吗?

答案

不一定。一次请求完成后相关对象通常可释放,但组件卸载后继续解析大响应、重试、更新状态或持有闭包会浪费网络和内存,流式请求还可能长期存在。

使用 AbortController 是生命周期和正确性管理,不是“所有 fetch 都永久泄漏”的证明。订阅、Observer、Worker 和计时器也要有对称清理。

Q129: Detached DOM 是什么,怎么定位?

答案

节点已从文档树移除,但仍被 JavaScript、监听器、闭包或缓存引用,就成为 detached node,整棵子树都可能无法回收。

用 Memory 面板的 Detached Elements 或 Heap Snapshot 搜索,查看 Retainers 找到引用链。修复时清除真正的持有者,而不是只再次调用 remove()

Q130: 对象池为什么不是通用内存优化?

答案

对象池可减少极高频固定对象的分配,但也会让对象长期存活、抬高常驻内存,重置遗漏还会串数据。现代引擎通常很擅长回收短命小对象。

只有 allocation profiling 证明分配和 GC 是瓶颈时,再在粒子、缓冲区等局部场景使用,并设置容量上限。

Q131: Worker 传大数据为什么优先考虑 Transferable?

答案

普通 postMessage 对多数数据使用结构化克隆,大 ArrayBuffer 复制会增加时间和内存;Transferable 可以把底层 buffer 所有权转移给接收方,避免复制。

转移后发送方 buffer 会 detached,代码必须明确所有权协议。若两边都需要数据,要重新设计流水线或复制,而不是转移后继续使用。

Q132: 使用 SharedArrayBuffer 需要注意什么?

答案

它允许多个线程共享内存,适合高频、大量、低复制通信,但需要跨源隔离响应头,并使用 Atomics 或无锁协议处理同步与可见性。

竞态、死锁和安全复杂度很高,多数业务用消息和 Transferable 更易维护。先证明复制是瓶颈,再考虑共享内存。

Q133: Worker 池大小应该怎么定?

答案

没有固定“CPU 核数减一”。要看任务是否 CPU 密集、单任务时长、设备核心数、内存、主线程需求和同时打开的页面。

从小池开始,用队列、优先级、取消和背压控制;在低端设备和后台状态降低并发。池太大会争抢 CPU、增加内存和上下文切换。

Q134: OffscreenCanvas 适合什么场景?

答案

它可把部分 Canvas 渲染移到 Worker,适合复杂图表、图像处理和高频绘制,减少主线程竞争。

但 DOM 交互、字体、事件和具体 API 支持仍需适配,数据传输也有成本。要提供能力检测和主线程降级,并验证端到端帧率而不是只看脚本位置。

Q135: requestIdleCallback 适合执行关键任务吗?

答案

不适合。空闲回调可能很晚才执行,繁忙页面甚至长期没有足够空闲时间;它适合可延迟、可中断的预计算或低优先级维护,并应设置合理 timeout。

关键数据提交、用户反馈和必须完成的清理要用确定的调度机制。分片循环还要每次检查 timeRemaining(),不能在一个 idle 回调里做完所有工作。

渲染架构、测试治理与综合场景

Q136: Hydration mismatch 常见原因和影响是什么?

答案

常见原因是服务端与客户端使用不同时间、随机数、地区、浏览器 API、数据版本或条件分支,生成了不同 DOM。框架可能警告、丢弃局部服务端结果并重新渲染,带来闪烁、额外工作甚至事件错误。

保证首屏输入一致,把仅客户端逻辑放到明确边界或 effect,稳定序列化时区与数据。不要用 suppress warning 掩盖未知差异,先找到不一致来源。

Q137: SSR 页面怎样做缓存而不泄露个性化数据?

答案

先把公共内容与用户私有内容分开:公共 HTML/数据可按路由和版本在 CDN 缓存,个性化部分在服务端私有缓存、客户端加载或局部动态渲染。

缓存键必须包含真正影响输出的语言、地区、实验等维度,但维度过多会降低命中。含 Cookie/Authorization 的响应不能误标 public;还要设计失效、stale 和权限变更策略。

Q138: Edge SSR 一定比中心区域 SSR 快吗?

答案

边缘计算能降低用户到计算节点的 RTT,但如果数据源仍在中心区,边缘到数据库的往返可能抵消收益;运行时限制、冷启动和缓存一致性也会增加复杂度。

按页面的数据依赖决定:可边缘缓存或数据就近的内容收益更大,强中心事务页面未必适合。用分地区 TTFB、后端阶段和成本实测。

Q139: 部分预渲染或静态壳的核心思路是什么?

答案

把稳定、可缓存的页面壳提前生成并快速返回,把个性化或慢数据区域留到请求时或流式边界补齐,从而兼顾静态分发与动态内容。

它不是免费的混合模式:要定义边界的缓存和鉴权、占位尺寸、错误恢复、客户端接管与数据一致性。具体框架 API 会随版本变化,面试时先讲原则再讲当前实现。

Q140: Speculation Rules 适合怎样的导航优化?

答案

它让站点声明哪些未来导航可 prefetch 或 prerender,浏览器结合 eagerness 和自身资源策略决定执行。适合高概率、同站且副作用可控的下一步页面。

应排除登出、删除、支付等敏感导航,控制流量与隐私,并在激活后正确统计性能和曝光。命中率低时,预渲染会浪费 CPU、内存和网络。

Q141: 性能测试矩阵至少包含哪些维度?

答案

至少包括页面/用户旅程、设备档位、网络、冷暖缓存、登录态、数据规模、浏览器、地区和发布版本。不能只测首页、桌面和空数据。

先覆盖流量大、商业价值高或历史易退化的组合,再逐步扩展。每个门禁要写清固定条件,线上 RUM 则按真实分布分群。

Q142: Lighthouse 或浏览器自动化为什么有噪声?

答案

共享 CI CPU、后台进程、网络抖动、服务器缓存、页面随机内容和浏览器版本都会影响结果。单次分数变化不等于代码回归。

固定镜像与数据、预热服务、重复运行,用中位数和基线差异判断;保留 trace 供失败诊断。高噪声指标采用波动带,不能设置小到频繁误报的硬阈值。

Q143: 为什么不应该只优化 Lighthouse 分数?

答案

分数是特定版本、配置和实验室环境下多个审计的加权结果,不能覆盖真实设备、登录后交互、长会话和业务任务。

应从具体 LCP、TBT、CLS 与 Insights 定位问题,再看 RUM P75、错误率和转化等业务结果。分数提高但关键交互更慢,不能算成功。

Q144: PR 出现性能回归时如何定位到具体变更?

答案

先确认结果超出正常波动,再比较主分支与 PR 的产物清单、网络瀑布、trace、关键组件 profiler 和服务端数据。

按资源发现、传输、脚本、布局和业务接口分段,必要时二分提交或关闭特性验证。报告应附本次与基线 trace,而不是只给红色分数。

Q145: 为什么不同路由要有不同性能预算?

答案

营销页、内容页、数据后台和在线编辑器的功能、缓存、交互和用户设备不同,共用一个 JS 或 LCP 上限会过松或不现实。

可先建立路由类型模板,再按关键用户旅程覆盖;每个预算注明压缩口径、设备网络、owner 和目标人群,同时保留全站体验底线。

Q146: 第三方脚本预算只限制个数够吗?

答案

不够。一个脚本可能比十个小脚本执行更重,还可能阻塞主线程、读取隐私数据、失败重试或再加载更多资源。

预算应同时看传输、主线程时间、长任务、请求链、隐私和业务价值,并给每个第三方 owner、加载时机、超时、熔断与下线日期。

Q147: 性能预算超标能否申请例外?

答案

可以,但例外应是可审计的工程流程,不是永久关闭门禁。记录业务原因、影响范围、当前值、补偿优化、审批人和到期日。

到期自动提醒并恢复阻断;如果预算本身不合理,应基于数据修改标准并保留历史,而不是给每个 PR 单独放行。

Q148: 如何评估一项性能优化是否值得做?

答案

看受影响用户比例、改善幅度、业务价值、实现与维护成本、风险和机会成本。优先解决大流量路径、低端设备尾部体验和明显阻塞业务任务的问题。

用灰度或 A/B 比较性能、错误、转化和资源成本。技术指标改善很小且复杂度显著上升时,可能不值得长期维护。

Q149: 性能监控如何和发布版本关联?

答案

每个样本带前端构建版本、后端/配置版本或发布批次,监控看板显示发布时间并支持版本分群。灰度时还要区分实验组和流量比例。

出现回归后比较同地区、设备和页面的前后版本,关联 source map、trace 和变更列表。版本字段必须有限且稳定,避免高基数。

Q150: 资源体积预算应该看原始、gzip 还是 Brotli?

答案

三者含义不同:压缩体积接近网络成本,原始体积更能提示解析、内存和未压缩环境风险。团队必须明确口径,不能拿 gzip 基线和 Brotli 本次值比较。

常见做法是关键入口同时记录原始与实际传输压缩体积,并另外关注执行时间。图片、视频等二进制资源按自身编码体积统计。

Q151: 如何设计一个可靠的首屏启动守卫?

答案

在独立于业务 bundle 的轻量壳中记录资源加载、bootstrap、挂载和首屏可见阶段;捕获脚本错误、Promise rejection、chunk 加载失败和超时。

超过合理阶段时间后展示可理解的恢复 UI,可刷新、清缓存、切换备用入口或联系支持。守卫要避免误判慢用户,并把诊断 ID 与版本上报。

Q152: 流式渲染和骨架屏的关系是什么?

答案

骨架屏提供稳定占位与等待反馈,流式渲染让已就绪内容真正提前到达;二者可以配合,但骨架本身不减少数据或脚本耗时。

边界应对应用户可理解的内容块,尺寸接近最终结果,失败时能局部重试。过多碎片会造成跳动和注意力干扰。

Q153: 上传分片大小应该怎么定?

答案

没有固定 5MB。小分片重传成本低但请求和服务端元数据多,大分片吞吐可能更好但失败损失与内存更高;对象存储还可能有最小 part 和最大数量限制。

由服务端会话返回规则,根据文件大小、RTT、失败率和存储约束选择。可以从示例值开始压测,再自适应调整。

Q154: Range 下载为什么需要 ETag 或 Last-Modified?

答案

断点期间源文件可能变化,如果继续把新旧字节拼接,文件会损坏。恢复请求带 If-Range 和资源版本,版本一致时服务端返回 206,不一致时返回 200 完整新资源。

客户端必须校验状态和 Content-Range;收到 200 时丢弃旧分片重新开始,不能当作当前 range。

Q155: 为什么大文件优先直传对象存储?

答案

浏览器通过短期签名 URL 直接上传,可避免大流量和长连接穿过业务服务,利用对象存储成熟的 multipart、校验、扩展与 CDN 能力。

业务服务仍负责鉴权、配额、对象键、会话和完成确认。签名要限制方法、路径、大小、类型和有效期,上传完成后再做扫描与状态发布。

Q156: 网络重试为什么要指数退避并加抖动?

答案

固定间隔会让大量客户端同时重试,形成惊群并加重故障。指数退避逐步降低压力,随机抖动把请求打散。

只重试超时、连接错误、429 和部分 5xx,尊重 Retry-After;鉴权失败和参数错误不应盲重试。写操作还要有幂等键。

Q157: 用户上传 SVG 有什么安全与性能风险?

答案

SVG 可包含脚本、事件属性、外链、滤镜和极复杂路径,直接内联可能引入 XSS,复杂内容也可能消耗大量 CPU。

不信任的 SVG 应严格清洗、隔离源使用 <img>、设置安全响应头,或转为栅格图。服务端限制大小、结构复杂度和外部引用,不能只看扩展名。

Q158: 为什么性能优化必须覆盖低端设备和低内存场景?

答案

高端开发机掩盖 JS 编译、长任务、图片解码和内存峰值,平均指标也可能掩盖真实尾部用户。低端设备更容易因 GC、后台进程和热限制出现问题。

根据用户设备分布选择代表性实机或模拟档位,跑长会话和多次导航;预算和告警至少按设备类型分群。

Q159: prefers-reduced-motion 为什么也是性能实践?

答案

它首先是可访问性要求,为对运动敏感的用户减少非必要动画;同时能避免持续动画占用 CPU/GPU 和电量。

不要简单把所有过渡设为 0 导致状态难以理解,应保留必要反馈,用淡入或即时状态替代大幅位移,并测试功能完整性。

Q160: View Transitions API 会自动保证动画流畅吗?

答案

不会。它提供状态前后快照与过渡编排,能简化同文档或支持环境中的页面过渡,但重 DOM 更新、昂贵绘制和大纹理仍会掉帧。

要做特性检测和无动画降级,尊重 reduced motion,并在导航、焦点、滚动与可访问性上验证。动画只是增强,不应阻塞内容可用。

Q161: 面试中如何讲一个完整的性能优化案例?

答案

按“背景—基线—证据—方案—验证—防回归”讲:说明哪个用户任务慢、受影响人群和初始指标;展示瀑布或 trace 如何定位;解释为何选该方案及取舍;最后给字段 P75、错误率和业务结果。

还要说明没有做什么、风险如何灰度,以及怎样用预算和监控防止回归。只说“压缩图片,Lighthouse 提高 20 分”说服力较弱。

Q162: 如果让你从零建设前端性能体系,你会怎么做?

答案

我会分四层建设:

  1. 指标:按核心路由和任务定义 LCP、INP、CLS、业务阶段与错误 SLO。
  2. 采集:接入 RUM、版本与 attribution,打通服务端 trace;实验室保留可重复旅程和 trace。
  3. 治理:建立路由资源预算、PR 基线对比、第三方 owner 和有到期日的例外。
  4. 运营:看板、发布标记、分级告警、事故复盘和优化收益评估。

先覆盖最重要的少量页面,保证数据可信和修复闭环,再扩大全站;否则一开始采很多数据但无人负责,体系很快失效。

Q163: SPA 路由切换很慢,你会怎样定位?

答案

我会把一次软导航拆成“用户触发、路由匹配、代码就绪、数据就绪、DOM 提交、主要内容可见”几个阶段,而不是先猜是框架渲染慢。

  1. 看路由 chunk 是否点击后才发现,是否与数据请求串行。
  2. 看数据是否散落在父子组件 useEffect 中形成请求瀑布,接口本身是否慢。
  3. 用 Performance 检查路由守卫、状态更新、Hydration、布局和图片解码产生的长任务。
  4. 对每个阶段埋业务时间点,按路由、版本、设备和网络查看 P75,并在修改后验证主要内容可见时间和 INP。

如果代码和数据都很快但用户仍觉得“没反应”,还要检查点击后是否缺少立即反馈。先显示选中态、进度或目标区域骨架,往往比只缩短几十毫秒更能改善感知。

Q164: SPA 是否应该把所有路由都预取?

答案

不应该。预取是在“未来命中率”和“当前资源竞争”之间做投资;全量预取会抢占 LCP、字体和关键接口,也会浪费移动流量、内存和解析时间。

我会按意图和概率分层:高概率下一步可以在 pointerenterpointerdown 或接近视口时预取代码与数据;低概率路由继续懒加载。开启 Save-Data、网络较慢、页面隐藏或当前关键资源未完成时降低积极程度,并监控预取命中率、浪费字节和过期率。

预取请求不能产生写操作、曝光副作用或一次性 token 消耗;预取失败也不能破坏正式导航的重试与错误处理。

Q165: Soft Navigations API 解决什么问题,生产环境怎么接入?

答案

它让浏览器识别没有发生整页加载的 SPA 软导航,并把导航与新的性能条目关联起来,从而更接近硬导航那样测量后续内容呈现,而不必完全依赖框架自定义埋点。

截至 2026 年,Chrome 官方计划从 Chrome 151 开始默认逐步启用,仍属于较新的 Chromium 能力,其他浏览器和旧版本不能假设支持。生产接入应:

  1. 通过 PerformanceObserver.supportedEntryTypes 做能力检测。
  2. 使用带 buffered 的 Observer 持续消费软导航和关联的内容呈现条目,长会话不要只调用 getEntriesByType()
  3. 注意指标仍基于原页面 time origin,计算某次导航耗时时要减去该软导航的开始时间。
  4. 保留路由开始、代码/数据就绪、提交和主要内容可见等业务指标作为跨浏览器基线。

Q166: BFCache、HTTP 缓存和 SPA 数据缓存有什么区别?

答案

三者缓存的层次不同:BFCache 冻结并恢复整页,包括 DOM、JavaScript 堆和滚动状态;HTTP 缓存复用网络响应;SPA 数据缓存保存查询结果和业务状态。

因此它们不能互相替代。页面从 BFCache 恢复时通常很快,但强时效数据仍需要轻量 revalidate,轮询、WebSocket 和监听也要避免重复注册;HTTP 缓存命中不代表应用数据符合当前用户和权限;数据缓存则需要正确的 key、TTL、失效、容量和登出清理。

实践中用 pageshow/pagehide 处理冻结与恢复,不把 unload 当作唯一清理入口,并用 DevTools 检查导致 BFCache 不可用的具体原因。

Q167: SPA 发布后出现旧页面请求新版本 chunk 404,怎么解决?

答案

根因通常是 HTML、清单和静态 chunk 没有作为一个兼容版本发布:用户保留了旧运行时,服务端却已经删除旧哈希文件;或者多节点/CDN 更新顺序不一致。

我会采用以下策略:

  1. 资源使用内容哈希和不可变长缓存,HTML/manifest 使用短缓存或可验证缓存。
  2. 先上传所有新资源,再原子切换 HTML 或清单,避免引用尚未存在的文件。
  3. 旧 chunk 保留一段覆盖真实长会话和灰度窗口的时间,而不是发布后立即删除。
  4. 对动态 import 失败做一次受控刷新提示,但不能无限自动刷新或把所有错误都归因为版本问题。
  5. 上报应用版本、HTML 版本、失败 URL、CDN 节点和 Service Worker 版本,定位错配发生在哪一层。

Q168: Speculation Rules 和 SPA 路由预取有什么区别?

答案

Speculation Rules 主要面向文档级导航,让浏览器按规则预取或预渲染候选页面;SPA 路由预取通常由框架或应用提前加载 import() chunk 和查询数据。

如果目标 URL 能由服务端直接访问,且是跨文档导航,Speculation Rules 可以让浏览器接管更多调度甚至准备完整页面。如果仍在同一文档内切路由,预取路由代码和数据更直接。两者都要基于概率、网络和副作用控制,预渲染尤其不能触发支付、登出、曝光或一次性凭证消费。

我不会为了“用了新 API”同时开启两套重复下载,而会从真实导航形态、缓存复用和命中率决定方案。

Q169: 第三方脚本加了 asyncdefer,为什么页面仍然卡?

答案

因为 async/defer 主要改变脚本下载和执行相对 HTML 解析的时机,不会消除脚本解析、编译和执行成本。SDK 一旦执行,仍可能在主线程创建长任务、同步布局、注入 iframe,并继续拉取更多脚本和接口。

我会进一步判断它是否必须现在执行:非首屏必要脚本延后到关键内容稳定、用户 consent 或首次使用;客服、地图、视频等重组件可先显示 facade,交互后再加载。然后用 Performance、Long Tasks/LoAF、Resource Timing 和启停实验测量供应商的网络、CPU、内存与 INP 影响。

Q170: 如何建立第三方脚本的治理机制?

答案

关键不是维护一张 URL 表,而是让每个第三方都有业务价值、负责人和完整生命周期。

接入前记录供应商、页面范围、加载条件、数据权限、性能预算、失败降级、更新方式和复审日期;经过安全、隐私和性能评估后灰度。上线后结合运行时扫描与 Tag Manager 导出持续监控,按季度或半年度复审业务收益、域名、权限和成本。无人认领或实验已经结束的脚本应告警并下线。

性能预算也要按供应商或功能归属,而不是把所有成本都记到前端团队名下。

Q171: 怎样衡量一个第三方 SDK 的真实性能成本?

答案

不能只看入口脚本的 gzip 体积,因为它可能继续加载配置、字体、iframe 和运行时代码。我会从四层衡量:

  1. 网络:完整依赖图、请求数、连接、传输体积、缓存和供应商 ready 时间。
  2. 主线程:解析、编译、执行、长任务、Long Animation Frame、样式和布局。
  3. 长期资源:内存、监听器、Worker、WebSocket、定时器以及路由切换后的泄漏。
  4. 用户与业务:启用和禁用实验下的 LCP、INP、CLS、错误率、转化和收入。

跨域 iframe 的精确归因有限,所以要结合 trace、脚本 URL、功能开关实验、RUM 分群和供应商版本,而不是宣称能精确分摊每一毫秒。

Q172: 第三方脚本自托管一定比供应商 CDN 更好吗?

答案

不一定。自托管可以控制缓存、版本、CSP 和供应商故障影响,也可能减少 DNS/TLS 连接;但团队必须负责及时更新、安全漏洞、许可证、API 兼容和上游动态配置。

适合自托管的通常是版本不可变、更新可控且许可证允许的静态资源。若 SDK 依赖实时配置、反欺诈规则或供应商要求的同源关系,复制一份文件可能无法正常工作,还会错过安全修复。

因此我会把可靠性、更新 SLA、完整性、隐私、缓存收益和维护成本放在一起评估,并建立自动检查上游版本的流程。

Q173: CSP 能解决第三方脚本的性能问题吗?

答案

不能直接解决。CSP 的核心作用是限制允许加载和执行的来源与能力,降低 XSS 和供应链风险;被允许的第三方脚本仍然可以占用网络、CPU、GPU 和内存。

不过 CSP 的域名白名单、Report-Only 报告和变更审核能帮助团队发现未经审批的第三方,因此是治理机制的一部分。性能方面还需要加载分层、facade、预算、超时、隔离和持续监控。SRI、iframe sandbox、Permissions Policy 也各自解决完整性或权限问题,不能替代性能优化。

Q174: A/B 测试的反闪烁脚本为什么危险,怎么优化?

答案

反闪烁脚本常在实验结果未确定前隐藏页面,能避免用户先看到 A 再切到 B,但供应商或网络一慢,就会直接把 FCP/LCP 和可用时间一起推迟。

优先方案是在服务端或边缘完成稳定分组,把变体随 HTML 返回。必须客户端分组时,只在参与实验的页面执行,设置很短且可观测的超时,预留布局避免 CLS,并在超时后立即展示默认内容。实验结束后及时删除脚本与规则,还要把 SDK 本身的性能影响纳入实验结果,避免测量工具改变转化基线。

Q175: 渐进式 MP4、HLS/DASH 和 MSE 的关系是什么?

答案

渐进式 MP4/WebM 是浏览器从单个媒体文件逐步下载并播放,适合短视频和简单点播。HLS/DASH 是基于清单和分片的流媒体方案,可描述多码率、音轨、字幕和直播窗口。

MSE 是浏览器提供的字节流缓冲接口,不是流媒体协议。浏览器原生不能直接处理某个 HLS/DASH 组合时,JavaScript 播放器解析清单、做 ABR、请求分片,再通过 MediaSource/SourceBuffer 交给 <video> 解码呈现。最终选择还取决于浏览器、系统、codec、DRM、直播延迟和运维成本。

Q176: MSE 播放器为什么必须管理 SourceBuffer 队列?

答案

appendBuffer()remove() 是异步更新,SourceBuffer.updating 为 true 时再修改会抛错;网络分片又可能乱序完成,所以必须把追加、删除、切轨和 seek 串行化。

一个可靠队列会等待 updateend,校验时间轴与分片序号,seek 后丢弃过期任务,错误时停止当前管线并决定重试或重建。还要限制前后缓冲,遇到 QuotaExceededError 时先清理远离播放点的数据,不能无限 append 或立即死循环重试。

Q177: MSE 中如何实现清晰度无缝切换?

答案

无缝切换的前提是转码侧让各清晰度的时间轴、分片边界和关键帧对齐。ABR 决定目标轨道后,从下一个对齐分片开始请求,在当前缓冲继续播放时下载,并追加到同一播放管线。

如果 codec 和容器兼容,通常可以连续追加;如果确实发生类型变化,要先检测 SourceBuffer.changeType(),不支持时限制轨道组合或受控重建。切换过程中要避免时间戳空洞和重叠,保留足够缓冲,并监控切换后卡顿、音画连续性和码率震荡。

SourceBuffer.remove() 主要用于后向缓冲和配额治理,不是每次切换都必须清空旧数据。

Q178: ABR 算法一般考虑哪些因素?

答案

我会把因素分为四类:网络侧看多个分片的有效吞吐和波动;播放器侧看当前缓冲、分片下载时长、卡顿和直播延迟;呈现侧看视口、分辨率、解码能力与掉帧;用户侧看手动画质、Save-Data 和流量成本。

策略上启动保守,吞吐估算留安全余量;缓冲下降或刚卡顿时快速降档,连续稳定后谨慎升档,并设置迟滞避免相邻档位反复跳。安全系数、目标缓冲秒数都要根据内容、分片长度、设备和网络分布实验确定,不能硬编码一套通用数字。

Q179: 视频缓冲应该怎样设计和调优?

答案

先按场景明确目标:短点播重首帧,长点播重抗抖动和 seek,普通直播重稳定,低延迟直播则要控制与 live edge 的距离。缓冲越多不一定越好,它会增加流量浪费、内存和 SourceBuffer 配额,直播中还会增加延迟。

我会分别设置启动、前向和后向缓冲策略,并处理 seek、页面隐藏、断网重试、长会话裁剪和 QuotaExceededError。调优时同时看首帧、重缓冲率与时长、退出率、内存、seek 命中和直播延迟,按设备与网络分群,而不是只追求一个“30 秒缓冲”的固定值。

Q180: WebCodecs 适合什么场景,为什么不直接用它写普通播放器?

答案

WebCodecs 适合浏览器剪辑、逐帧特效、机器视觉、实时通信和需要把帧交给 Canvas/WebGL/WebGPU 的自定义管线,它提供低层编解码和帧访问能力。

普通播放器还需要清单、网络、ABR、容器 demux、播放时钟、缓冲、字幕、控件和可访问性,这些都不是 WebCodecs 的完整职责。自行实现会显著增加兼容和资源管理成本。

确实使用时要通过 isConfigSupported() 检测准确 codec 配置,控制解码队列和背压,在 Worker 中分担工作,并及时 VideoFrame.close()。普通播放优先使用成熟 <video>、原生协议支持或 MSE 播放器。

相关链接