《打破 “慢“ 的黑箱:前端请求全链路耗时统计方案》

发布于:2025-09-01 ⋅ 阅读:(19) ⋅ 点赞:(0)

接续上文:网站酷炫换皮肤?——PC 端 H5 换肤方案实战分享-CSDN博客

 主页: 有更多有趣的实例教程哟!!!!!!!

专栏有更多内容:

https://blog.csdn.net/m0_73589512/category_13028539.html

目录

《打破 "慢" 的黑箱:前端请求全链路耗时统计方案》

一、为什么需要请求耗时统计工具?

二、核心需求拆解:我们需要统计哪些数据?

三、技术方案设计:怎么捕获这些数据?

3.1 捕获 XMLHttpRequest 请求

3.2 捕获 Fetch API 请求

3.3 捕获页面资源请求

3.4 统一的数据收集函数

四、数据存储与上报:怎么保存和传输数据?

4.1 本地存储:localStorage 暂存

4.2 数据上报:发送到后端

五、数据可视化:让数据一目了然

5.1 简单的面板 UI

5.2 数据展示组件

5.3 高级可视化:添加图表

六、高级功能:让工具更实用

6.1 过滤和搜索

6.2 性能预警

6.3 环境区分

七、总结与扩展

可以扩展的功能:

八、实战部署:从代码到产品

8.1 模块化封装

8.2 按需加载

8.3 性能影响控制

九、结语:让数据驱动性能优化

《打破 "慢" 的黑箱:前端请求全链路耗时统计方案》

在前端开发的日常中,我们总会遇到这样的灵魂拷问:

产品经理:"用户说页面加载好慢啊!赶紧优化一下!" 你:"有多慢?具体哪个接口慢?" 产品经理:"用户就是说慢啊!你自己感受一下嘛!" 你内心 OS:我感受个锤子哦!没有数据我优化个寂寞?

这种 "凭感觉" 的性能优化,就像闭着眼睛修水管 —— 运气好能堵住漏水,运气不好能捅出喷泉。今天我们就来设计一个 "全站请求耗时统计工具",用数据说话,让 "慢" 这个模糊的概念变得清晰可量化。

一、为什么需要请求耗时统计工具?

在开始搬砖之前,我们得先搞明白:为什么要费力气做这个工具?

想象一下,你开了家奶茶店,顾客总说 "等太久了"。如果你不知道:

  • 是点单环节慢?

  • 是制作环节慢?

  • 还是某个特定饮品(比如杨枝甘露这种配料复杂的)拖慢了整体速度?

那你的优化方案大概率是瞎折腾 —— 可能给点单员加了速,结果发现问题出在制作台效率上。

前端请求也是一个道理。一个页面从打开到能交互,可能要发十几个甚至几十个请求:HTML、CSS、JS 等静态资源,接口数据、图片、埋点上报... 哪个请求慢了?是网络问题还是服务器问题?是偶尔慢还是一直慢?没有统计工具,这些问题永远是谜。

一个靠谱的请求统计工具能帮我们解决这些问题

  • 精确记录每个请求的耗时(从发起到完成的全流程)

  • 区分请求类型(接口、静态资源、图片等)

  • 标记异常请求(超时、失败的)

  • 聚合分析数据(哪个接口平均耗时最长?哪个时段请求最慢?)

  • 前端可视化展示(用图表让数据一目了然)

有了这些数据,优化性能就能有的放矢 —— 就像医生看病先做检查,而不是凭感觉开药方。

二、核心需求拆解:我们需要统计哪些数据?

设计工具前,先明确我们要收集哪些信息。就像做问卷调查前要设计问题,不能想到啥加啥。

一个 HTTP 请求从发出到完成,有很多关键节点,我们需要记录的核心数据包括:

数据项 作用 示例
唯一标识 区分不同请求 "req_1629273845621_324"
请求地址 知道是哪个请求 "https://api.example.com/userinfo"
请求类型 区分接口 / 静态资源 "xhr"(接口)、"fetch"(接口)、"resource"(静态资源)
资源类型 更细的分类 "script"(JS 文件)、"image"(图片)、"font"(字体)
方法 请求方式 "GET"、"POST"
状态码 判断请求是否成功 200(成功)、404(未找到)、500(服务器错误)
开始时间 计算耗时的起点 1629273845621(时间戳)
结束时间 计算耗时的终点 1629273846123(时间戳)
耗时 核心指标(结束 - 开始) 502ms
发起位置 知道是哪个页面 / 组件发起的 "首页 /index.vue"、"购物车组件"
错误信息 失败时记录原因 "Network Error"、"Timeout"
环境信息 辅助分析(是否和环境有关) "production"(生产环境)、"Chrome 92"

