<!DOCTYPE html> |
| ---------------------------------------------------------------------------------------------------------- |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>H5页面性能监控工具</title> |
| <script src="<https://cdn.jsdelivr.net/npm/chart.js>"></script> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| } |
| |
| body { |
| background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d); |
| color: #fff; |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| } |
| |
| header { |
| text-align: center; |
| margin-bottom: 30px; |
| padding: 20px; |
| background: rgba(0, 0, 0, 0.3); |
| border-radius: 15px; |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); |
| } |
| |
| h1 { |
| font-size: 2.5rem; |
| margin-bottom: 10px; |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
| } |
| |
| .subtitle { |
| font-size: 1.2rem; |
| opacity: 0.9; |
| } |
| |
| .dashboard { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px; |
| margin-bottom: 30px; |
| } |
| |
| @media (max-width: 768px) { |
| .dashboard { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .card { |
| background: rgba(255, 255, 255, 0.1); |
| backdrop-filter: blur(10px); |
| border-radius: 15px; |
| padding: 20px; |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); |
| transition: transform 0.3s ease; |
| } |
| |
| .card:hover { |
| transform: translateY(-5px); |
| } |
| |
| .card h2 { |
| font-size: 1.5rem; |
| margin-bottom: 15px; |
| border-bottom: 2px solid rgba(255, 255, 255, 0.3); |
| padding-bottom: 10px; |
| } |
| |
| .metrics { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| gap: 15px; |
| margin-bottom: 30px; |
| } |
| |
| .metric-card { |
| background: rgba(255, 255, 255, 0.15); |
| border-radius: 10px; |
| padding: 15px; |
| text-align: center; |
| transition: all 0.3s ease; |
| } |
| |
| .metric-card:hover { |
| background: rgba(255, 255, 255, 0.25); |
| transform: scale(1.05); |
| } |
| |
| .metric-value { |
| font-size: 1.8rem; |
| font-weight: bold; |
| margin: 10px 0; |
| } |
| |
| .metric-label { |
| font-size: 0.9rem; |
| opacity: 0.8; |
| } |
| |
| .chart-container { |
| height: 300px; |
| margin-top: 20px; |
| } |
| |
| .timeline { |
| margin-top: 20px; |
| } |
| |
| .timeline-item { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 10px; |
| padding: 8px 15px; |
| background: rgba(255, 255, 255, 0.1); |
| border-radius: 8px; |
| } |
| |
| .timeline-label { |
| font-weight: bold; |
| } |
| |
| .timeline-value { |
| color: #fdbb2d; |
| } |
| |
| .buttons { |
| display: flex; |
| gap: 15px; |
| margin-top: 20px; |
| } |
| |
| button { |
| background: #fdbb2d; |
| color: #1a2a6c; |
| border: none; |
| padding: 12px 25px; |
| border-radius: 8px; |
| font-weight: bold; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| flex: 1; |
| } |
| |
| button:hover { |
| background: #ffcc44; |
| transform: translateY(-2px); |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); |
| } |
| |
| button:active { |
| transform: translateY(0); |
| } |
| |
| .resources { |
| margin-top: 20px; |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .resource-item { |
| display: flex; |
| justify-content: space-between; |
| padding: 10px; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| |
| .resource-name { |
| flex: 2; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .resource-size, .resource-time { |
| flex: 1; |
| text-align: right; |
| } |
| |
| footer { |
| text-align: center; |
| margin-top: 30px; |
| padding: 20px; |
| font-size: 0.9rem; |
| opacity: 0.8; |
| } |
| |
| .good { |
| color: #4CAF50; |
| } |
| |
| .medium { |
| color: #FFC107; |
| } |
| |
| .poor { |
| color: #F44336; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1>H5页面性能监控工具</h1> |
| <p class="subtitle">基于浏览器Performance API的页面性能分析</p> |
| </header> |
| |
| <div class="metrics"> |
| <div class="metric-card"> |
| <div class="metric-label">DOM解析完成</div> |
| <div class="metric-value" id="domComplete">0ms</div> |
| <div class="metric-label">domComplete</div> |
| </div> |
| <div class="metric-card"> |
| <div class="metric-label">首次内容绘制</div> |
| <div class="metric-value" id="fcp">0ms</div> |
| <div class="metric-label">First Contentful Paint</div> |
| </div> |
| <div class="metric-card"> |
| <div class="metric-label">最大内容绘制</div> |
| <div class="metric-value" id="lcp">0ms</div> |
| <div class="metric-label">Largest Contentful Paint</div> |
| </div> |
| <div class="metric-card"> |
| <div class="metric-label">首次输入延迟</div> |
| <div class="metric-value" id="fid">0ms</div> |
| <div class="metric-label">First Input Delay</div> |
| </div> |
| </div> |
| |
| <div class="dashboard"> |
| <div class="card"> |
| <h2>页面加载时间线</h2> |
| <div class="chart-container"> |
| <canvas id="timelineChart"></canvas> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h2>性能指标详情</h2> |
| <div class="timeline"> |
| <div class="timeline-item"> |
| <span class="timeline-label">重定向时间</span> |
| <span class="timeline-value" id="redirectTime">0ms</span> |
| </div> |
| <div class="timeline-item"> |
| <span class="timeline-label">DNS查询时间</span> |
| <span class="timeline-value" id="dnsLookupTime">0ms</span> |
| </div> |
| <div class="timeline-item"> |
| <span class="timeline-label">TCP连接时间</span> |
| <span class="timeline-value" id="tcpTime">0ms</span> |
| </div> |
| <div class="timeline-item"> |
| <span class="timeline-label">请求响应时间</span> |
| <span class="timeline-value" id="requestTime">0ms</span> |
| </div> |
| <div class="timeline-item"> |
| <span class="timeline-label">DOM解析时间</span> |
| <span class="timeline-value" id="domParseTime">0ms</span> |
| </div> |
| <div class="timeline-item"> |
| <span class="timeline-label">页面完全加载</span> |
| <span class="timeline-value" id="loadTime">0ms</span> |
| </div> |
| </div> |
| |
| <div class="buttons"> |
| <button id="refreshBtn">刷新数据</button> |
| <button id="simulateLoadBtn">模拟资源加载</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h2>资源加载详情</h2> |
| <div class="resources" id="resourcesList"> |
| <!-- 资源列表将在这里动态生成 --> |
| </div> |
| </div> |
| |
| <footer> |
| <p>H5性能监控工具 © 2023 | 基于Performance API实现</p> |
| </footer> |
| </div> |
| |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| // 初始化图表 |
| const timelineCtx = document.getElementById('timelineChart').getContext('2d'); |
| const timelineChart = new Chart(timelineCtx, { |
| type: 'bar', |
| data: { |
| labels: ['重定向', 'DNS查询', 'TCP连接', '请求响应', 'DOM解析', '页面加载'], |
| datasets: [{ |
| label: '时间(ms)', |
| data: [0, 0, 0, 0, 0, 0], |
| backgroundColor: [ |
| 'rgba(255, 99, 132, 0.7)', |
| 'rgba(54, 162, 235, 0.7)', |
| 'rgba(255, 206, 86, 0.7)', |
| 'rgba(75, 192, 192, 0.7)', |
| 'rgba(153, 102, 255, 0.7)', |
| 'rgba(255, 159, 64, 0.7)' |
| ], |
| borderColor: [ |
| 'rgba(255, 99, 132, 1)', |
| 'rgba(54, 162, 235, 1)', |
| 'rgba(255, 206, 86, 1)', |
| 'rgba(75, 192, 192, 1)', |
| 'rgba(153, 102, 255, 1)', |
| 'rgba(255, 159, 64, 1)' |
| ], |
| borderWidth: 1 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| scales: { |
| y: { |
| beginAtZero: true, |
| ticks: { |
| color: 'white' |
| }, |
| grid: { |
| color: 'rgba(255, 255, 255, 0.1)' |
| } |
| }, |
| x: { |
| ticks: { |
| color: 'white' |
| }, |
| grid: { |
| color: 'rgba(255, 255, 255, 0.1)' |
| } |
| } |
| }, |
| plugins: { |
| legend: { |
| labels: { |
| color: 'white' |
| } |
| } |
| } |
| } |
| }); |
| |
| // 获取性能数据 |
| function getPerformanceData() { |
| // 使用Performance Timeline API |
| const performance = window.performance; |
| if (!performance) { |
| alert('您的浏览器不支持Performance API'); |
| return; |
| } |
| |
| // 获取导航计时数据 |
| const timing = performance.timing; |
| const navigation = performance.getEntriesByType('navigation')[0]; |
| |
| // 计算各个阶段的时间 |
| const redirectTime = timing.redirectEnd - timing.redirectStart; |
| const dnsLookupTime = timing.domainLookupEnd - timing.domainLookupStart; |
| const tcpTime = timing.connectEnd - timing.connectStart; |
| const requestTime = timing.responseEnd - timing.requestStart; |
| const domParseTime = timing.domComplete - timing.domLoading; |
| const loadTime = timing.loadEventEnd - timing.navigationStart; |
| |
| // 更新时间线图表 |
| timelineChart.data.datasets[0].data = [ |
| redirectTime, dnsLookupTime, tcpTime, requestTime, domParseTime, loadTime |
| ]; |
| timelineChart.update(); |
| |
| // 更新指标显示 |
| document.getElementById('redirectTime').textContent = redirectTime + 'ms'; |
| document.getElementById('dnsLookupTime').textContent = dnsLookupTime + 'ms'; |
| document.getElementById('tcpTime').textContent = tcpTime + 'ms'; |
| document.getElementById('requestTime').textContent = requestTime + 'ms'; |
| document.getElementById('domParseTime').textContent = domParseTime + 'ms'; |
| document.getElementById('loadTime').textContent = loadTime + 'ms'; |
| |
| // 更新核心性能指标 |
| document.getElementById('domComplete').textContent = (timing.domComplete - timing.navigationStart) + 'ms'; |
| |
| // 获取FCP (First Contentful Paint) |
| const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0]; |
| if (fcpEntry) { |
| document.getElementById('fcp').textContent = Math.round(fcpEntry.startTime) + 'ms'; |
| // 根据FCP时间添加颜色标识 |
| const fcpElement = document.getElementById('fcp'); |
| fcpElement.className = 'metric-value ' + |
| (fcpEntry.startTime < 1000 ? 'good' : |
| fcpEntry.startTime < 3000 ? 'medium' : 'poor'); |
| } |
| |
| // 获取LCP (Largest Contentful Paint) |
| const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); |
| if (lcpEntries.length > 0) { |
| const lcp = lcpEntries[lcpEntries.length - 1]; |
| document.getElementById('lcp').textContent = Math.round(lcp.startTime) + 'ms'; |
| // 根据LCP时间添加颜色标识 |
| const lcpElement = document.getElementById('lcp'); |
| lcpElement.className = 'metric-value ' + |
| (lcp.startTime < 2500 ? 'good' : |
| lcp.startTime < 4000 ? 'medium' : 'poor'); |
| } |
| |
| // 获取FID (First Input Delay) |
| const fidEntries = performance.getEntriesByType('first-input'); |
| if (fidEntries.length > 0) { |
| const fid = fidEntries[0]; |
| document.getElementById('fid').textContent = Math.round(fid.processingStart - fid.startTime) + 'ms'; |
| // 根据FID时间添加颜色标识 |
| const fidElement = document.getElementById('fid'); |
| const fidValue = fid.processingStart - fid.startTime; |
| fidElement.className = 'metric-value ' + |
| (fidValue < 100 ? 'good' : |
| fidValue < 300 ? 'medium' : 'poor'); |
| } |
| |
| // 获取资源加载信息 |
| const resources = performance.getEntriesByType('resource'); |
| const resourcesList = document.getElementById('resourcesList'); |
| resourcesList.innerHTML = ''; |
| |
| resources.forEach(resource => { |
| const resourceItem = document.createElement('div'); |
| resourceItem.className = 'resource-item'; |
| |
| const name = document.createElement('div'); |
| name.className = 'resource-name'; |
| name.textContent = resource.name; |
| |
| const size = document.createElement('div'); |
| size.className = 'resource-size'; |
| size.textContent = (resource.transferSize / 1024).toFixed(2) + ' KB'; |
| |
| const time = document.createElement('div'); |
| time.className = 'resource-time'; |
| time.textContent = Math.round(resource.duration) + 'ms'; |
| |
| resourceItem.appendChild(name); |
| resourceItem.appendChild(size); |
| resourceItem.appendChild(time); |
| |
| resourcesList.appendChild(resourceItem); |
| }); |
| } |
| |
| // 初始加载时获取性能数据 |
| // 使用setTimeout确保在页面完全加载后获取数据 |
| setTimeout(getPerformanceData, 500); |
| |
| // 刷新按钮事件 |
| document.getElementById('refreshBtn').addEventListener('click', function() { |
| getPerformanceData(); |
| }); |
| |
| // 模拟资源加载按钮事件 |
| document.getElementById('simulateLoadBtn').addEventListener('click', function() { |
| // 创建一些图片资源来模拟资源加载 |
| for (let i = 0; i < 3; i++) { |
| const img = new Image(); |
| img.src = `https://picsum.photos/400/200?random=${Date.now()}-${i}`; |
| document.body.appendChild(img); |
| } |
| |
| // 延迟一下再获取更新的性能数据 |
| setTimeout(getPerformanceData, 1000); |
| }); |
| |
| // 监听性能条目变化 |
| if (window.PerformanceObserver) { |
| // 观察LCP变化 |
| const lcpObserver = new PerformanceObserver((entryList) => { |
| const entries = entryList.getEntries(); |
| const lastEntry = entries[entries.length - 1]; |
| document.getElementById('lcp').textContent = Math.round(lastEntry.startTime) + 'ms'; |
| }); |
| |
| lcpObserver.observe({entryTypes: ['largest-contentful-paint']}); |
| |
| // 观察FID变化 |
| const fidObserver = new PerformanceObserver((entryList) => { |
| const entries = entryList.getEntries(); |
| const firstEntry = entries[0]; |
| document.getElementById('fid').textContent = |
| Math.round(firstEntry.processingStart - firstEntry.startTime) + 'ms'; |
| }); |
| |
| fidObserver.observe({entryTypes: ['first-input']}); |
| } |
| }); |
| </script> |
| </body> |
| </html>
其他可能用到的性能数据
// 计算页面完全加载(load 事件)耗时
const timing = window.performance.timing
const loadTime = timing.loadEventEnd - timing.navigationStart
console.log(`页面完全加载耗时:${loadTime}ms`)
const performance = window.performance
const paintEntries = performance.getEntriesByType('paint')
// 获取 FCP (首次内容绘制) 时间:
paintEntries.forEach((entry) => {
console.log('entry===', entry)
if (entry.name === 'first-paint') {
console.log(`FMP: ${entry.startTime}ms`)
}
})
// 统计所有图片资源的加载时间:
const resources = performance.getEntriesByType('resource')
const images = resources.filter(r => r.initiatorType === 'img')
images.forEach(img => {
console.log(`图片 ${img.name} A加载耗时:${img.duration}ms`)
})
const navEntry = performance.getEntriesByType('navigation')[0]
console.log('WebView初始化完成时间(近似):' + navEntry.fetchStart + 'ms')
performance.mark('component-start')
// 组件渲染完成后(可结合nextTick确保DOM更新)
this.$nextTick(() => {
performance.mark('component-end')
const duration = performance.measure('component-init', 'component-start', 'component-end').duration
console.log('组件创建完成耗时:' + duration + 'ms')
})
// 获取所有资源性能条目
const resourceEntries = performance.getEntriesByType('resource')
// 分类存储不同类型资源的加载时间
const resourceStats = {
js: [],
css: [],
image: []
}
resourceEntries.forEach(entry => {
console.log('entry==A==', entry)
const duration = entry.responseEnd - entry.fetchStart
const initiatorType = entry.initiatorType
const entryName = entry.name
if (initiatorType === 'link' && entryName.endsWith('.js')) {
resourceStats.js.push({ url: entry.name, duration })
} else if (initiatorType === 'link' && entryName.endsWith('.css')) {
resourceStats.css.push({ url: entry.name, duration })
} else if (initiatorType === 'link' && (entryName.endsWith('.jpg') || entryName.endsWith('.jpeg') || entryName.endsWith('.png'))) {
resourceStats.image.push({ url: entry.name, duration })
}
// switch (entry.initiatorType) {
// case 'script':
// resourceStats.js.push({ url: entry.name, duration })
// break
// case 'link':
// resourceStats.css.push({ url: entry.name, duration })
// break
// case 'img':
// resourceStats.image.push({ url: entry.name, duration })
// break
// // 可扩展其他类型(如 'xmlhttprequest' 对应 AJAX)
// }
})
// 打印统计结果
console.log('JS 资源加载时间:', resourceStats.js)
console.log('CSS 资源加载时间:', resourceStats.css)
console.log('图片资源加载时间:', resourceStats.image)
// 计算每种资源的平均耗时(可选)
const avgJs = resourceStats.js.reduce((sum, item) => sum + item.duration, 0) / resourceStats.js.length || 0
const avgCss = resourceStats.css.reduce((sum, item) => sum + item.duration, 0) / resourceStats.css.length || 0
const avgImage = resourceStats.image.reduce((sum, item) => sum + item.duration, 0) / resourceStats.image.length || 0
console.log(`JS 资源平均加载耗时:${avgJs.toFixed(2)}ms`)
console.log(`CSS 资源平均加载耗时:${avgCss.toFixed(2)}ms`)
console.log(`图片资源平均加载耗时:${avgImage.toFixed(2)}ms`)
// 获取导航性能条目(现代浏览器推荐方式)
const navEntries = performance.getEntriesByType('navigation')
console.log('navEntries===', navEntries)
if (navEntries.length > 0) {
const nav = navEntries[0]
// DNS 耗时
const dnsTime = nav.domainLookupEnd - nav.domainLookupStart
// TCP 连接耗时
const connectTime = nav.connectEnd - nav.connectStart
// 服务器加载时间(请求+响应)
const serverTime = nav.responseEnd - nav.requestStart
console.log(`DNS 耗时:${dnsTime.toFixed(2)}ms`)
console.log(`TCP 连接耗时:${connectTime.toFixed(2)}ms`)
console.log(`服务器加载时间:${serverTime.toFixed(2)}ms`)
}
// 最大内容绘制时间(页面加载过程中最大内容元素呈现的时间)
new PerformanceObserver((entries) => {
const lcpEntry = entries.getEntries()[0]
console.log(`最大内容绘制(LCP):${lcpEntry.startTime}ms`)
}).observe({ type: 'largest-contentful-paint', buffered: true })
// 累积布局偏移(页面元素意外移动的总评分,反映视觉稳定性)
new PerformanceObserver((entryList) => {
// 先通过 getEntries() 获取条目数组
const entries = entryList.getEntries()
entries.forEach(entry => {
if (!entry.hadRecentInput) { // 排除用户输入导致的偏移
console.log(`布局偏移:${entry.value}`)
}
})
}).observe({ type: 'layout-shift', buffered: true })
// 长任务(执行时间超过 50ms 的任务)会阻塞主线程,导致页面卡顿。
new PerformanceObserver((entryList) => {
// 先通过 getEntries() 获取条目数组
const entries = entryList.getEntries()
entries.forEach(task => {
console.log(`长任务耗时:${task.duration}ms,开始于:${task.startTime}ms`)
})
}).observe({ type: 'longtask', buffered: true })
console.log('当前 JS 堆内存使用:', performance.memory.usedJSHeapSize)
// 测量函数执行耗时:
// performance.mark('functionStart') // 标记开始
// // 执行目标函数
// heavyFunction()
// performance.mark('functionEnd') // 标记结束
// performance.measure('functionDuration', 'functionStart', 'functionEnd') // 计算耗时
// const measure = performance.getEntriesByName('functionDuration')[0]
// console.log(`函数执行耗时:${measure.duration}ms`)