接续上文:网站酷炫换皮肤?——PC 端 H5 换肤方案实战分享-CSDN博客
主页: 有更多有趣的实例教程哟!!!!!!!
专栏有更多内容:
https://blog.csdn.net/m0_73589512/category_13028539.html
目录
《打破 "慢" 的黑箱:前端请求全链路耗时统计方案》
在前端开发的日常中,我们总会遇到这样的灵魂拷问:
产品经理:"用户说页面加载好慢啊!赶紧优化一下!" 你:"有多慢?具体哪个接口慢?" 产品经理:"用户就是说慢啊!你自己感受一下嘛!" 你内心 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 浏览器上出现,那可能是浏览器兼容性导致的问题。
三、技术方案设计:怎么捕获这些数据?
明确了要统计什么,接下来就是怎么实现 —— 怎么 "抓" 到这些请求的数据。
前端的请求主要有三种类型:
XMLHttpRequest(传统的 Ajax 请求)
Fetch API(现代的请求方式)
页面资源请求(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); } }
这个函数做了几件事:
补充环境信息(当前页面、浏览器、网络类型等)
合并数据并存储到数组中
控制台打印(方便开发时查看)
简单的内存管理(避免数组无限增长)
四、数据存储与上报:怎么保存和传输数据?
收集了数据,接下来要考虑怎么存储和上报。如果只是在控制台看,刷新页面就没了,没什么用。
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 秒,是主要瓶颈,我们可以优化图片大小"—— 有理有据,再也不用凭感觉优化了。
可以扩展的功能:
更详细的错误分析(记录错误堆栈、响应内容)
与后端配合实现多用户数据聚合分析
自定义阈值配置:允许开发者根据接口特性设置不同的耗时阈值(比如文件上传接口允许 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)`); }
用户行为关联:记录请求发生时的用户操作(如 "点击登录按钮后发起的 /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 // 关联最近的用户行为 }; // ... }
性能趋势分析:存储多天的统计数据,生成日 / 周 / 月趋势图表,观察性能变化。
八、实战部署:从代码到产品
设计完工具后,还需要考虑如何在实际项目中部署使用。以下是一些实用建议:
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.)希望这个工具能帮助你衡量并改进你的网站性能,让 "用户说慢" 不再是难题。
现在,不妨把这个工具加到你的项目中,看看你的网站到底哪里 "慢" 吧!