这些数据就像给请求拍了个 "全身照",既能看到整体耗时,又能分析具体细节。比如发现某个接口平均耗时 2 秒,但只在 Chrome 浏览器上出现,那可能是浏览器兼容性导致的问题。

三、技术方案设计:怎么捕获这些数据?

明确了要统计什么,接下来就是怎么实现 —— 怎么 "抓" 到这些请求的数据。

前端的请求主要有三种类型:

  1. XMLHttpRequest(传统的 Ajax 请求)

  2. Fetch API(现代的请求方式)

  3. 页面资源请求(JS、CSS、图片等)

我们需要针对这三种类型分别设计捕获方案。

3.1 捕获 XMLHttpRequest 请求

XMLHttpRequest 是老古董了,但现在还有很多项目在⽤。它的特点是有明确的事件和方法,我们可以通过 "重写" 它的方法来实现监控。

原理很简单:就像给快递盒贴了个追踪器,原本的快递运输流程不变,但我们能知道它什么时候发出、什么时候签收。

// 保存原生的XMLHttpRequest
const originalXHR = window.XMLHttpRequest;
​
// 重写XMLHttpRequest
window.XMLHttpRequest = function() {
  const xhr = new originalXHR();
  let requestData = {
    id: `xhr_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, // 生成唯一ID
    url: '',
    method: '',
    startTime: 0,
    endTime: 0,
    duration: 0,
    status: 0,
    type: 'xhr',
    error: ''
  };
​
  // 监听open方法,获取请求地址和方法
  const originalOpen = xhr.open;
  xhr.open = function(method, url) {
    requestData.method = method;
    requestData.url = url;
    originalOpen.apply(xhr, arguments); // 调用原生方法
  };
​
  // 监听send方法,记录开始时间
  const originalSend = xhr.send;
  xhr.send = function() {
    requestData.startTime = Date.now();
    originalSend.apply(xhr, arguments);
  };
​
  // 监听load事件,记录成功的请求
  xhr.addEventListener('load', function() {
    requestData.endTime = Date.now();
    requestData.duration = requestData.endTime - requestData.startTime;
    requestData.status = xhr.status;
    collectRequestData(requestData); // 收集数据
  });
​
  // 监听error事件,记录失败的请求
  xhr.addEventListener('error', function() {
    requestData.endTime = Date.now();
    requestData.duration = requestData.endTime - requestData.startTime;
    requestData.status = xhr.status;
    requestData.error = 'Network Error';
    collectRequestData(requestData);
  });
​
  // 监听abort事件,记录被取消的请求
  xhr.addEventListener('abort', function() {
    requestData.endTime = Date.now();
    requestData.duration = requestData.endTime - requestData.startTime;
    requestData.status = 0;
    requestData.error = 'Request Aborted';
    collectRequestData(requestData);
  });
​
  return xhr;
};

这段代码的核心是 "代理模式"—— 我们没有改变 XMLHttpRequest 的功能,只是在它的关键节点(打开、发送、完成、失败)添加了数据收集的逻辑。就像给原本的管道装了个流量计,不影响水流,但能知道流量多少。

3.2 捕获 Fetch API 请求

Fetch API 是 ES6 新增的请求方式,基于 Promise,比 XMLHttpRequest 更现代。捕获它的思路和 XHR 类似,但实现方式略有不同。

// 保存原生的fetch
const originalFetch = window.fetch;
​
// 重写fetch
window.fetch = function(input, init = {}) {
  // 处理请求地址(input可能是Request对象或URL字符串)
  const url = typeof input === 'string' ? input : input.url;
  const method = (init.method || 'GET').toUpperCase();
​
  const requestData = {
    id: `fetch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    url: url,
    method: method,
    startTime: Date.now(),
    endTime: 0,
    duration: 0,
    status: 0,
    type: 'fetch',
    error: ''
  };
​
  // 调用原生fetch,并在Promise resolved/rejected时收集数据
  return originalFetch(input, init)
    .then(response => {
      // 克隆响应对象(因为response.body只能读取一次)
      const clonedResponse = response.clone();
      requestData.endTime = Date.now();
      requestData.duration = requestData.endTime - requestData.startTime;
      requestData.status = clonedResponse.status;
      collectRequestData(requestData);
      return response; // 不影响原请求的响应
    })
    .catch(error => {
      requestData.endTime = Date.now();
      requestData.duration = requestData.endTime - requestData.startTime;
      requestData.error = error.message || 'Fetch Error';
      collectRequestData(requestData);
      throw error; // 不影响原请求的错误处理
    });
};

