登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

前端怎样统计静态资源等待时间

来源:17golang原创

时间:2026-10-18 02:47:53 215浏览 收藏

页面里“某个静态资源很慢”通常不是一个单一数字:从开始抓取到收到首字节,反映的是请求等待;从首字节到最后一个字节,反映的是传输;如果还要把重定向算进去,则要看整条资源计时记录。前端可以用 PerformanceResourceTiming 配合 PerformanceObserver 直接统计这些时间,不需要改写 、 或 前端怎样统计静态资源等待时间 标签。

官方资料:https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming

最实用的做法是:页面尽早扩大 resource 缓冲区,再用一个 buffered 的 PerformanceObserver 读取历史和新增条目;用 responseStart - requestStart 表示首字节前等待,用 responseEnd - fetchStart 表示最终资源抓取时长,并把跨源字段为 0 单独标记为“计时信息不可见”,不要直接当成 0 毫秒。

先把“等待时间”拆成四个时间点

一条静态资源记录的关键时间点可以先这样理解:

字段含义适合计算
fetchStart浏览器开始抓取最终资源的时间与 responseEnd 相减,得到最终资源的抓取时长
requestStart浏览器开始发出资源请求的时间与 responseStart 相减,观察首字节前等待
responseStart收到响应第一个字节的时间判断服务端响应、网络等待是否偏长
responseEnd收到最后一个字节,或传输连接关闭前的时间观察剩余内容下载耗时

因此,本文把“静态资源等待时间”定义成一个可拆分的数据集合,而不是强行只返回一个字段:

  • ttfb = responseStart - requestStart:首字节前等待。值为 0 时,要检查缓存、取消请求或跨源限制。
  • download = responseEnd - responseStart:从首字节到最后字节的传输区间。
  • fetch = responseEnd - fetchStart:不含重定向的最终资源抓取时长。
  • duration:资源条目的整体持续时间;如果页面只关心完整计时链路,可以保留它作为总时长参考。
PerformanceResourceTiming 四个时间字段与资源条目的静态关系说明图
图1:fetchStart、requestStart、responseStart、responseEnd 与等待和下载指标的关系说明图,不是浏览器运行截图。

用 PerformanceObserver 接住已经产生的条目

如果只在页面加载完成后调用 performance.getEntriesByType('resource'),你能看到调用时已经存在的条目,但不容易持续接收后续加载的资源。更适合监控模块的方式是创建 PerformanceObserver,并设置 buffered: true,这样观察器创建前已经记录的资源也会进入回调。

// 先扩大资源计时缓冲区,避免长页面很快超过默认容量。
performance.setResourceTimingBufferSize(500);

const resourceObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 只保留常见静态资源,避免把所有资源都上报给业务接口。
    const staticTypes = new Set(["script", "link", "css", "img", "image", "font"]);
    if (!staticTypes.has(entry.initiatorType)) {
      continue;
    }

    const ttfb = entry.responseStart - entry.requestStart;
    const download = entry.responseEnd - entry.responseStart;
    const fetch = entry.responseEnd - entry.fetchStart;

    // 跨源资源可能因缺少 Timing-Allow-Origin 而返回 0,先标成不可见。
    const timingHidden = entry.requestStart === 0 || entry.responseStart === 0;
    const record = {
      name: entry.name,
      initiatorType: entry.initiatorType,
      ttfb: timingHidden ? null : Math.max(0, ttfb),
      download: timingHidden ? null : Math.max(0, download),
      fetch: timingHidden ? null : Math.max(0, fetch),
      duration: entry.duration,
      transferSize: entry.transferSize,
      timingHidden,
    };

    // 这里可以替换为采样上报;示例只输出结构,不代表真实运行结果。
    console.log("resource-timing", record);
  }
});

// buffered 让观察器同时接收创建前已经记录的 resource 条目。
resourceObserver.observe({ type: "resource", buffered: true });

这里没有把 console.log 的内容写死成某个数值,因为真正的耗时取决于浏览器缓存、网络、服务器和资源大小。实际接入时,可以把 record 交给采样器,并在上报前去掉完整查询参数,避免把用户数据带入监控。

把 URL、类型和大小整理成可行动数据

只有时间差还不够定位问题。name 可以帮助你定位资源,initiatorType 可以区分脚本、样式、图片和字体,transferSize、encodedBodySize 与 decodedBodySize 则可以帮助判断响应体和传输体量。建议先在浏览器内做轻量整理,再决定是否上报。

