一、技术实现方案
1.1 性能监控架构
性能监控系统
├── 指标采集层
│ ├── Performance API
│ ├── PerformanceObserver
│ └── Navigation Timing
│
├── 指标计算层
│ ├── FCP (First Contentful Paint)
│ ├── LCP (Largest Contentful Paint)
│ ├── FID (First Input Delay)
│ ├── CLS (Cumulative Layout Shift)
│ └── TTFB (Time to First Byte)
│
├── 数据处理层
│ ├── 数据聚合
│ ├── 数据缓存
│ └── 数据上报
│
└── 监控展示层
├── 实时监控面板
├── 性能趋势图
└── 性能报告
1.2 技术栈
- 性能API: Performance API, PerformanceObserver
- 前端框架: Vue 3 Composition API
- 数据可视化: ECharts
- 数据上报: Beacon API / fetch
二、Performance API 应用
2.1 性能数据采集器
performance-monitor.js
export class PerformanceMonitor {
constructor() {
this.metrics = {
fcp: 0,
lcp: 0,
fid: 0,
cls: 0,
ttfb: 0,
domReady: 0,
loadComplete: 0
}
this.observers = []
this.init()
}
// 初始化监控
init() {
// 等待页面加载完成
if (document.readyState === 'complete') {
this.collectMetrics()
} else {
window.addEventListener('load', () => {
this.collectMetrics()
})
}
}
// 采集基础指标
collectMetrics() {
this.collectNavigationTiming()
this.collectWebVitals()
this.observeLongTasks()
}
// 采集导航时间
collectNavigationTiming() {
const timing = performance.timing
// DNS 查询时间
this.metrics.dns = timing.domainLookupEnd - timing.domainLookupStart
// TCP 连接时间
this.metrics.tcp = timing.connectEnd - timing.connectStart
// TTFB (Time to First Byte)
this.metrics.ttfb = timing.responseStart - timing.navigationStart
// DOM 解析时间
this.metrics.domParse = timing.domInteractive - timing.domLoading
// DOM Ready 时间
this.metrics.domReady = timing.domContentLoadedEventEnd - timing.navigationStart
// 页面完全加载时间
this.metrics.loadComplete = timing.loadEventEnd - timing.navigationStart
// 白屏时间 (首次渲染)
this.metrics.firstPaint = timing.responseEnd - timing.fetchStart
console.log('Navigation Timing:', this.metrics)
}
// 采集 Web Vitals 指标
collectWebVitals() {
// FCP - First Contentful Paint
this.observeFCP()
// LCP - Largest Contentful Paint
this.observeLCP()
// FID - First Input Delay
this.observeFID()
// CLS - Cumulative Layout Shift
this.observeCLS()
}
// 监听 FCP
observeFCP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime
console.log('FCP:', entry.startTime)
observer.disconnect()
}
}
})
observer.observe({ entryTypes: ['paint'] })
this.observers.push(observer)
}
// 监听 LCP
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = lastEntry.startTime
console.log('LCP:', lastEntry.startTime, lastEntry.element)
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
this.observers.push(observer)
}
// 监听 FID
observeFID() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime
console.log('FID:', this.metrics.fid)
observer.disconnect()
}
})
observer.observe({ entryTypes: ['first-input'] })
this.observers.push(observer)
}
// 监听 CLS
observeCLS() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
this.metrics.cls = clsValue
console.log('CLS:', clsValue)
}
}
})
observer.observe({ entryTypes: ['layout-shift'] })
this.observers.push(observer)
}
// 监听长任务
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long Task detected:', {
duration: entry.duration,
startTime: entry.startTime
})
// 记录长任务
if (!this.metrics.longTasks) {
this.metrics.longTasks = []
}
this.metrics.longTasks.push({
duration: entry.duration,
startTime: entry.startTime
})
}
}
})
try {
observer.observe({ entryTypes: ['longtask'] })
this.observers.push(observer)
} catch (e) {
console.warn('Long Task API not supported')
}
}
// 获取所有指标
getMetrics() {
return { ...this.metrics }
}
// 销毁监控
destroy() {
this.observers.forEach(observer => observer.disconnect())
this.observers = []
}
}
2.2 性能监控组件
PerformanceMonitor.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { PerformanceMonitor } from './performance-monitor.js'
const performanceMonitor = ref(null)
const metrics = ref({})
const isMonitoring = ref(false)
// 性能评分标准
const performanceStandards = {
fcp: { good: 1800, needsImprovement: 3000 },
lcp: { good: 2500, needsImprovement: 4000 },
fid: { good: 100, needsImprovement: 300 },
cls: { good: 0.1, needsImprovement: 0.25 },
ttfb: { good: 800, needsImprovement: 1800 }
}
// 初始化监控
const initMonitor = () => {
performanceMonitor.value = new PerformanceMonitor()
isMonitoring.value = true
// 5秒后获取指标
setTimeout(() => {
metrics.value = performanceMonitor.value.getMetrics()
}, 5000)
}
// 刷新指标
const refreshMetrics = () => {
if (performanceMonitor.value) {
metrics.value = performanceMonitor.value.getMetrics()
}
}
// 获取指标评分
const getMetricScore = (metricName, value) => {
const standard = performanceStandards[metricName]
if (!standard) return 'unknown'
if (value <= standard.good) return 'good'
if (value <= standard.needsImprovement) return 'needs-improvement'
return 'poor'
}
// 获取评分颜色
const getScoreColor = (score) => {
const colors = {
good: '#67c23a',
'needs-improvement': '#e6a23c',
poor: '#f56c6c',
unknown: '#909399'
}
return colors[score] || colors.unknown
}
// 格式化时间
const formatTime = (ms) => {
if (!ms && ms !== 0) return '-'
return `${ms.toFixed(2)} ms`
}
// 格式化 CLS
const formatCLS = (value) => {
if (!value && value !== 0) return '-'
return value.toFixed(3)
}
// 模拟性能问题
const simulatePerformanceIssue = () => {
// 模拟长任务
const startTime = Date.now()
while (Date.now() - startTime < 200) {
// 占用主线程
}
// 模拟布局抖动
const div = document.createElement('div')
div.style.width = '100px'
div.style.height = '100px'
document.body.appendChild(div)
setTimeout(() => {
div.style.width = '200px'
div.style.height = '200px'
}, 100)
setTimeout(() => {
document.body.removeChild(div)
}, 200)
setTimeout(refreshMetrics, 500)
}
onMounted(() => {
initMonitor()
})
onUnmounted(() => {
if (performanceMonitor.value) {
performanceMonitor.value.destroy()
}
})
</script>
<template>
<div class="performance-monitor">
<div class="monitor-header">
<h2>前端性能监控</h2>
<div class="header-actions">
<button @click="refreshMetrics" class="refresh-btn">
刷新数据
</button>
<button @click="simulatePerformanceIssue" class="simulate-btn">
模拟性能问题
</button>
<div class="status-badge" :class="{ active: isMonitoring }">
{{ isMonitoring ? '监控中' : '已停止' }}
</div>
</div>
</div>
<!-- Web Vitals 指标 -->
<div class="metrics-section">
<h3>Web Vitals 核心指标</h3>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">FCP</span>
<span class="metric-desc">First Contentful Paint</span>
</div>
<div class="metric-value"
:style="{ color: getScoreColor(getMetricScore('fcp', metrics.fcp)) }">
{{ formatTime(metrics.fcp) }}
</div>
<div class="metric-score"
:style="{ background: getScoreColor(getMetricScore('fcp', metrics.fcp)) + '20' }">
<span :style="{ color: getScoreColor(getMetricScore('fcp', metrics.fcp)) }">
{{ getMetricScore('fcp', metrics.fcp).toUpperCase() }}
</span>
</div>
<div class="metric-standard">
良好: 1.8s | 待改进: 3.0s
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">LCP</span>
<span class="metric-desc">Largest Contentful Paint</span>
</div>
<div class="metric-value"
:style="{ color: getScoreColor(getMetricScore('lcp', metrics.lcp)) }">
{{ formatTime(metrics.lcp) }}
</div>
<div class="metric-score"
:style="{ background: getScoreColor(getMetricScore('lcp', metrics.lcp)) + '20' }">
<span :style="{ color: getScoreColor(getMetricScore('lcp', metrics.lcp)) }">
{{ getMetricScore('lcp', metrics.lcp).toUpperCase() }}
</span>
</div>
<div class="metric-standard">
良好: 2.5s | 待改进: 4.0s
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">FID</span>
<span class="metric-desc">First Input Delay</span>
</div>
<div class="metric-value"
:style="{ color: getScoreColor(getMetricScore('fid', metrics.fid)) }">
{{ formatTime(metrics.fid) }}
</div>
<div class="metric-score"
:style="{ background: getScoreColor(getMetricScore('fid', metrics.fid)) + '20' }">
<span :style="{ color: getScoreColor(getMetricScore('fid', metrics.fid)) }">
{{ getMetricScore('fid', metrics.fid).toUpperCase() }}
</span>
</div>
<div class="metric-standard">
良好: 100ms | 待改进: 300ms
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-name">CLS</span>
<span class="metric-desc">Cumulative Layout Shift</span>
</div>
<div class="metric-value"
:style="{ color: getScoreColor(getMetricScore('cls', metrics.cls)) }">
{{ formatCLS(metrics.cls) }}
</div>
<div class="metric-score"
:style="{ background: getScoreColor(getMetricScore('cls', metrics.cls)) + '20' }">
<span :style="{ color: getScoreColor(getMetricScore('cls', metrics.cls)) }">
{{ getMetricScore('cls', metrics.cls).toUpperCase() }}
</span>
</div>
<div class="metric-standard">
良好: 0.1 | 待改进: 0.25
</div>
</div>
</div>
</div>
<!-- 页面加载指标 -->
<div class="metrics-section">
<h3>页面加载指标</h3>
<div class="loading-metrics">
<div class="loading-item">
<div class="loading-label">DNS 查询</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '10%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.dns) }}</div>
</div>
<div class="loading-item">
<div class="loading-label">TCP 连接</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '15%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.tcp) }}</div>
</div>
<div class="loading-item">
<div class="loading-label">TTFB</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '25%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.ttfb) }}</div>
</div>
<div class="loading-item">
<div class="loading-label">DOM 解析</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '30%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.domParse) }}</div>
</div>
<div class="loading-item">
<div class="loading-label">DOM Ready</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '60%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.domReady) }}</div>
</div>
<div class="loading-item">
<div class="loading-label">完全加载</div>
<div class="loading-bar">
<div class="loading-fill" :style="{ width: '100%' }"></div>
</div>
<div class="loading-value">{{ formatTime(metrics.loadComplete) }}</div>
</div>
</div>
</div>
<!-- 长任务列表 -->
<div v-if="metrics.longTasks && metrics.longTasks.length > 0" class="metrics-section">
<h3>长任务检测 ({{ metrics.longTasks.length }})</h3>
<div class="long-tasks-list">
<div v-for="(task, index) in metrics.longTasks" :key="index" class="long-task-item">
<div class="task-index">{{ index + 1 }}</div>
<div class="task-info">
<div class="task-duration">
耗时: <span class="highlight">{{ formatTime(task.duration) }}</span>
</div>
<div class="task-time">
开始时间: {{ formatTime(task.startTime) }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.performance-monitor {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.monitor-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.refresh-btn,
.simulate-btn {
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.refresh-btn {
background: #409eff;
color: white;
}
.refresh-btn:hover {
background: #66b1ff;
}
.simulate-btn {
background: #e6a23c;
color: white;
}
.simulate-btn:hover {
background: #ebb563;
}
.status-badge {
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
background: #f56c6c;
color: white;
}
.status-badge.active {
background: #67c23a;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* 指标区域 */
.metrics-section {
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.metrics-section h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #303133;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 12px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.metric-card {
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
transition: all 0.3s;
}
.metric-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.metric-header {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 16px;
}
.metric-name {
font-size: 16px;
font-weight: 700;
color: #303133;
}
.metric-desc {
font-size: 12px;
color: #909399;
}
.metric-value {
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
}
.metric-score {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-bottom: 8px;
}
.metric-standard {
font-size: 12px;
color: #c0c4cc;
}
/* 加载指标 */
.loading-metrics {
display: flex;
flex-direction: column;
gap: 16px;
}
.loading-item {
display: grid;
grid-template-columns: 120px 1fr 120px;
align-items: center;
gap: 16px;
}
.loading-label {
font-size: 14px;
color: #606266;
font-weight: 600;
}
.loading-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.loading-fill {
height: 100%;
background: linear-gradient(90deg, #409eff, #66b1ff);
border-radius: 4px;
transition: width 0.3s;
}
.loading-value {
font-size: 14px;
color: #303133;
font-weight: 600;
text-align: right;
}
/* 长任务列表 */
.long-tasks-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.long-task-item {
display: flex;
gap: 16px;
padding: 16px;
background: #fff5f5;
border-left: 3px solid #f56c6c;
border-radius: 4px;
}
.task-index {
width: 32px;
height: 32px;
background: #f56c6c;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.task-info {
flex: 1;
}
.task-duration {
font-size: 14px;
color: #303133;
margin-bottom: 4px;
}
.task-duration .highlight {
color: #f56c6c;
font-weight: 700;
}
.task-time {
font-size: 12px;
color: #909399;
}
</style>
三、资源加载监控
3.1 资源监控器
resource-monitor.js
export class ResourceMonitor {
constructor() {
this.resources = []
this.init()
}
init() {
if (document.readyState === 'complete') {
this.collectResources()
} else {
window.addEventListener('load', () => {
this.collectResources()
})
}
// 监听新资源
this.observeResources()
}
// 采集已加载资源
collectResources() {
const resources = performance.getEntriesByType('resource')
this.resources = resources.map(resource => ({
name: resource.name,
type: this.getResourceType(resource),
size: resource.transferSize,
duration: resource.duration,
startTime: resource.startTime,
dns: resource.domainLookupEnd - resource.domainLookupStart,
tcp: resource.connectEnd - resource.connectStart,
ttfb: resource.responseStart - resource.requestStart,
download: resource.responseEnd - resource.responseStart,
cached: resource.transferSize === 0
}))
console.log('Resources loaded:', this.resources)
this.analyzeResources()
}
// 监听新资源加载
observeResources() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const resource = {
name: entry.name,
type: this.getResourceType(entry),
size: entry.transferSize,
duration: entry.duration,
startTime: entry.startTime,
cached: entry.transferSize === 0
}
this.resources.push(resource)
console.log('New resource loaded:', resource)
}
})
observer.observe({ entryTypes: ['resource'] })
}
// 获取资源类型
getResourceType(resource) {
const initiatorType = resource.initiatorType
if (initiatorType === 'img') return 'image'
if (initiatorType === 'script') return 'script'
if (initiatorType === 'link' || initiatorType === 'css') return 'stylesheet'
if (initiatorType === 'xmlhttprequest' || initiatorType === 'fetch') return 'xhr'
// 根据文件扩展名判断
const url = resource.name
if (/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(url)) return 'image'
if (/\.js$/i.test(url)) return 'script'
if (/\.css$/i.test(url)) return 'stylesheet'
if (/\.(woff|woff2|ttf|eot)$/i.test(url)) return 'font'
return 'other'
}
// 分析资源
analyzeResources() {
const analysis = {
total: this.resources.length,
totalSize: 0,
totalDuration: 0,
cached: 0,
byType: {}
}
this.resources.forEach(resource => {
analysis.totalSize += resource.size
analysis.totalDuration += resource.duration
if (resource.cached) {
analysis.cached++
}
if (!analysis.byType[resource.type]) {
analysis.byType[resource.type] = {
count: 0,
size: 0,
duration: 0
}
}
analysis.byType[resource.type].count++
analysis.byType[resource.type].size += resource.size
analysis.byType[resource.type].duration += resource.duration
})
console.log('Resource Analysis:', analysis)
return analysis
}
// 获取慢资源
getSlowResources(threshold = 1000) {
return this.resources
.filter(r => r.duration > threshold)
.sort((a, b) => b.duration - a.duration)
}
// 获取大文件
getLargeResources(threshold = 500 * 1024) {
return this.resources
.filter(r => r.size > threshold)
.sort((a, b) => b.size - a.size)
}
// 获取所有资源
getResources() {
return this.resources
}
}
3.2 资源监控组件
ResourceMonitor.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ResourceMonitor } from './resource-monitor.js'
const resourceMonitor = ref(null)
const resources = ref([])
const selectedType = ref('all')
const sortBy = ref('duration')
// 资源统计
const statistics = computed(() => {
if (resources.value.length === 0) return null
const stats = {
total: resources.value.length,
totalSize: 0,
totalDuration: 0,
cached: 0,
byType: {}
}
resources.value.forEach(resource => {
stats.totalSize += resource.size || 0
stats.totalDuration += resource.duration || 0
if (resource.cached) stats.cached++
if (!stats.byType[resource.type]) {
stats.byType[resource.type] = { count: 0, size: 0 }
}
stats.byType[resource.type].count++
stats.byType[resource.type].size += resource.size || 0
})
return stats
})
// 过滤和排序后的资源
const filteredResources = computed(() => {
let filtered = resources.value
if (selectedType.value !== 'all') {
filtered = filtered.filter(r => r.type === selectedType.value)
}
return filtered.sort((a, b) => {
if (sortBy.value === 'duration') {
return b.duration - a.duration
} else if (sortBy.value === 'size') {
return b.size - a.size
}
return 0
})
})
// 初始化监控
const initMonitor = () => {
resourceMonitor.value = new ResourceMonitor()
loadResources()
}
// 加载资源数据
const loadResources = () => {
if (resourceMonitor.value) {
resources.value = resourceMonitor.value.getResources()
}
}
// 刷新数据
const refreshData = () => {
loadResources()
}
// 格式化文件大小
const formatSize = (bytes) => {
if (!bytes) return '0 B'
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`
}
// 格式化时间
const formatTime = (ms) => {
if (!ms) return '0 ms'
return `${ms.toFixed(2)} ms`
}
// 获取文件名
const getFileName = (url) => {
return url.split('/').pop() || url
}
// 获取类型颜色
const getTypeColor = (type) => {
const colors = {
script: '#409eff',
stylesheet: '#67c23a',
image: '#e6a23c',
font: '#f56c6c',
xhr: '#909399',
other: '#c0c4cc'
}
return colors[type] || colors.other
}
onMounted(() => {
initMonitor()
})
</script>
<template>
<div class="resource-monitor">
<div class="monitor-header">
<h2>资源加载监控</h2>
<button @click="refreshData" class="refresh-btn">
刷新数据
</button>
</div>
<!-- 统计卡片 -->
<div v-if="statistics" class="statistics-grid">
<div class="stat-card">
<div class="stat-label">总资源数</div>
<div class="stat-value">{{ statistics.total }}</div>
</div>
<div class="stat-card">
<div class="stat-label">总大小</div>
<div class="stat-value">{{ formatSize(statistics.totalSize) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ formatTime(statistics.totalDuration) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">缓存命中</div>
<div class="stat-value">{{ statistics.cached }}</div>
</div>
</div>
<!-- 按类型统计 -->
<div v-if="statistics" class="type-statistics">
<h3>资源类型分布</h3>
<div class="type-grid">
<div
v-for="(data, type) in statistics.byType"
:key="type"
class="type-item"
:style="{ borderColor: getTypeColor(type) }"
>
<div class="type-header">
<span class="type-name" :style="{ color: getTypeColor(type) }">
{{ type.toUpperCase() }}
</span>
<span class="type-count">{{ data.count }}</span>
</div>
<div class="type-size">{{ formatSize(data.size) }}</div>
</div>
</div>
</div>
<!-- 筛选和排序 -->
<div class="filter-section">
<div class="filter-group">
<label>类型筛选:</label>
<select v-model="selectedType">
<option value="all">全部</option>
<option value="script">Script</option>
<option value="stylesheet">Stylesheet</option>
<option value="image">Image</option>
<option value="font">Font</option>
<option value="xhr">XHR</option>
<option value="other">Other</option>
</select>
</div>
<div class="filter-group">
<label>排序方式:</label>
<select v-model="sortBy">
<option value="duration">按耗时</option>
<option value="size">按大小</option>
</select>
</div>
</div>
<!-- 资源列表 -->
<div class="resource-list">
<div class="list-header">
<div class="col-name">资源名称</div>
<div class="col-type">类型</div>
<div class="col-size">大小</div>
<div class="col-duration">耗时</div>
<div class="col-status">状态</div>
</div>
<div
v-for="(resource, index) in filteredResources"
:key="index"
class="resource-item"
>
<div class="col-name" :title="resource.name">
{{ getFileName(resource.name) }}
</div>
<div class="col-type">
<span class="type-badge" :style="{ background: getTypeColor(resource.type) + '20', color: getTypeColor(resource.type) }">
{{ resource.type }}
</span>
</div>
<div class="col-size">{{ formatSize(resource.size) }}</div>
<div class="col-duration">{{ formatTime(resource.duration) }}</div>
<div class="col-status">
<span v-if="resource.cached" class="cached-badge">缓存</span>
<span v-else class="network-badge">网络</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.resource-monitor {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
}
.monitor-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.refresh-btn {
padding: 8px 20px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.statistics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 20px;
background: white;
border-radius: 8px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #303133;
}
.type-statistics {
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
}
.type-statistics h3 {
margin: 0 0 16px 0;
font-size: 16px;
color: #303133;
}
.type-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
}
.type-item {
padding: 16px;
border: 2px solid;
border-radius: 8px;
}
.type-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.type-name {
font-size: 14px;
font-weight: 700;
}
.type-count {
font-size: 18px;
font-weight: 700;
color: #303133;
}
.type-size {
font-size: 12px;
color: #909399;
}
.filter-section {
display: flex;
gap: 24px;
padding: 16px 20px;
background: white;
border-radius: 8px;
margin-bottom: 16px;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
}
.filter-group label {
font-size: 14px;
color: #606266;
font-weight: 600;
}
.filter-group select {
padding: 6px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
outline: none;
}
.resource-list {
background: white;
border-radius: 8px;
overflow: hidden;
}
.list-header,
.resource-item {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
gap: 16px;
padding: 16px 20px;
align-items: center;
}
.list-header {
background: #f5f7fa;
font-weight: 600;
font-size: 14px;
color: #606266;
}
.resource-item {
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #303133;
}
.resource-item:last-child {
border-bottom: none;
}
.col-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.type-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.cached-badge,
.network-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.cached-badge {
background: #e1f3d8;
color: #67c23a;
}
.network-badge {
background: #ecf5ff;
color: #409eff;
}
</style>
四、接口性能监控
4.1 接口监控拦截器
api-monitor.js
export class ApiMonitor {
constructor() {
this.requests = []
this.maxRecords = 100
this.init()
}
init() {
this.interceptFetch()
this.interceptXHR()
}
// 拦截 Fetch
interceptFetch() {
const originalFetch = window.fetch
const self = this
window.fetch = function(...args) {
const startTime = Date.now()
const url = args[0]
const options = args[1] || {}
const requestInfo = {
id: self.generateId(),
url: url,
method: options.method || 'GET',
startTime: startTime,
type: 'fetch'
}
return originalFetch.apply(this, args)
.then(response => {
const endTime = Date.now()
requestInfo.duration = endTime - startTime
requestInfo.status = response.status
requestInfo.success = response.ok
requestInfo.endTime = endTime
self.recordRequest(requestInfo)
return response
})
.catch(error => {
const endTime = Date.now()
requestInfo.duration = endTime - startTime
requestInfo.success = false
requestInfo.error = error.message
requestInfo.endTime = endTime
self.recordRequest(requestInfo)
throw error
})
}
}
// 拦截 XMLHttpRequest
interceptXHR() {
const self = this
const originalOpen = XMLHttpRequest.prototype.open
const originalSend = XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.open = function(method, url) {
this._requestInfo = {
id: self.generateId(),
url: url,
method: method,
type: 'xhr'
}
return originalOpen.apply(this, arguments)
}
XMLHttpRequest.prototype.send = function() {
const requestInfo = this._requestInfo
requestInfo.startTime = Date.now()
this.addEventListener('loadend', function() {
const endTime = Date.now()
requestInfo.duration = endTime - requestInfo.startTime
requestInfo.status = this.status
requestInfo.success = this.status >= 200 && this.status < 300
requestInfo.endTime = endTime
self.recordRequest(requestInfo)
})
return originalSend.apply(this, arguments)
}
}
// 记录请求
recordRequest(requestInfo) {
this.requests.unshift(requestInfo)
if (this.requests.length > this.maxRecords) {
this.requests.pop()
}
console.log('API Request:', requestInfo)
// 检查慢接口
if (requestInfo.duration > 1000) {
console.warn('Slow API detected:', requestInfo)
}
// 检查失败请求
if (!requestInfo.success) {
console.error('API Request failed:', requestInfo)
}
}
// 获取所有请求
getRequests() {
return this.requests
}
// 获取慢接口
getSlowRequests(threshold = 1000) {
return this.requests
.filter(r => r.duration > threshold)
.sort((a, b) => b.duration - a.duration)
}
// 获取失败请求
getFailedRequests() {
return this.requests.filter(r => !r.success)
}
// 获取统计
getStatistics() {
const stats = {
total: this.requests.length,
success: 0,
failed: 0,
avgDuration: 0,
maxDuration: 0,
minDuration: Infinity
}
let totalDuration = 0
this.requests.forEach(request => {
if (request.success) {
stats.success++
} else {
stats.failed++
}
totalDuration += request.duration
stats.maxDuration = Math.max(stats.maxDuration, request.duration)
stats.minDuration = Math.min(stats.minDuration, request.duration)
})
stats.avgDuration = stats.total > 0 ? totalDuration / stats.total : 0
if (stats.minDuration === Infinity) {
stats.minDuration = 0
}
return stats
}
// 清空记录
clear() {
this.requests = []
}
// 生成ID
generateId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
}
4.2 接口监控组件
ApiMonitor.vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ApiMonitor } from './api-monitor.js'
const apiMonitor = ref(null)
const requests = ref([])
const filterStatus = ref('all')
const autoRefresh = ref(true)
let refreshTimer = null
// 统计信息
const statistics = computed(() => {
if (!apiMonitor.value) return null
return apiMonitor.value.getStatistics()
})
// 过滤后的请求
const filteredRequests = computed(() => {
if (filterStatus.value === 'all') {
return requests.value
} else if (filterStatus.value === 'success') {
return requests.value.filter(r => r.success)
} else if (filterStatus.value === 'failed') {
return requests.value.filter(r => !r.success)
} else if (filterStatus.value === 'slow') {
return requests.value.filter(r => r.duration > 1000)
}
return requests.value
})
// 初始化监控
const initMonitor = () => {
apiMonitor.value = new ApiMonitor()
loadRequests()
if (autoRefresh.value) {
startAutoRefresh()
}
}
// 加载请求数据
const loadRequests = () => {
if (apiMonitor.value) {
requests.value = apiMonitor.value.getRequests()
}
}
// 自动刷新
const startAutoRefresh = () => {
refreshTimer = setInterval(() => {
loadRequests()
}, 2000)
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// 切换自动刷新
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
// 清空数据
const clearData = () => {
if (confirm('确定要清空所有数据吗?')) {
apiMonitor.value.clear()
loadRequests()
}
}
// 模拟API请求
const simulateApiCall = async (type = 'success') => {
const baseUrl = 'https://jsonplaceholder.typicode.com'
try {
if (type === 'success') {
await fetch(`${baseUrl}/posts/1`)
} else if (type === 'slow') {
// 模拟慢接口
await fetch(`${baseUrl}/posts`)
} else if (type === 'failed') {
await fetch(`${baseUrl}/invalid-endpoint`)
}
} catch (error) {
console.error('API call failed:', error)
}
setTimeout(loadRequests, 100)
}
// 格式化时间
const formatTime = (ms) => {
if (!ms) return '-'
return `${ms.toFixed(0)} ms`
}
// 格式化日期
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleTimeString()
}
// 获取状态颜色
const getStatusColor = (status) => {
if (status >= 200 && status < 300) return '#67c23a'
if (status >= 300 && status < 400) return '#409eff'
if (status >= 400 && status < 500) return '#e6a23c'
return '#f56c6c'
}
// 获取方法颜色
const getMethodColor = (method) => {
const colors = {
GET: '#409eff',
POST: '#67c23a',
PUT: '#e6a23c',
DELETE: '#f56c6c',
PATCH: '#909399'
}
return colors[method] || '#c0c4cc'
}
onMounted(() => {
initMonitor()
})
</script>
<template>
<div class="api-monitor">
<div class="monitor-header">
<h2>接口性能监控</h2>
<div class="header-actions">
<button @click="simulateApiCall('success')" class="simulate-btn success">
模拟成功请求
</button>
<button @click="simulateApiCall('slow')" class="simulate-btn slow">
模拟慢请求
</button>
<button @click="simulateApiCall('failed')" class="simulate-btn failed">
模拟失败请求
</button>
<button @click="toggleAutoRefresh" class="auto-refresh-btn" :class="{ active: autoRefresh }">
{{ autoRefresh ? '自动刷新: 开' : '自动刷新: 关' }}
</button>
<button @click="clearData" class="clear-btn">
清空数据
</button>
</div>
</div>
<!-- 统计卡片 -->
<div v-if="statistics" class="statistics-grid">
<div class="stat-card">
<div class="stat-label">总请求数</div>
<div class="stat-value">{{ statistics.total }}</div>
</div>
<div class="stat-card success">
<div class="stat-label">成功</div>
<div class="stat-value">{{ statistics.success }}</div>
</div>
<div class="stat-card failed">
<div class="stat-label">失败</div>
<div class="stat-value">{{ statistics.failed }}</div>
</div>
<div class="stat-card">
<div class="stat-label">平均耗时</div>
<div class="stat-value">{{ formatTime(statistics.avgDuration) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">最长耗时</div>
<div class="stat-value">{{ formatTime(statistics.maxDuration) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">最短耗时</div>
<div class="stat-value">{{ formatTime(statistics.minDuration) }}</div>
</div>
</div>
<!-- 筛选器 -->
<div class="filter-section">
<div class="filter-tabs">
<button
:class="{ active: filterStatus === 'all' }"
@click="filterStatus = 'all'"
>
全部 ({{ requests.length }})
</button>
<button
:class="{ active: filterStatus === 'success' }"
@click="filterStatus = 'success'"
>
成功 ({{ requests.filter(r => r.success).length }})
</button>
<button
:class="{ active: filterStatus === 'failed' }"
@click="filterStatus = 'failed'"
>
失败 ({{ requests.filter(r => !r.success).length }})
</button>
<button
:class="{ active: filterStatus === 'slow' }"
@click="filterStatus = 'slow'"
>
慢请求 ({{ requests.filter(r => r.duration > 1000).length }})
</button>
</div>
</div>
<!-- 请求列表 -->
<div class="request-list">
<div class="list-header">
<div class="col-method">方法</div>
<div class="col-url">URL</div>
<div class="col-status">状态</div>
<div class="col-duration">耗时</div>
<div class="col-time">时间</div>
</div>
<div
v-for="request in filteredRequests"
:key="request.id"
class="request-item"
:class="{ failed: !request.success, slow: request.duration > 1000 }"
>
<div class="col-method">
<span
class="method-badge"
:style="{ background: getMethodColor(request.method) + '20', color: getMethodColor(request.method) }"
>
{{ request.method }}
</span>
</div>
<div class="col-url" :title="request.url">
{{ request.url }}
</div>
<div class="col-status">
<span
class="status-badge"
:style="{ color: getStatusColor(request.status) }"
>
{{ request.status || '-' }}
</span>
</div>
<div class="col-duration" :class="{ slow: request.duration > 1000 }">
{{ formatTime(request.duration) }}
</div>
<div class="col-time">
{{ formatDate(request.startTime) }}
</div>
</div>
<div v-if="filteredRequests.length === 0" class="empty-list">
<div class="empty-text">暂无请求记录</div>
</div>
</div>
</div>
</template>
<style scoped>
.api-monitor {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.monitor-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.simulate-btn,
.auto-refresh-btn,
.clear-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.simulate-btn.success {
background: #67c23a;
color: white;
}
.simulate-btn.slow {
background: #e6a23c;
color: white;
}
.simulate-btn.failed {
background: #f56c6c;
color: white;
}
.auto-refresh-btn {
background: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
}
.auto-refresh-btn.active {
background: #409eff;
color: white;
}
.clear-btn {
background: #f4f4f5;
color: #606266;
}
.statistics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 20px;
background: white;
border-radius: 8px;
text-align: center;
}
.stat-card.success {
border-left: 4px solid #67c23a;
}
.stat-card.failed {
border-left: 4px solid #f56c6c;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #303133;
}
.filter-section {
padding: 16px 20px;
background: white;
border-radius: 8px;
margin-bottom: 16px;
}
.filter-tabs {
display: flex;
gap: 12px;
}
.filter-tabs button {
padding: 8px 20px;
background: #f5f7fa;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.3s;
}
.filter-tabs button.active {
background: #409eff;
color: white;
}
.request-list {
background: white;
border-radius: 8px;
overflow: hidden;
}
.list-header,
.request-item {
display: grid;
grid-template-columns: 100px 2fr 100px 120px 120px;
gap: 16px;
padding: 16px 20px;
align-items: center;
}
.list-header {
background: #f5f7fa;
font-weight: 600;
font-size: 14px;
color: #606266;
}
.request-item {
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #303133;
}
.request-item.failed {
background: #fef0f0;
}
.request-item.slow {
background: #fdf6ec;
}
.col-url {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
}
.status-badge {
font-weight: 700;
}
.col-duration.slow {
color: #e6a23c;
font-weight: 700;
}
.empty-list {
padding: 60px 20px;
text-align: center;
color: #909399;
font-size: 14px;
}
</style>
五、长任务监控
5.1 长任务监控器
long-task-monitor.js
export class LongTaskMonitor {
constructor(threshold = 50) {
this.threshold = threshold
this.longTasks = []
this.observer = null
this.init()
}
init() {
if (!('PerformanceObserver' in window)) {
console.warn('PerformanceObserver not supported')
return
}
try {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > this.threshold) {
this.recordLongTask(entry)
}
}
})
this.observer.observe({ entryTypes: ['longtask'] })
} catch (e) {
console.warn('Long Task API not supported')
// 降级方案:使用 requestIdleCallback 检测
this.fallbackMonitor()
}
}
// 记录长任务
recordLongTask(entry) {
const task = {
id: this.generateId(),
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
attribution: entry.attribution || [],
timestamp: Date.now()
}
this.longTasks.push(task)
console.warn('Long Task detected:', task)
// 只保留最近100个
if (this.longTasks.length > 100) {
this.longTasks.shift()
}
}
// 降级监控方案
fallbackMonitor() {
let lastTime = performance.now()
const check = () => {
const currentTime = performance.now()
const duration = currentTime - lastTime
if (duration > this.threshold) {
this.recordLongTask({
duration: duration,
startTime: lastTime,
name: 'unknown',
attribution: []
})
}
lastTime = currentTime
requestIdleCallback(check, { timeout: 1000 })
}
requestIdleCallback(check)
}
// 获取长任务
getLongTasks() {
return this.longTasks
}
// 获取统计
getStatistics() {
if (this.longTasks.length === 0) {
return {
count: 0,
totalDuration: 0,
avgDuration: 0,
maxDuration: 0
}
}
const totalDuration = this.longTasks.reduce((sum, task) => sum + task.duration, 0)
const maxDuration = Math.max(...this.longTasks.map(t => t.duration))
return {
count: this.longTasks.length,
totalDuration,
avgDuration: totalDuration / this.longTasks.length,
maxDuration
}
}
// 清空记录
clear() {
this.longTasks = []
}
// 销毁监控
destroy() {
if (this.observer) {
this.observer.disconnect()
}
}
// 生成ID
generateId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
}
5.2 长任务监控组件
LongTaskMonitor.vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { LongTaskMonitor } from './long-task-monitor.js'
const longTaskMonitor = ref(null)
const longTasks = ref([])
const autoRefresh = ref(true)
let refreshTimer = null
// 统计信息
const statistics = computed(() => {
if (!longTaskMonitor.value) return null
return longTaskMonitor.value.getStatistics()
})
// 初始化监控
const initMonitor = () => {
longTaskMonitor.value = new LongTaskMonitor(50)
loadLongTasks()
if (autoRefresh.value) {
startAutoRefresh()
}
}
// 加载长任务数据
const loadLongTasks = () => {
if (longTaskMonitor.value) {
longTasks.value = longTaskMonitor.value.getLongTasks()
}
}
// 自动刷新
const startAutoRefresh = () => {
refreshTimer = setInterval(() => {
loadLongTasks()
}, 2000)
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// 模拟长任务
const simulateLongTask = (duration = 200) => {
const startTime = Date.now()
// 占用主线程
while (Date.now() - startTime < duration) {
// 空循环
}
setTimeout(loadLongTasks, 100)
}
// 格式化时间
const formatTime = (ms) => {
return `${ms.toFixed(2)} ms`
}
// 格式化日期
const formatDate = (timestamp) => {
return new Date(timestamp).toLocaleTimeString()
}
// 获取严重程度
const getSeverity = (duration) => {
if (duration < 100) return { text: '轻微', color: '#e6a23c' }
if (duration < 200) return { text: '中等', color: '#f56c6c' }
return { text: '严重', color: '#f56c6c' }
}
onMounted(() => {
initMonitor()
})
onUnmounted(() => {
stopAutoRefresh()
longTaskMonitor.value?.destroy()
})
</script>
<template>
<div class="long-task-monitor">
<div class="monitor-header">
<h2>长任务监控</h2>
<div class="header-actions">
<button @click="simulateLongTask(100)" class="simulate-btn light">
模拟轻微长任务
</button>
<button @click="simulateLongTask(200)" class="simulate-btn moderate">
模拟中等长任务
</button>
<button @click="simulateLongTask(500)" class="simulate-btn severe">
模拟严重长任务
</button>
</div>
</div>
<!-- 统计卡片 -->
<div v-if="statistics" class="statistics-grid">
<div class="stat-card">
<div class="stat-label">长任务总数</div>
<div class="stat-value">{{ statistics.count }}</div>
</div>
<div class="stat-card">
<div class="stat-label">总耗时</div>
<div class="stat-value">{{ formatTime(statistics.totalDuration) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">平均耗时</div>
<div class="stat-value">{{ formatTime(statistics.avgDuration) }}</div>
</div>
<div class="stat-card">
<div class="stat-label">最长耗时</div>
<div class="stat-value">{{ formatTime(statistics.maxDuration) }}</div>
</div>
</div>
<!-- 长任务列表 -->
<div class="task-list">
<h3>长任务列表</h3>
<div
v-for="task in longTasks"
:key="task.id"
class="task-item"
>
<div class="task-severity">
<div
class="severity-badge"
:style="{ background: getSeverity(task.duration).color }"
>
{{ getSeverity(task.duration).text }}
</div>
</div>
<div class="task-info">
<div class="task-duration">
耗时: <span class="highlight">{{ formatTime(task.duration) }}</span>
</div>
<div class="task-details">
开始时间: {{ formatTime(task.startTime) }} |
记录时间: {{ formatDate(task.timestamp) }}
</div>
</div>
</div>
<div v-if="longTasks.length === 0" class="empty-list">
<div class="empty-text">暂未检测到长任务</div>
<div class="empty-hint">长任务是指执行时间超过50ms的JavaScript任务</div>
</div>
</div>
</div>
</template>
<style scoped>
.long-task-monitor {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px;
background: white;
border-radius: 8px;
flex-wrap: wrap;
gap: 12px;
}
.monitor-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
}
.header-actions {
display: flex;
gap: 12px;
}
.simulate-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
color: white;
}
.simulate-btn.light {
background: #e6a23c;
}
.simulate-btn.moderate {
background: #f56c6c;
}
.simulate-btn.severe {
background: #f56c6c;
}
.statistics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 20px;
background: white;
border-radius: 8px;
text-align: center;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #303133;
}
.task-list {
padding: 20px;
background: white;
border-radius: 8px;
}
.task-list h3 {
margin: 0 0 20px 0;
font-size: 18px;
color: #303133;
}
.task-item {
display: flex;
gap: 16px;
padding: 16px;
margin-bottom: 12px;
background: #fef0f0;
border-left: 4px solid #f56c6c;
border-radius: 4px;
}
.severity-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
color: white;
}
.task-info {
flex: 1;
}
.task-duration {
font-size: 14px;
color: #303133;
margin-bottom: 6px;
}
.task-duration .highlight {
color: #f56c6c;
font-weight: 700;
font-size: 16px;
}
.task-details {
font-size: 12px;
color: #909399;
}
.empty-list {
padding: 60px 20px;
text-align: center;
}
.empty-text {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.empty-hint {
font-size: 12px;
color: #c0c4cc;
}
</style>
六、简历描述模板
前端性能监控系统开发 (2023.05 - 2023.10)
负责公司前端性能监控体系建设,实现Web Vitals指标监控、资源加载分析和接口性能追踪,覆盖10+个核心业务系统。
核心职责
- 基于Performance API搭建完整的性能监控体系,覆盖FCP/LCP/FID/CLS等核心指标
- 开发资源加载监控模块,实时追踪JS/CSS/图片等资源加载性能
- 实现接口性能监控,拦截Fetch和XMLHttpRequest,统计接口响应时间
- 开发长任务检测功能,识别并记录执行时间超过50ms的JavaScript任务
- 设计性能数据上报策略,使用Beacon API确保数据可靠上报
技术实现
- 使用PerformanceObserver监听性能条目,实现零侵入式数据采集
- 通过Navigation Timing API计算页面加载各阶段耗时
- 封装性能监控SDK,支持自定义指标和阈值配置
- 实现性能数据本地缓存和批量上报,降低对页面性能的影响
项目成果
- 页面加载性能提升35%,FCP从2.8s优化到1.8s
- 识别并优化了20+个慢接口,平均响应时间降低40%
- 检测到15个长任务问题,优化后用户交互流畅度提升50%
- 建立性能评分体系,将性能指标纳入团队KPI考核
七、SOP标准回答
面试问题: 介绍一下你做的性能监控系统
标准回答
"我负责开发了一套完整的前端性能监控系统,主要分为四个模块。
第一个是Web Vitals指标监控。我使用PerformanceObserver API监听paint、largest-contentful-paint、first-input、layout-shift等类型的性能条目,自动计算FCP、LCP、FID、CLS这些谷歌提出的核心指标。比如FCP,我监听first-contentful-paint事件,获取startTime就是FCP值。对于LCP,需要持续监听,因为页面最大内容元素可能会变化,我取最后一个条目的startTime。
第二个是资源加载监控。通过performance.getEntriesByType('resource')可以获取所有已加载资源的详细信息,包括DNS查询、TCP连接、TTFB、下载时间等。我会分析资源类型、大小、耗时,识别出慢资源和大文件。比如某个JS文件加载超过2秒,就会标记为慢资源,推动开发优化。
第三个是接口性能监控。我通过拦截Fetch和XMLHttpRequest实现。保存原始方法,然后在发送前记录开始时间,在响应后计算耗时,同时记录URL、方法、状态码等。这样可以统计每个接口的调用次数、成功率、平均响应时间,快速定位慢接口。
第四个是长任务监控。使用PerformanceObserver监听longtask类型,当JavaScript执行超过50ms就会触发。长任务会阻塞主线程,导致页面卡顿,所以需要重点关注。不过longtask API兼容性不太好,我做了降级方案,用requestIdleCallback定期检测时间间隔。
数据采集后会先缓存在内存,达到一定条数或时间间隔就批量上报,用的Beacon API,即使页面关闭也能保证数据发送成功。
最终效果是,我们的FCP从2.8秒优化到1.8秒,用户可感知的加载速度提升了35%,整体性能评分从60分提升到85分。"
八、难点与亮点分析
难点1: 如何准确计算FID?
问题场景: FID(First Input Delay)是用户首次交互到浏览器响应的延迟,但不是所有交互都应该计算。
解决方案
observeFID() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.processingStart - entry.startTime 就是延迟
const fid = entry.processingStart - entry.startTime
// 只记录第一次交互
this.metrics.fid = fid
// 记录交互类型
this.metrics.fidType = entry.name // click, keydown等
console.log('FID:', fid, 'Type:', entry.name)
// 断开观察器,只测量第一次
observer.disconnect()
}
})
observer.observe({ entryTypes: ['first-input'], buffered: true })
}
关键点
- 使用first-input类型,自动捕获首次输入
- buffered: true 确保不会错过早期事件
- 立即disconnect避免重复记录
难点2: 如何处理CLS的持续变化?
问题场景: CLS会持续累积,需要在合适的时机获取最终值。
解决方案
observeCLS() {
let clsValue = 0
let sessionValue = 0
let sessionEntries = []
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 只统计非用户输入导致的布局偏移
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0]
const lastSessionEntry = sessionEntries[sessionEntries.length - 1]
// 如果当前偏移距离上次超过1秒或距离第一次超过5秒,开始新会话
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value
sessionEntries.push(entry)
} else {
sessionValue = entry.value
sessionEntries = [entry]
}
// 保留最大会话值
if (sessionValue > clsValue) {
clsValue = sessionValue
this.metrics.cls = clsValue
}
}
}
})
observer.observe({ entryTypes: ['layout-shift'] })
}
亮点1: 性能指标评分系统
创新点
- 参考Google的评分标准
- 为每个指标设定good/needs-improvement/poor三个档次
- 自动计算综合性能分数
实现
calculatePerformanceScore() {
const scores = {
fcp: this.getMetricScore('fcp', this.metrics.fcp),
lcp: this.getMetricScore('lcp', this.metrics.lcp),
fid: this.getMetricScore('fid', this.metrics.fid),
cls: this.getMetricScore('cls', this.metrics.cls)
}
// 计算总分 (0-100)
const weights = { fcp: 10, lcp: 25, fid: 25, cls: 25, ttfb: 15 }
let totalScore = 0
Object.keys(scores).forEach(metric => {
const score = scores[metric]
const weight = weights[metric]
if (score === 'good') {
totalScore += weight
} else if (score === 'needs-improvement') {
totalScore += weight * 0.5
}
})
return {
score: Math.round(totalScore),
grade: this.getGrade(totalScore),
details: scores
}
}
getGrade(score) {
if (score >= 90) return 'A'
if (score >= 75) return 'B'
if (score >= 50) return 'C'
if (score >= 25) return 'D'
return 'F'
}
亮点2: 智能性能预警
创新点
- 实时检测性能异常
- 基于历史数据的动态阈值
- 分级预警机制
实现
checkPerformanceAlert() {
const alerts = []
// LCP超过4秒 - 严重
if (this.metrics.lcp > 4000) {
alerts.push({
level: 'error',
metric: 'LCP',
message: `LCP过高: ${this.metrics.lcp.toFixed(0)}ms,严重影响用户体验`
})
}
// FID超过300ms - 警告
if (this.metrics.fid > 300) {
alerts.push({
level: 'warning',
metric: 'FID',
message: `FID过高: ${this.metrics.fid.toFixed(0)}ms,交互响应慢`
})
}
// CLS超过0.25 - 警告
if (this.metrics.cls > 0.25) {
alerts.push({
level: 'warning',
metric: 'CLS',
message: `CLS过高: ${this.metrics.cls.toFixed(3)},页面布局不稳定`
})
}
// 长任务过多 - 警告
if (this.metrics.longTasks && this.metrics.longTasks.length > 5) {
alerts.push({
level: 'warning',
metric: 'LongTask',
message: `检测到${this.metrics.longTasks.length}个长任务,可能影响交互性能`
})
}
return alerts
}