Fetch 的监控和 XHR 类似,但因为它返回 Promise,所以我们通过链式调用的方式在请求完成或失败时收集数据。这里要注意克隆 response 对象,因为 response 的 body 是可读流,只能读取一次,不克隆的话会影响原请求的处理。

3.3 捕获页面资源请求

页面加载时会请求各种资源:JS、CSS、图片、字体等。这些请求可以通过 Performance API 来捕获。

Performance API 就像一个系统日志,记录了页面加载过程中各种事件的时间戳。我们可以通过它获取所有资源的加载信息。

// 监听页面加载完成事件(此时大部分资源已加载)
window.addEventListener('load', collectResourceData);
​
// 也可以定时检查新加载的资源
setInterval(collectResourceData, 1000);
​
function collectResourceData() {
  // 获取所有资源的性能数据
  const performanceEntries = performance.getEntriesByType('resource');
  
  performanceEntries.forEach(entry => {
    // 过滤已处理过的资源(避免重复收集)
    if (entry._tracked) return;
    entry._tracked = true;
​
    const requestData = {
      id: `resource_${entry.name}_${Date.now()}`,
      url: entry.name,
      method: 'GET', // 资源请求一般是GET
      startTime: entry.startTime,
      endTime: entry.responseEnd,
      duration: entry.responseEnd - entry.startTime,
      status: 200, // 资源请求成功默认200(实际可能有失败,这里简化处理)
      type: 'resource',
      resourceType: entry.initiatorType, // 资源类型:script、link、img等
      error: ''
    };
​
    // 简单判断资源是否加载失败(实际场景可能需要更复杂的判断)
    if (requestData.duration < 1 && entry.responseEnd === 0) {
      requestData.error = 'Resource Load Failed';
    }
​
    collectRequestData(requestData);
  });
}

这里用performance.getEntriesByType('resource')获取所有资源的加载信息,包括开始时间、结束时间、资源类型等。需要注意的是,资源加载是异步的,所以我们可以在页面加载完成后收集一次,再定时检查新加载的资源(比如懒加载的图片)。

3.4 统一的数据收集函数

上面三种捕获方式最后都会调用collectRequestData函数,我们需要实现这个函数来统一处理数据:

// 存储所有请求数据的数组
const allRequests = [];
​
// 数据收集函数
function collectRequestData(data) {
  // 补充环境信息
  const envInfo = {
    timestamp: Date.now(), // 记录数据收集的时间
    page: window.location.href, // 当前页面URL
    userAgent: navigator.userAgent, // 浏览器信息
    network: navigator.connection ? navigator.connection.effectiveType : 'unknown' // 网络类型(4g/3g等)
  };
​
  // 合并数据
  const request = { ...data, ...envInfo };
  
  // 添加到数组
  allRequests.push(request);
  
  // 控制台打印(开发环境用)
  console.log(`[请求统计] ${request.method} ${request.url} 耗时: ${request.duration}ms 状态: ${request.status}`);
  
  // 可以在这里添加数据上报逻辑(发送到后端)
  // reportToServer(request);
  
  // 保持数组不要太大,超过1000条就清空(避免内存占用过多)
  if (allRequests.length > 1000) {
    allRequests.splice(0, allRequests.length - 500);
  }
}

这个函数做了几件事:

  1. 补充环境信息(当前页面、浏览器、网络类型等)

  2. 合并数据并存储到数组中

  3. 控制台打印(方便开发时查看)

  4. 简单的内存管理(避免数组无限增长)

四、数据存储与上报:怎么保存和传输数据?

收集了数据,接下来要考虑怎么存储和上报。如果只是在控制台看,刷新页面就没了,没什么用。

4.1 本地存储:localStorage 暂存

可以用 localStorage 暂时存储数据,避免页面刷新丢失:

// 保存数据到localStorage
function saveToLocalStorage() {
  try {
    localStorage.setItem('requestStats', JSON.stringify(allRequests));
  } catch (e) {
    console.warn('本地存储请求数据失败', e);
  }
}
​
// 从localStorage恢复数据
function loadFromLocalStorage() {
  try {
    const saved = localStorage.getItem('requestStats');
    if (saved) {
      allRequests.push(...JSON.parse(saved));
    }
  } catch (e) {
    console.warn('恢复本地请求数据失败', e);
  }
}
​
// 初始化时恢复数据
loadFromLocalStorage();
​
// 定时保存数据
setInterval(saveToLocalStorage, 5000);