// 将资源条目整理成页面级汇总,适合在页面退出前做一次采样。
function summarizeResources(entries) {
  const summary = new Map();

  for (const entry of entries) {
    const type = entry.initiatorType || "unknown";
    const current = summary.get(type) || {
      count: 0,
      totalFetch: 0,
      totalTransfer: 0,
      hiddenTiming: 0,
    };

    current.count += 1;
    // duration 是条目自身的时间范围,不能替代跨源不可见字段的判断。
    current.totalFetch += entry.duration;
    current.totalTransfer += entry.transferSize;
    if (entry.requestStart === 0 || entry.responseStart === 0) {
      current.hiddenTiming += 1;
    }
    summary.set(type, current);
  }

  return [...summary.entries()].map(([initiatorType, value]) => ({
    initiatorType,
    ...value,
  }));
}

// 读取当前已存在的资源条目;观察器仍负责接收后续新增条目。
const currentResources = performance.getEntriesByType("resource");
const resourceSummary = summarizeResources(currentResources);
console.log("resource-summary", resourceSummary);

汇总时不要把不同类型简单相加后称为“页面加载时间”。脚本、字体和图片可能并行加载,类型汇总更适合回答“哪一类资源最慢、哪一类资源体积最大”,不能直接推导用户端的完整首屏体验。

处理缓冲区满和跨源字段为零

资源条目写入浏览器的 resource timing buffer。MDN 说明该缓冲区初始容量至少为 250;页面资源较多时,应该在尽早的位置调用 setResourceTimingBufferSize()。如果需要周期性采集,也要考虑采集后调用 clearResourceTimings(),否则旧条目会持续占用缓冲区。

// 缓冲区满时只做轻量通知;清理动作要与采样周期配合。
performance.addEventListener("resourcetimingbufferfull", () => {
  console.warn("resource timing buffer is full");
  // 清理会移除已有 resource 条目,清理前应先完成需要的汇总或上报。
  performance.clearResourceTimings();
});

// 让后续的页面资源拥有更大的记录空间。
performance.setResourceTimingBufferSize(1000);

另一个常见误判来自跨源资源。requestStart、responseStart 等字段在没有 Timing-Allow-Origin 时可能是 0;这表示页面看不到对应的计时细节,不代表服务器瞬间响应。若资源由你控制,可以让资源响应带上合适的 Timing-Allow-Origin;否则应保留 timingHidden 标志,不要把这些记录混进耗时平均值。

静态资源等待时间的指标边界与跨源可见性关系说明图
图2:首字节等待、下载区间、总抓取和跨源不可见字段的边界说明图,不是性能面板截图。

从页面采集到可复用的小项目结构

可以把上面的代码拆成三个职责:采集器只接收条目,归一化函数只计算字段,上报层再做采样和脱敏。这样后续切换到日志、性能平台或自建接口时,不需要重新理解浏览器计时字段。

  1. 在应用入口尽早设置缓冲区大小,并安装唯一的 PerformanceObserver。
  2. 在回调中先过滤 initiatorType,再计算 ttfb、download 和 fetch。
  3. 把 requestStart 或 responseStart 为 0 的条目标记为不可见,不参与跨源耗时平均。
  4. 上报时只保留资源主机、路径模板或哈希后的标识,避免把完整查询参数和业务数据带出页面。
  5. 采集周期结束后再清理资源条目,确保清理不会让同一批数据重复计入。

这个结构能回答“哪类静态资源等待长”“等待发生在首字节前还是下载阶段”“是不是跨源计时不可见”,但它不会自动回答“用户看到了什么”。如果要分析首屏或交互,还需要把资源计时和导航、绘制、长任务等指标放在同一套性能模型中。

常见问题

为什么 responseStart 是 0,但资源明明加载成功?

最常见原因是资源跨源且响应没有提供 Timing-Allow-Origin,也可能是缓存命中、请求被取消等情况。先结合 name、transferSize 和资源归属判断,不要直接把 0 当成 0 毫秒。

responseEnd - fetchStart 能代表用户等待时间吗?

它代表最终资源的抓取时长,不等同于资源从页面开始加载到用户真正看到内容的时间。渲染阻塞、解码、布局和主线程任务都可能发生在资源接收之后。

为什么观察器创建后还要设置 buffered?

页面早期资源可能在监控模块初始化前就已经完成。设置 buffered: true 可以让观察器读取已经记录的 resource 条目,再继续接收后续条目。

总结来说,静态资源等待统计的核心不是记住一个“总耗时”字段,而是稳定保存资源条目、明确时间差口径、区分缓冲区丢失和跨源不可见。先把数据边界做对,再谈平均值、分位数和告警阈值,结果才不会被几个 0 毫秒误导。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>