你知道自己的页面占了多少内存吗?
不是 DevTools 里的快照,不是某一次调试时瞄到的 JS Heap 数字------我说的是生产环境里,真实用户在使用时,你的页面到底吃掉了多少内存。
多数前端对这个问题没有答案。原因很简单:以前根本没有靠谱的 API 让你在线上量这件事。
现在有了。它叫 performance.measureUserAgentSpecificMemory()。名字长得令人窒息,但它解决的问题非常精准:在生产环境中,测量你的页面使用的全部内存。
一、为什么旧 API 不够用?
你可能用过 performance.memory,它返回三个数字:usedJSHeapSize、totalJSHeapSize、jsHeapSizeLimit。
看起来挺好?问题一大堆:
它只量 JS 堆。你的 DOM 节点、iframe 里的内容、Web Worker 占的内存,全部不算在内。
共享堆的噪声。如果多个同源页面共享一个渲染进程,你拿到的数字可能把别人的内存也算进来了。
时机不确定。它返回的是当前时刻的瞬时快照,有可能 GC 还没跑,数字虚高。
非标准 API。它用的是"堆"这种跟浏览器内部实现强绑定的概念,无法跨浏览器标准化。
这就像量体重时穿着棉袄、背着书包,还站在别人的秤上。
| 维度 | performance.memory |
measureUserAgentSpecificMemory() |
|---|---|---|
| 测量范围 | 仅 JS 堆 | JS + DOM + iframe + Worker |
| 共享堆干扰 | 可能混入其他页面 | 按页面隔离归因 |
| 测量时机 | 即时快照(可能 GC 前) | GC 后测量(噪声更低) |
| 标准化 | 非标准,Chrome 私有 | W3C 提案,目标标准化 |
| 返回方式 | 同步 | 异步 Promise |
| 前提条件 | 无 | 需要 cross-origin isolation |
新 API 的核心升级是两点:量得更全 (覆盖整个页面的关联执行环境),量得更准(在 GC 后测量,减少噪声)。
二、一个前提:跨源隔离
这个 API 有个门槛------你的页面必须处于跨源隔离(Cross-Origin Isolation)状态。
为什么?因为内存归因需要精确知道"这块内存属于哪个来源",如果你的页面和第三方 iframe 共享进程、没有隔离,泄露细粒度的内存信息就可能变成旁路攻击(Spectre)的帮凶。
实现跨源隔离需要在服务端设置两个 HTTP 响应头:
makefile
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
COOP: same-origin 确保你的页面和跨源弹出窗口不共享浏览上下文组。COEP: require-corp 要求页面加载的所有跨源资源必须明确授权(通过 CORS 或 CORP)。
这不是为了给你添麻烦,而是一个安全契约:你承诺不随便加载第三方资源,浏览器才放心把精确的内存数据给你。
检测当前页面是否已隔离很简单:
javascript
if (window.crossOriginIsolated) {
// 可以使用 measureUserAgentSpecificMemory
}
三、基本用法
API 调用本身非常简洁:
javascript
if (window.crossOriginIsolated && performance.measureUserAgentSpecificMemory) {
try {
const result = await performance.measureUserAgentSpecificMemory();
console.log(result);
} catch (error) {
if (error instanceof DOMException && error.name === 'SecurityError') {
console.log('安全上下文不满足');
}
}
}
返回的数据结构长这样:
css
{
bytes: 60_100_000, // 总内存估算值
breakdown: [
{
bytes: 40_000_000,
attribution: [{ url: "https://你的页面.com/", scope: "Window" }],
types: ["JavaScript"]
},
{
bytes: 20_000_000,
attribution: [{
url: "https://你的页面.com/iframe",
container: { id: "iframe-id-a", src: "/iframe" },
scope: "Window"
}],
types: ["JavaScript"]
},
{
bytes: 100_000,
attribution: [],
types: ["DOM"]
}
]
}
bytes 是总量,breakdown 是明细------拆到每个执行环境(主页面、iframe、Worker),标注了 URL、作用域和内存类型。
这比 performance.memory 的三个裸数字强到不知道哪里去了。 你不只知道"页面占了多少",还知道"是主页面占的、还是哪个 iframe 占的、是 JS 内存还是 DOM 内存"。
四、生产环境里怎么用才对
单次调用的价值有限。这个 API 真正的威力在于长期趋势监控。
原文推荐了一个巧妙的采样策略:泊松过程随机采样。
为什么不用 setInterval 每隔 5 分钟固定测一次?因为固定间隔的采样有系统性偏差------如果内存峰值恰好出现在你两次采样的间隙,你永远捕捉不到。
泊松过程的采样间隔服从指数分布,每个时刻被采到的概率是相等的。这是统计学里消除采样偏差的经典方法:
javascript
function measurementInterval() {
const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000; // 平均 5 分钟
return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}
-Math.log(Math.random()) 生成一个指数分布的随机数,乘以平均间隔。有时 30 秒就测一次,有时 15 分钟才测一次,但长期平均下来是每 5 分钟一次。
完整的监控器:
scss
function scheduleMeasurement() {
if (!window.crossOriginIsolated ||
!performance.measureUserAgentSpecificMemory) {
return;
}
setTimeout(performMeasurement, measurementInterval());
}
async function performMeasurement() {
try {
const result = await performance.measureUserAgentSpecificMemory();
// 上报到你的监控系统
reportToAnalytics({
totalBytes: result.bytes,
breakdown: result.breakdown,
timestamp: Date.now(),
url: location.href
});
} catch (e) {
// 静默失败,不影响用户
}
// 递归调度下一次
scheduleMeasurement();
}
scheduleMeasurement();
核心价值不在于某一次的数字,而在于同一个环境下的纵向趋势。
版本 A 平均 45MB,版本 B 上线后变成 60MB------即使你不知道绝对值是否"正确",这个 15MB 的增量就足以触发警报。这跟你体检时的血压一样:140/90 具体准不准不重要,重要的是上次是 120/80,这次高了 20。
五、三个典型使用场景
1. 发布回归检测
新版本上线后,对比前后两周的内存中位数。如果显著上升,说明新代码可能引入了泄漏。
2. A/B 测试的内存成本
开启一个实验特性后,实验组的内存消耗比对照组高 30%------这个信息可以帮你决定是否上线这个功能,或者先优化内存开销。
3. 长时间使用场景
SPA 类应用最怕的就是"用久了变慢"。用泊松采样持续监控,如果内存随会话时长单调递增,大概率有泄漏。
| 场景 | 怎么看数据 | 判断标准 |
|---|---|---|
| 版本回归 | 前后版本的内存中位数对比 | 升高 > 10% 需要排查 |
| A/B 测试 | 实验组 vs 对照组的内存分布 | 差异显著则需评估 |
| 长会话泄漏 | 内存 vs 会话时长的相关性 | 正相关 = 泄漏 |
六、别踩的坑
不能跨浏览器比较。 不同浏览器(甚至同一浏览器的不同版本)对"内存"的定义和估算方式不同。Chrome 和 Edge 返回的数字可能差 30%,这不代表一个比另一个"更省内存",只是量法不一样。
breakdown 可能是空的。 某些浏览器可能返回空的 breakdown 或 attribution。你的代码必须处理这种情况,不要硬编码 result.breakdown[0].attribution[0].url。
本地调试有延迟。 API 的 Promise 在 GC 后才 resolve,本地测试可能要等 20 秒。如果你着急,启动 Chrome 时加参数:
ini
chrome --enable-blink-features='ForceEagerMeasureMemory'
这会跳过等待,立刻返回结果。只用于调试,生产环境不需要。
COOP/COEP 可能影响第三方资源。 开启 COEP: require-corp 后,所有跨域资源(图片、脚本、iframe)都必须带 CORS 头或 CORP 头,否则加载失败。上线前一定要在灰度环境充分测试。
七、和 Heap Snapshot 是什么关系?
一句话总结:
measureUserAgentSpecificMemory是体检,Heap Snapshot 是手术台。
前者告诉你"体重是多少、最近是不是变重了",后者告诉你"脂肪堆在哪里、是内脏脂肪还是皮下脂肪"。
生产环境用 measureUserAgentSpecificMemory 做趋势监控,发现异常后,本地用 Heap Snapshot 定位具体的泄漏对象和引用链。两者搭配,才是完整的内存治理方案。
如果你只想带走一句话,我建议记这个:
内存优化不是一次性修 bug,而是持续可观测的趋势管理------先量得准,才治得好。
参考来源
• Brendan Kenny, Ulan Degenbaev ------ Monitor your web page's total memory usage with measureUserAgentSpecificMemory() | web.dev
• 原文链接:web.dev/articles/mo...