注意 localStorage 有容量限制(一般是 5MB),所以不能存太多数据。可以定期清理旧数据,只保留最近的请求。

4.2 数据上报:发送到后端

本地存储只能在当前设备查看,要实现多设备、多用户的数据聚合分析,需要把数据上报到后端:

// 数据上报函数
function reportToServer(data) {
  // 避免上报请求本身被统计(否则会陷入无限循环)
  if (data.url.includes('/api/report')) {
    return;
  }
​
  // 用Image对象发送请求(比XHR/fetch更轻量,不阻塞页面)
  const img = new Image();
  const reportUrl = 'https://api.example.com/report/request-stats';
  
  // 构造上报参数(用JSON.stringify转成字符串)
  const params = new URLSearchParams();
  params.append('data', JSON.stringify(data));
  
  img.src = `${reportUrl}?${params.toString()}`;
  
  // 简单的错误处理
  img.onerror = () => {
    console.warn('数据上报失败', reportUrl);
  };
}

这里用 Image 对象发送上报请求,比 XHR 或 fetch 更轻量,而且不会阻塞页面。需要注意排除上报请求本身,否则上报请求会被统计,然后又触发上报,陷入无限循环。

实际项目中,可能需要批量上报(而不是每个请求都上报),可以攒一批数据再发送,减少请求次数:

// 批量上报队列
const reportQueue = [];
​
// 批量上报函数
function batchReport() {
  if (reportQueue.length === 0) return;
  
  // 取出队列中的所有数据
  const dataToReport = [...reportQueue];
  reportQueue.length = 0;
  
  // 发送批量上报请求
  const img = new Image();
  const reportUrl = 'https://api.example.com/report/batch-request-stats';
  const params = new URLSearchParams();
  params.append('data', JSON.stringify(dataToReport));
  img.src = `${reportUrl}?${params.toString()}`;
}
​
// 修改collectRequestData,把数据加入队列
function collectRequestData(data) {
  // ... 前面的逻辑不变 ...
  
  // 加入上报队列
  reportQueue.push(request);
  
  // 每满10条或每30秒上报一次
  if (reportQueue.length >= 10) {
    batchReport();
  } else {
    // 防抖:30秒内没满10条也上报
    clearTimeout(window.reportTimer);
    window.reportTimer = setTimeout(batchReport, 30000);
  }
}

批量上报能减少网络请求,更适合生产环境。

五、数据可视化:让数据一目了然

有了数据,还需要直观的展示方式。光看一堆 JSON 数据,谁也看不出所以然来。我们可以在页面上添加一个可视化面板,展示请求统计信息。

5.1 简单的面板 UI

先创建一个悬浮的面板,点击可以展开查看详情:

<!-- 在页面中添加这个div -->
<div id="requestStatsPanel" style="position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: white; border: 1px solid #ccc; border-radius: 4px; padding: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
  <div style="cursor: pointer; font-weight: bold;" id="panelHeader">请求统计 (点击展开)</div>
  <div id="panelContent" style="display: none; max-height: 400px; overflow-y: auto; margin-top: 10px; width: 600px;">
    <!-- 统计内容会在这里动态生成 -->
  </div>
</div>

然后写点 JS 控制面板的展开 / 折叠:

// 面板交互
const panelHeader = document.getElementById('panelHeader');
const panelContent = document.getElementById('panelContent');
​
panelHeader.addEventListener('click', () => {
  panelContent.style.display = panelContent.style.display === 'none' ? 'block' : 'none';
  panelHeader.textContent = panelContent.style.display === 'none' ? '请求统计 (点击展开)' : '请求统计 (点击收起)';
  // 展开时更新数据
  updatePanelContent();
});

5.2 数据展示组件

我们需要展示的数据包括:

  • 总请求数、成功数、失败数

  • 平均耗时、最长耗时、最短耗时

  • 按类型分组的统计(XHR、Fetch、资源)

  • 最近的请求列表

先来实现一个更新面板内容的函数:

function updatePanelContent() {
  if (allRequests.length === 0) {
    panelContent.innerHTML = '<p>暂无请求数据</p>';
    return;
  }
​
  // 计算统计指标
  const stats = calculateStats(allRequests);
  
  // 生成HTML
  let html = `
    <div style="margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px dashed #eee;">
      <h4>总体统计</h4>
      <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
        <div>总请求: <strong>${stats.total}</strong></div>
        <div>成功: <strong style="color: green;">${stats.success}</strong></div>
        <div>失败: <strong style="color: red;">${stats.failed}</strong></div>
        <div>平均耗时: <strong>${stats.avgDuration}ms</strong></div>
        <div>最长耗时: <strong>${stats.maxDuration}ms</strong></div>
        <div>最短耗时: <strong>${stats.minDuration}ms</strong></div>
      </div>
    </div>
​
    <div style="margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px dashed #eee;">
      <h4>按类型统计</h4>
      <div style="display: flex; gap: 15px;">
  `;
​
  // 按类型添加统计
  Object.keys(stats.byType).forEach(type => {
    const typeStats = stats.byType[type];
    html += `
      <div>
        <strong>${type.toUpperCase()}</strong>: ${typeStats.count}个<br>
        平均: ${typeStats.avgDuration}ms
      </div>
    `;
  });
​
  html += `
      </div>
    </div>
​
    <div>
      <h4>最近10条请求</h4>
      <table style="width: 100%; border-collapse: collapse; font-size: 12px;">
        <thead>
          <tr style="background: #f5f5f5;">
            <th style="border: 1px solid #eee; padding: 5px; text-align: left;">方法</th>
            <th style="border: 1px solid #eee; padding: 5px; text-align: left; width: 30%;">URL</th>
            <th style="border: 1px solid #eee; padding: 5px; text-align: center;">耗时</th>
            <th style="border: 1px solid #eee; padding: 5px; text-align: center;">状态</th>
          </tr>
        </thead>
        <tbody>
  `;
​
  // 添加最近10条请求
  const recentRequests = [...allRequests].reverse().slice(0, 10);
  recentRequests.forEach(req => {
    const statusClass = req.status === 200 ? 'color: green;' : 'color: red;';
    const durationClass = req.duration > 1000 ? 'color: red;' : (req.duration > 500 ? 'color: orange;' : '');
    html += `
      <tr>
        <td style="border: 1px solid #eee; padding: 5px;">${req.method}</td>
        <td style="border: 1px solid #eee; padding: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${req.url}</td>
        <td style="border: 1px solid #eee; padding: 5px; text-align: center; ${durationClass}">${req.duration}ms</td>
        <td style="border: 1px solid #eee; padding: 5px; text-align: center; ${statusClass}">${req.status || req.error}</td>
      </tr>
    `;
  });
​
  html += `
        </tbody>
      </table>
    </div>
  `;
​
  panelContent.innerHTML = html;
}
​
// 计算统计指标的函数
function calculateStats(requests) {
  const total = requests.length;
  const success = requests.filter(r => r.status >= 200 && r.status < 300 && !r.error).length;
  const failed = total - success;
  
  const durations = requests.map(r => r.duration);
  const avgDuration = Math.round(durations.reduce((sum, d) => sum + d, 0) / total);
  const maxDuration = Math.max(...durations);
  const minDuration = Math.min(...durations);
  
  // 按类型统计
  const byType = {};
  requests.forEach(req => {
    if (!byType[req.type]) {
      byType[req.type] = { count: 0, totalDuration: 0, avgDuration: 0 };
    }
    byType[req.type].count++;
    byType[req.type].totalDuration += req.duration;
  });
  
  // 计算每种类型的平均耗时
  Object.keys(byType).forEach(type => {
    byType[type].avgDuration = Math.round(byType[type].totalDuration / byType[type].count);
  });
  
  return {
    total,
    success,
    failed,
    avgDuration,
    maxDuration,
    minDuration,
    byType
  };
}

这个面板会显示总体统计、按类型统计和最近的请求列表,还会用不同颜色标记耗时过长(>1000ms 标红,>500ms 标橙)和失败的请求,一眼就能看出问题。

5.3 高级可视化:添加图表

如果想更直观地展示数据趋势,可以引入 Chart.js 等图表库。先引入 Chart.js:

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

然后在面板中添加一个图表容器:

<!-- 在panelContent中添加 -->
<div style="margin-bottom: 15px;">
  <h4>耗时分布</h4>
  <canvas id="durationChart" height="200"></canvas>
</div>

再写代码生成图表:

function updateCharts() {
  const ctx = document.getElementById('durationChart').getContext('2d');
  
  // 按类型分组的耗时数据
  const typeData = {};
  allRequests.forEach(req => {
    if (!typeData[req.type]) {
      typeData[req.type] = [];
    }
    typeData[req.type].push(req.duration);
  });
  
  // 准备图表数据
  const labels = Object.keys(typeData);
  const data = labels.map(type => {
    // 计算每种类型的平均耗时
    const avg = typeData[type].reduce((sum, d) => sum + d, 0) / typeData[type].length;
    return Math.round(avg);
  });
  
  // 颜色
  const backgroundColors = [
    'rgba(255, 99, 132, 0.5)',
    'rgba(54, 162, 235, 0.5)',
    'rgba(255, 206, 86, 0.5)'
  ];
  
  // 销毁旧图表(如果存在)
  if (window.durationChart) {
    window.durationChart.destroy();
  }
  
  // 创建新图表
  window.durationChart = new Chart(ctx, {
    type: 'bar',
    data: {
      labels: labels.map(t => t.toUpperCase()),
      datasets: [{
        label: '平均耗时 (ms)',
        data: data,
        backgroundColor: backgroundColors.slice(0, labels.length),
        borderColor: backgroundColors.map(c => c.replace('0.5', '1')),
        borderWidth: 1
      }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true,
          title: {
            display: true,
            text: '耗时 (ms)'
          }
        }
      },
      plugins: {
        tooltip: {
          callbacks: {
            label: function(context) {
              return `平均耗时: ${context.raw}ms`;
            }
          }
        }
      }
    }
  });
}
​
// 在updatePanelContent的最后调用updateCharts
function updatePanelContent() {
  // ... 前面的代码 ...
  updateCharts();
}

这样就会生成一个柱状图,展示不同类型请求的平均耗时,一眼就能看出哪种类型的请求最慢。

六、高级功能:让工具更实用

基础功能完成后,我们可以添加一些高级功能,让工具更实用。

6.1 过滤和搜索

当请求很多时,想找到特定的请求很麻烦,可以添加过滤和搜索功能:

<!-- 在panelContent顶部添加 -->
<div style="margin-bottom: 10px; display: flex; gap: 10px; align-items: center;">
  <input type="text" id="searchInput" placeholder="搜索URL..." style="flex: 1; padding: 5px;">
  <select id="filterType" style="padding: 5px;">
    <option value="all">所有类型</option>
    <option value="xhr">XHR</option>
    <option value="fetch">Fetch</option>
    <option value="resource">资源</option>
  </select>
  <select id="filterStatus" style="padding: 5px;">
    <option value="all">所有状态</option>
    <option value="success">成功</option>
    <option value="failed">失败</option>
  </select>
</div>

然后添加过滤逻辑:

// 监听搜索和过滤变化
document.getElementById('searchInput').addEventListener('input', updateFilteredRequests);
document.getElementById('filterType').addEventListener('change', updateFilteredRequests);
document.getElementById('filterStatus').addEventListener('change', updateFilteredRequests);
​
// 存储过滤后的请求
let filteredRequests = [...allRequests];
​
function updateFilteredRequests() {
  const searchText = document.getElementById('searchInput').value.toLowerCase();
  const filterType = document.getElementById('filterType').value;
  const filterStatus = document.getElementById('filterStatus').value;
  
  filteredRequests = allRequests.filter(req => {
    // 搜索URL
    const matchesSearch = req.url.toLowerCase().includes(searchText);
    
    // 过滤类型
    const matchesType = filterType === 'all' || req.type === filterType;
    
    // 过滤状态
    let matchesStatus = true;
    if (filterStatus === 'success') {
      matchesStatus = req.status >= 200 && req.status < 300 && !req.error;
    } else if (filterStatus === 'failed') {
      matchesStatus = !(req.status >= 200 && req.status < 300 && !req.error);
    }
    
    return matchesSearch && matchesType && matchesStatus;
  });
  
  // 更新面板内容(需要修改updatePanelContent使用filteredRequests)
  updatePanelContent();
}
​
// 修改calculateStats和updatePanelContent,使用filteredRequests而不是allRequests
function updatePanelContent() {
  if (filteredRequests.length === 0) {
    panelContent.innerHTML = '<p>没有匹配的请求数据</p>';
    return;
  }
  // ... 其余代码使用filteredRequests ...
}
​
function calculateStats(requests) {
  // 保持不变,但传入的是filteredRequests
}

现在用户可以通过 URL 关键词、请求类型、成功 / 失败状态来过滤请求,查找特定请求变得很方便。

6.2 性能预警

当请求耗时超过阈值(比如 2 秒)时,可以自动提醒开发者:

// 预警阈值(毫秒)
const WARNING_THRESHOLD = 2000;
​
// 修改collectRequestData,添加预警逻辑
function collectRequestData(data) {
  // ... 前面的代码 ...
  
  // 如果耗时超过阈值,发出预警
  if (request.duration > WARNING_THRESHOLD) {
    console.warn(`[性能预警] 请求 ${request.method} ${request.url} 耗时过长: ${request.duration}ms`);
    
    // 在面板上显示预警
    showWarning(`请求 ${request.url} 耗时过长 (${request.duration}ms)`);
  }
  
  // ... 后面的代码 ...
}
​
// 显示预警的函数
function showWarning(message) {
  const warningDiv = document.createElement('div');
  warningDiv.style.cssText = `
    position: fixed; top: 20px; left: 50%; transform: translateX(-50%); 
    background: #fff3cd; color: #856404; padding: 10px 20px; border: 1px solid #ffeeba;
    border-radius: 4px; z-index: 10000; animation: fadein 0.5s;
  `;
  warningDiv.textContent = message;
  document.body.appendChild(warningDiv);
  
  // 3秒后自动消失
  setTimeout(() => {
    warningDiv.style.opacity = '0';
    warningDiv.style.transition = 'opacity 0.5s';
    setTimeout(() => warningDiv.remove(), 500);
  }, 3000);
}

这样当有请求耗时过长时,会在控制台警告并在页面顶部显示提示,开发者能及时发现问题。

6.3 环境区分

在开发环境需要详细的统计,但生产环境可能只需要上报关键数据,避免影响用户体验:

// 环境变量(实际项目中可以通过构建工具注入)
const ENV = 'development'; // 'development' 或 'production'
​
// 修改collectRequestData,根据环境决定是否显示控制台日志
function collectRequestData(data) {
  // ... 前面的代码 ...
  
  // 开发环境才在控制台打印
  if (ENV === 'development') {
    console.log(`[请求统计] ${request.method} ${request.url} 耗时: ${request.duration}ms 状态: ${request.status}`);
  }
  
  // 生产环境只上报,不显示面板
  if (ENV === 'production') {
    document.getElementById('requestStatsPanel').style.display = 'none';
  }
  
  // ... 后面的代码 ...
}

这样既能在开发时方便调试,又不会在生产环境打扰用户。

七、总结与扩展

到这里,一个功能完整的全站请求耗时统计工具就设计完成了。它能:

  • 捕获 XHR、Fetch、资源三种类型的请求

  • 记录详细的请求信息和环境数据

  • 本地存储和批量上报数据

  • 可视化展示统计结果和图表

  • 支持过滤搜索和性能预警

这个工具就像给网站装了个 "速度仪表盘",让原本模糊的 "慢" 变得可量化、可分析。以后产品经理再说 "用户说慢",你就可以打开面板,指着数据说:"根据统计,首页的 banner 图片平均加载需要 2.3 秒,是主要瓶颈,我们可以优化图片大小"—— 有理有据,再也不用凭感觉优化了。

可以扩展的功能:

  1. 更详细的错误分析(记录错误堆栈、响应内容)

  2. 与后端配合实现多用户数据聚合分析

  3. 自定义阈值配置:允许开发者根据接口特性设置不同的耗时阈值(比如文件上传接口允许 5 秒,普通接口限制 2 秒)。

    // 自定义阈值配置
    const THRESHOLD_CONFIG = {
      default: 2000, // 默认阈值
      '/api/upload': 5000, // 上传接口放宽到5秒
      '/api/large-data': 3000 // 大数据接口放宽到3秒
    };
    ​
    // 修改预警逻辑,支持自定义阈值
    function getThresholdByUrl(url) {
      // 查找是否有匹配的自定义阈值
      const matchedKey = Object.keys(THRESHOLD_CONFIG).find(key => url.includes(key));
      return matchedKey ? THRESHOLD_CONFIG[matchedKey] : THRESHOLD_CONFIG.default;
    }
    ​
    // 在collectRequestData中使用
    if (request.duration > getThresholdByUrl(request.url)) {
      console.warn(`[性能预警] 请求 ${request.method} ${request.url} 耗时过长: ${request.duration}ms`);
      showWarning(`请求 ${request.url} 耗时过长 (${request.duration}ms)`);
    }
    1. 用户行为关联:记录请求发生时的用户操作(如 "点击登录按钮后发起的 /login 请求"),便于定位问题场景。

      // 记录用户行为的函数
      let lastAction = '';
      function recordUserAction(action) {
        lastAction = `${new Date().toLocaleTimeString()}:${action}`;
      }
      ​
      // 在关键用户操作处调用
      document.getElementById('loginBtn').addEventListener('click', () => {
        recordUserAction('点击登录按钮');
      });
      ​
      // 在请求数据中添加用户行为
      function collectRequestData(data) {
        // ...
        const request = { 
          ...data, 
          ...envInfo,
          userAction: lastAction // 关联最近的用户行为
        };
        // ...
      }
    2. 性能趋势分析:存储多天的统计数据,生成日 / 周 / 月趋势图表,观察性能变化。

    八、实战部署:从代码到产品

    设计完工具后,还需要考虑如何在实际项目中部署使用。以下是一些实用建议:

    8.1 模块化封装

    将工具封装成独立模块,方便在多个项目中复用:

    // request-stats.js
    export default class RequestStats {
      constructor(options = {}) {
        this.options = {
          env: 'development',
          threshold: 2000,
          reportUrl: 'https://api.example.com/report',
          ...options
        };
        this.allRequests = [];
        this.init();
      }
    ​
      // 初始化监控
      init() {
        this.hookXHR();
        this.hookFetch();
        this.monitorResources();
        if (this.options.env === 'development') {
          this.createPanel();
        }
      }
    ​
      // XHR监控方法
      hookXHR() { /* ... 之前的XHR监控代码 ... */ }
    ​
      // Fetch监控方法
      hookFetch() { /* ... 之前的Fetch监控代码 ... */ }
    ​
      // 资源监控方法
      monitorResources() { /* ... 之前的资源监控代码 ... */ }
    ​
      // 创建面板方法
      createPanel() { /* ... 之前的面板创建代码 ... */ }
    ​
      // 数据收集方法
      collectData(data) { /* ... 之前的数据收集代码 ... */ }
    ​
      // 数据上报方法
      report(data) { /* ... 之前的上报代码 ... */ }
    }

    使用时只需引入并实例化:

    import RequestStats from './request-stats.js';
    ​
    // 初始化工具
    new RequestStats({
      env: process.env.NODE_ENV, // 从环境变量获取当前环境
      threshold: 1500, // 自定义默认阈值
      reportUrl: '/api/performance/report'
    });

    8.2 按需加载

    在生产环境中,可以根据用户角色或 URL 参数决定是否启用工具,避免对普通用户造成影响:

    // 只对管理员或测试账号启用
    const userRole = getCurrentUserRole(); // 获取当前用户角色
    if (userRole === 'admin' || location.search.includes('debug=1')) {
      import('./request-stats.js').then(({ default: RequestStats }) => {
        new RequestStats({ env: 'production' });
      });
    }

    8.3 性能影响控制

    监控工具本身也可能影响页面性能,需要注意:

    • 避免在短时间内处理大量数据(如一次性加载 1000 + 请求)

    • 上报数据时使用防抖 / 节流,减少请求次数

    • 不在主线程做复杂计算(可使用 Web Worker 处理大数据分析)

    // 使用Web Worker处理数据计算
    // stats-worker.js
    self.onmessage = (e) => {
      const stats = calculateStats(e.data); // 复杂计算在Worker中进行
      self.postMessage(stats);
    };
    ​
    // 在主线程中使用
    const worker = new Worker('stats-worker.js');
    worker.postMessage(allRequests);
    worker.onmessage = (e) => {
      console.log('计算结果', e.data);
      updatePanelWithStats(e.data);
    };

    九、结语:让数据驱动性能优化

    "页面慢" 是前端开发中最常见也最棘手的问题之一。没有数据支撑时,优化就像盲人摸象 —— 你以为是图片太大,实际是接口响应慢;你以为是代码冗余,实际是 CDN 节点故障。

    本文设计的全站请求耗时统计工具,就像给开发者装上了 "性能显微镜":

    • 它能告诉你具体哪个请求慢(定位问题点)

    • 能告诉你慢了多久(量化问题严重程度)

    • 能告诉你什么时候慢(发现时间规律)

    • 能告诉你在什么环境下慢(排查环境因素)

    有了这些数据,性能优化就从 "凭感觉" 变成了 "按数据"—— 先通过工具找到性能瓶颈,再针对性优化,最后用工具验证优化效果。这才是科学的性能优化流程。

    最后送大家一句名言:"你无法改进你无法衡量的东西。"(You can't improve what you can't measure.)希望这个工具能帮助你衡量并改进你的网站性能,让 "用户说慢" 不再是难题。

    现在,不妨把这个工具加到你的项目中,看看你的网站到底哪里 "慢" 吧!


网站公告

今日签到

点亮在社区的每一天
去签到