手搓HTML性能监控:實時檢測渲染性能與資源加載
引言:性能監控的重要性
在現代Web開發中,性能已成為衡量用戶體驗的關鍵指標。根據Google的研究,頁面加載時間每延遲1秒,移動端轉化率就會下降20%。而渲染性能直接影響用戶的視覺體驗,資源加載效率則決定用戶能否快速獲取內容。本文將從零開始,深入探討如何手動構建一個完整的HTML性能監控系統,實現對渲染性能與資源加載的實時檢測。
第一章:性能監控基礎理論
1.1 性能指標分類
Web性能監控主要分為以下幾類:
載入性能指標:衡量頁面資源加載效率
-
First Contentful Paint (FCP)
-
Largest Contentful Paint (LCP)
-
Time to First Byte (TTFB)
交互性能指標:衡量頁面響應用戶交互的能力
-
First Input Delay (FID)
-
Interaction to Next Paint (INP)
視覺穩定性指標:衡量頁面視覺穩定性
-
Cumulative Layout Shift (CLS)
自定義業務指標:根據業務需求定義的特殊指標
1.2 瀏覽器性能API簡介
現代瀏覽器提供了一系列性能監控API:
javascript
// Performance Timeline API
window.performance.getEntries()
// Navigation Timing API
window.performance.timing
// Resource Timing API
window.performance.getEntriesByType('resource')
// Paint Timing API
window.performance.getEntriesByType('paint')
// Layout Instability API
new PerformanceObserver()
// Long Tasks API
performance.now()
1.3 監控系統架構設計
一個完整的性能監控系統應包含以下組件:
數據收集層:收集各類性能數據
數據處理層:過濾、聚合、分析數據
數據存儲層:存儲歷史性能數據
可視化層:展示性能指標和趨勢
告警層:當性能下降時發出告警
第二章:渲染性能監控實作
2.1 FPS(每秒幀數)監控
FPS是衡量頁面流暢度的關鍵指標,理想的FPS應保持在60左右。
javascript
class FPSMonitor {
constructor() {
this.fps = 0;
this.frameCount = 0;
this.lastTime = performance.now();
this.animationId = null;
this.fpsHistory = [];
this.maxHistoryLength = 100;
this.init();
}
init() {
this.loop();
}
loop() {
this.animationId = requestAnimationFrame(() => {
this.frameCount++;
const currentTime = performance.now();
const deltaTime = currentTime – this.lastTime;
// 每秒計算一次FPS
if (deltaTime >= 1000) {
this.fps = Math.round((this.frameCount * 1000) / deltaTime);
this.frameCount = 0;
this.lastTime = currentTime;
// 記錄歷史數據
this.recordFPS();
// 觸發回調
this.onFPSUpdate && this.onFPSUpdate(this.fps);
}
this.loop();
});
}
recordFPS() {
this.fpsHistory.push({
timestamp: Date.now(),
fps: this.fps
});
// 限制歷史記錄長度
if (this.fpsHistory.length > this.maxHistoryLength) {
this.fpsHistory.shift();
}
}
getAverageFPS() {
if (this.fpsHistory.length === 0) return 0;
const sum = this.fpsHistory.reduce((acc, entry) => acc + entry.fps, 0);
return Math.round(sum / this.fpsHistory.length);
}
getFPSStats() {
const fpsValues = this.fpsHistory.map(entry => entry.fps);
return {
current: this.fps,
average: this.getAverageFPS(),
min: Math.min(…fpsValues),
max: Math.max(…fpsValues),
history: […this.fpsHistory]
};
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
}
// 使用示例
const fpsMonitor = new FPSMonitor();
fpsMonitor.onFPSUpdate = (fps) => {
console.log(`當前FPS: ${fps}`);
if (fps < 30) {
console.warn('FPS過低,可能影響用戶體驗');
}
};
2.2 長任務監控
長任務(Long Tasks)是指執行時間超過50毫秒的任務,會阻塞主線程,影響頁面響應性。
javascript
class LongTaskMonitor {
constructor() {
this.longTasks = [];
this.observer = null;
this.maxTasks = 50;
this.init();
}
init() {
if (!window.PerformanceObserver) {
console.warn('瀏覽器不支持 PerformanceObserver API');
return;
}
try {
this.observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
this.handleLongTask(entry);
});
});
this.observer.observe({ entryTypes: ['longtask'] });
} catch (e) {
console.error('長任務監控初始化失敗:', e);
}
}
handleLongTask(entry) {
const longTask = {
startTime: entry.startTime,
duration: entry.duration,
name: entry.name || 'unknown',
attribution: entry.attribution || [],
timestamp: Date.now()
};
this.longTasks.push(longTask);
// 限制記錄數量
if (this.longTasks.length > this.maxTasks) {
this.longTasks.shift();
}
// 觸發長任務事件
this.onLongTaskDetected && this.onLongTaskDetected(longTask);
// 輸出警告
console.warn(`檢測到長任務: ${longTask.duration.toFixed(2)}ms`, longTask);
}
getLongTaskStats() {
if (this.longTasks.length === 0) {
return {
count: 0,
averageDuration: 0,
maxDuration: 0,
totalDuration: 0
};
}
const durations = this.longTasks.map(task => task.duration);
const totalDuration = durations.reduce((sum, duration) => sum + duration, 0);
return {
count: this.longTasks.length,
averageDuration: totalDuration / this.longTasks.length,
maxDuration: Math.max(…durations),
totalDuration: totalDuration,
tasks: […this.longTasks]
};
}
clear() {
this.longTasks = [];
}
disconnect() {
if (this.observer) {
this.observer.disconnect();
}
}
}
// 使用示例
const longTaskMonitor = new LongTaskMonitor();
longTaskMonitor.onLongTaskDetected = (task) => {
// 可以將長任務信息發送到服務器
sendToAnalytics('long_task', task);
};
2.3 佈局偏移監控(CLS)
累積佈局偏移(CLS)衡量頁面視覺穩定性,是Core Web Vitals之一。
javascript
class LayoutShiftMonitor {
constructor() {
this.clsScore = 0;
this.sessionValue = 0;
this.sessionEntries = [];
this.lastEntry = null;
this.maxEntries = 100;
this.observer = null;
this.init();
}
init() {
if (!window.PerformanceObserver) {
console.warn('瀏覽器不支持 PerformanceObserver API');
return;
}
try {
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 僅處理沒有hadRecentInput的佈局偏移
if (!entry.hadRecentInput) {
this.sessionEntries.push(entry);
this.processCLS();
}
}
});
this.observer.observe({ entryTypes: ['layout-shift'] });
} catch (e) {
console.error('佈局偏移監控初始化失敗:', e);
}
// 監聽頁面可見性變化,重置session
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.lastEntry = null;
}
});
}
processCLS() {
// 限制記錄數量
if (this.sessionEntries.length > this.maxEntries) {
this.sessionEntries.shift();
}
// 計算CLS分數
this.sessionValue = this.calculateCLS();
// 更新總CLS分數
this.clsScore = Math.max(this.clsScore, this.sessionValue);
// 觸發CLS更新事件
this.onCLSUpdate && this.onCLSUpdate({
current: this.sessionValue,
cumulative: this.clsScore,
entries: […this.sessionEntries]
});
}
calculateCLS() {
let cls = 0;
this.sessionEntries.forEach(entry => {
// 檢查是否與上一次偏移在同一session中
if (this.lastEntry &&
entry.startTime – this.lastEntry.startTime < 1000 &&
entry.startTime – this.lastEntry.startTime > 0) {
cls += entry.value;
} else {
cls = entry.value;
}
this.lastEntry = entry;
});
return cls;
}
getCLSStats() {
return {
current: this.sessionValue,
cumulative: this.clsScore,
entryCount: this.sessionEntries.length,
entries: […this.sessionEntries]
};
}
reset() {
this.sessionValue = 0;
this.sessionEntries = [];
this.lastEntry = null;
}
disconnect() {
if (this.observer) {
this.observer.disconnect();
}
}
}
// 使用示例
const layoutShiftMonitor = new LayoutShiftMonitor();
layoutShiftMonitor.onCLSUpdate = (clsData) => {
console.log(`當前CLS: ${clsData.current.toFixed(4)}, 累積CLS: ${clsData.cumulative.toFixed(4)}`);
if (clsData.cumulative > 0.1) {
console.warn('CLS過高,影響用戶體驗');
}
};
第三章:資源加載監控實作
3.1 資源加載性能監控
監控頁面中所有資源的加載性能,包括圖片、腳本、樣式表等。
javascript
class ResourceLoadMonitor {
constructor() {
this.resources = [];
this.resourceMap = new Map();
this.observer = null;
this.maxResources = 200;
this.init();
}
init() {
// 首先收集已加載的資源
this.collectExistingResources();
// 然後監聽後續加載的資源
this.setupResourceObserver();
// 監聽頁面卸載事件,收集最終數據
window.addEventListener('beforeunload', () => {
this.collectFinalResourceData();
});
}
collectExistingResources() {
if (!window.performance || !window.performance.getEntriesByType) {
return;
}
const resourceEntries = performance.getEntriesByType('resource');
resourceEntries.forEach(entry => {
this.processResourceEntry(entry);
});
}
setupResourceObserver() {
if (!window.PerformanceObserver) {
return;
}
try {
this.observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
this.processResourceEntry(entry);
});
});
this.observer.observe({ entryTypes: ['resource'] });
} catch (e) {
console.error('資源監控初始化失敗:', e);
}
}
processResourceEntry(entry) {
const resource = {
name: entry.name,
initiatorType: entry.initiatorType,
startTime: entry.startTime,
duration: entry.duration,
transferSize: entry.transferSize,
encodedSize: entry.encodedBodySize,
decodedSize: entry.decodedBodySize,
protocol: this.extractProtocol(entry.name),
domain: this.extractDomain(entry.name),
redirectTime: entry.redirectEnd – entry.redirectStart,
dnsTime: entry.domainLookupEnd – entry.domainLookupStart,
tcpTime: entry.connectEnd – entry.connectStart,
sslTime: entry.secureConnectionStart > 0 ?
entry.connectEnd – entry.secureConnectionStart : 0,
ttfb: entry.responseStart – entry.requestStart,
downloadTime: entry.responseEnd – entry.responseStart,
timestamp: Date.now()
};
// 計算總時間
resource.totalTime = resource.duration;
// 添加到資源列表
this.addResource(resource);
// 觸發資源加載事件
this.onResourceLoaded && this.onResourceLoaded(resource);
}
extractProtocol(url) {
try {
const urlObj = new URL(url);
return urlObj.protocol.replace(':', '');
} catch (e) {
return 'unknown';
}
}
extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
return 'unknown';
}
}
addResource(resource) {
// 使用資源URL作為鍵,避免重複
const key = resource.name;
if (!this.resourceMap.has(key)) {
this.resources.push(resource);
this.resourceMap.set(key, resource);
// 限制資源數量
if (this.resources.length > this.maxResources) {
const removed = this.resources.shift();
if (removed) {
this.resourceMap.delete(removed.name);
}
}
}
}
collectFinalResourceData() {
// 在頁面卸載前收集最終的資源數據
this.resources.forEach(resource => {
// 標記為最終數據
resource.final = true;
});
// 觸發最終收集事件
this.onFinalCollection && this.onFinalCollection(this.getResourceStats());
}
getResourceStats() {
if (this.resources.length === 0) {
return {
count: 0,
byType: {},
byDomain: {},
totalSize: 0,
averageLoadTime: 0
};
}
const byType = {};
const byDomain = {};
let totalSize = 0;
let totalLoadTime = 0;
this.resources.forEach(resource => {
// 按類型統計
const type = resource.initiatorType;
if (!byType[type]) {
byType[type] = {
count: 0,
totalSize: 0,
totalLoadTime: 0,
resources: []
};
}
byType[type].count++;
byType[type].totalSize += resource.transferSize || 0;
byType[type].totalLoadTime += resource.duration;
byType[type].resources.push(resource);
// 按域名統計
const domain = resource.domain;
if (!byDomain[domain]) {
byDomain[domain] = {
count: 0,
totalSize: 0,
totalLoadTime: 0,
resources: []
};
}
byDomain[domain].count++;
byDomain[domain].totalSize += resource.transferSize || 0;
byDomain[domain].totalLoadTime += resource.duration;
byDomain[domain].resources.push(resource);
// 累計總大小和加載時間
totalSize += resource.transferSize || 0;
totalLoadTime += resource.duration;
});
// 計算平均加載時間
const averageLoadTime = totalLoadTime / this.resources.length;
return {
count: this.resources.length,
byType: byType,
byDomain: byDomain,
totalSize: totalSize,
averageLoadTime: averageLoadTime,
resources: […this.resources]
};
}
getSlowResources(threshold = 1000) {
return this.resources.filter(resource => resource.duration > threshold);
}
getLargestResources(limit = 10) {
return […this.resources]
.sort((a, b) => (b.transferSize || 0) – (a.transferSize || 0))
.slice(0, limit);
}
clear() {
this.resources = [];
this.resourceMap.clear();
}
disconnect() {
if (this.observer) {
this.observer.disconnect();
}
}
}
// 使用示例
const resourceMonitor = new ResourceLoadMonitor();
resourceMonitor.onResourceLoaded = (resource) => {
if (resource.duration > 2000) {
console.warn(`資源加載過慢: ${resource.name} (${resource.duration.toFixed(2)}ms)`);
}
};
resourceMonitor.onFinalCollection = (stats) => {
console.log('資源加載統計:', stats);
// 發送到分析服務器
sendToAnalytics('resource_stats', stats);
};
3.2 圖片懶加載與監控
針對圖片資源的特殊監控和優化。
javascript
class ImageLoadMonitor {
constructor() {
this.images = [];
this.intersectionObserver = null;
this.maxImages = 100;
this.lazyLoadEnabled = false;
this.init();
}
init() {
// 監聽所有圖片加載事件
this.setupImageListeners();
// 初始化懶加載
this.initLazyLoad();
}
setupImageListeners() {
// 監聽頁面上已存在的圖片
document.querySelectorAll('img').forEach(img => {
this.monitorImage(img);
});
// 監聽動態添加的圖片
this.setupMutationObserver();
}
setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 檢查新增元素中的圖片
if (node.tagName === 'IMG') {
this.monitorImage(node);
}
// 檢查新增元素的子元素中的圖片
node.querySelectorAll('img').forEach(img => {
this.monitorImage(img);
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
monitorImage(img) {
// 跳過已監聽的圖片
if (img.dataset.monitored === 'true') {
return;
}
img.dataset.monitored = 'true';
// 記錄圖片初始信息
const imageInfo = {
src: img.src || img.dataset.src,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
displayWidth: img.width,
displayHeight: img.height,
loading: img.loading,
complete: img.complete,
startTime: performance.now(),
loadTime: null,
success: false,
error: false,
size: null,
isLazy: img.loading === 'lazy' || img.dataset.src
};
// 如果圖片已經加載完成
if (img.complete) {
this.handleImageLoad(img, imageInfo);
} else {
// 監聽加載事件
img.addEventListener('load', () => {
this.handleImageLoad(img, imageInfo);
});
// 監聽錯誤事件
img.addEventListener('error', () => {
this.handleImageError(img, imageInfo);
});
}
this.images.push(imageInfo);
// 限制記錄數量
if (this.images.length > this.maxImages) {
this.images.shift();
}
}
handleImageLoad(img, imageInfo) {
imageInfo.loadTime = performance.now() – imageInfo.startTime;
imageInfo.success = true;
imageInfo.naturalWidth = img.naturalWidth;
imageInfo.naturalHeight = img.naturalHeight;
imageInfo.displayWidth = img.width;
imageInfo.displayHeight = img.height;
imageInfo.complete = true;
// 嘗試獲取圖片大小
this.getImageSize(img.src).then(size => {
imageInfo.size = size;
}).catch(() => {
// 忽略錯誤
});
// 觸發圖片加載事件
this.onImageLoaded && this.onImageLoaded(imageInfo);
// 檢查是否尺寸不匹配
this.checkImageSizing(img, imageInfo);
}
handleImageError(img, imageInfo) {
imageInfo.loadTime = performance.now() – imageInfo.startTime;
imageInfo.error = true;
// 觸發圖片錯誤事件
this.onImageError && this.onImageError(imageInfo);
console.error(`圖片加載失敗: ${imageInfo.src}`);
}
async getImageSize(src) {
try {
const response = await fetch(src, { method: 'HEAD' });
const size = response.headers.get('content-length');
return size ? parseInt(size, 10) : null;
} catch (e) {
return null;
}
}
checkImageSizing(img, imageInfo) {
// 檢查圖片是否被縮放
if (imageInfo.naturalWidth > 0 && imageInfo.displayWidth > 0) {
const widthRatio = imageInfo.displayWidth / imageInfo.naturalWidth;
const heightRatio = imageInfo.displayHeight / imageInfo.naturalHeight;
// 如果顯示尺寸與原始尺寸差異過大
if (widthRatio < 0.5 || heightRatio < 0.5) {
console.warn(`圖片可能尺寸過大: ${imageInfo.src}`);
console.warn(`原始尺寸: ${imageInfo.naturalWidth}x${imageInfo.naturalHeight}`);
console.warn(`顯示尺寸: ${imageInfo.displayWidth}x${imageInfo.displayHeight}`);
imageInfo.oversized = true;
imageInfo.sizeRatio = Math.min(widthRatio, heightRatio);
}
}
}
initLazyLoad() {
if (!('IntersectionObserver' in window)) {
console.warn('瀏覽器不支持 IntersectionObserver,無法啟用懶加載');
return;
}
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadLazyImage(img);
this.intersectionObserver.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
this.enableLazyLoad();
}
enableLazyLoad() {
this.lazyLoadEnabled = true;
// 找到所有需要懶加載的圖片
const lazyImages = document.querySelectorAll('img[data-src], img[loading="lazy"]');
lazyImages.forEach(img => {
// 如果圖片在可視區域內,立即加載
if (this.isInViewport(img)) {
this.loadLazyImage(img);
} else {
// 否則開始觀察
this.intersectionObserver.observe(img);
}
});
}
isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
loadLazyImage(img) {
const src = img.dataset.src;
if (!src) return;
// 記錄開始時間
img.dataset.lazyLoadStart = performance.now();
// 設置src屬性開始加載
img.src = src;
// 移除data-src屬性
delete img.dataset.src;
// 監聽加載完成
img.addEventListener('load', () => {
const loadTime = performance.now() – parseFloat(img.dataset.lazyLoadStart);
console.log(`懶加載圖片完成: ${src} (${loadTime.toFixed(2)}ms)`);
// 觸發懶加載完成事件
this.onLazyImageLoaded && this.onLazyImageLoaded({
src: src,
loadTime: loadTime,
element: img
});
});
}
getImageStats() {
if (this.images.length === 0) {
return {
count: 0,
loaded: 0,
errors: 0,
averageLoadTime: 0,
totalSize: 0,
oversizedCount: 0
};
}
const loadedImages = this.images.filter(img => img.success);
const errorImages = this.images.filter(img => img.error);
const oversizedImages = this.images.filter(img => img.oversized);
const totalLoadTime = loadedImages.reduce((sum, img) => sum + img.loadTime, 0);
const totalSize = loadedImages.reduce((sum, img) => sum + (img.size || 0), 0);
return {
count: this.images.length,
loaded: loadedImages.length,
errors: errorImages.length,
averageLoadTime: loadedImages.length > 0 ? totalLoadTime / loadedImages.length : 0,
totalSize: totalSize,
oversizedCount: oversizedImages.length,
images: […this.images]
};
}
disconnect() {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
}
}
// 使用示例
const imageMonitor = new ImageLoadMonitor();
imageMonitor.onImageLoaded = (imageInfo) => {
if (imageInfo.loadTime > 1000) {
console.warn(`圖片加載過慢: ${imageInfo.src} (${imageInfo.loadTime.toFixed(2)}ms)`);
}
if (imageInfo.oversized) {
console.warn(`圖片尺寸過大,考慮優化: ${imageInfo.src}`);
}
};
imageMonitor.onLazyImageLoaded = (lazyImageInfo) => {
console.log(`懶加載圖片完成: ${lazyImageInfo.src}`);
};
第四章:綜合性能監控系統
4.1 性能數據收集器
整合各類性能監控,提供統一的數據收集接口。
javascript
class PerformanceCollector {
constructor(options = {}) {
this.options = {
samplingRate: 1.0, // 採樣率
maxEntries: 1000, // 最大記錄條目
reportInterval: 10000, // 上報間隔(ms)
enableFPS: true,
enableLongTasks: true,
enableLayoutShift: true,
enableResources: true,
enableImages: true,
…options
};
this.monitors = {};
this.data = {
fps: [],
longTasks: [],
layoutShifts: [],
resources: [],
images: [],
navigation: null,
paint: [],
customMetrics: []
};
this.reportTimer = null;
this.initialized = false;
this.init();
}
init() {
if (this.initialized) return;
// 初始化各個監控器
if (this.options.enableFPS) {
this.monitors.fps = new FPSMonitor();
this.monitors.fps.onFPSUpdate = (fps) => {
this.recordMetric('fps', {
timestamp: Date.now(),
value: fps
});
};
}
if (this.options.enableLongTasks) {
this.monitors.longTasks = new LongTaskMonitor();
this.monitors.longTasks.onLongTaskDetected = (task) => {
this.recordMetric('longTasks', task);
};
}
if (this.options.enableLayoutShift) {
this.monitors.layoutShift = new LayoutShiftMonitor();
this.monitors.layoutShift.onCLSUpdate = (clsData) => {
this.recordMetric('layoutShifts', {
timestamp: Date.now(),
…clsData
});
};
}
if (this.options.enableResources) {
this.monitors.resources = new ResourceLoadMonitor();
this.monitors.resources.onResourceLoaded = (resource) => {
this.recordMetric('resources', resource);
};
}
if (this.options.enableImages) {
this.monitors.images = new ImageLoadMonitor();
this.monitors.images.onImageLoaded = (imageInfo) => {
this.recordMetric('images', imageInfo);
};
}
// 收集導航計時數據
this.collectNavigationTiming();
// 收集繪製計時數據
this.collectPaintTiming();
// 設置定時上報
if (this.options.reportInterval > 0) {
this.setupReporting();
}
// 監聽頁面卸載
window.addEventListener('beforeunload', () => {
this.finalize();
});
this.initialized = true;
}
recordMetric(type, entry) {
// 採樣控制
if (Math.random() > this.options.samplingRate) {
return;
}
if (!this.data[type]) {
this.data[type] = [];
}
this.data[type].push(entry);
// 限制數據量
if (this.data[type].length > this.options.maxEntries) {
this.data[type].shift();
}
// 觸擊數據記錄事件
this.onMetricRecorded && this.onMetricRecorded(type, entry);
}
collectNavigationTiming() {
if (!window.performance || !window.performance.timing) {
return;
}
const timing = window.performance.timing;
this.data.navigation = {
dnsLookup: timing.domainLookupEnd – timing.domainLookupStart,
tcpConnect: timing.connectEnd – timing.connectStart,
request: timing.responseStart – timing.requestStart,
response: timing.responseEnd – timing.responseStart,
domInteractive: timing.domInteractive – timing.navigationStart,
domContentLoaded: timing.domContentLoadedEventEnd – timing.navigationStart,
domComplete: timing.domComplete – timing.navigationStart,
loadEvent: timing.loadEventEnd – timing.loadEventStart,
total: timing.loadEventEnd – timing.navigationStart
};
}
collectPaintTiming() {
if (!window.performance || !window.performance.getEntriesByType) {
return;
}
const paintEntries = performance.getEntriesByType('paint');
this.data.paint = paintEntries.map(entry => ({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration
}));
}
setupReporting() {
this.reportTimer = setInterval(() => {
this.report();
}, this.options.reportInterval);
}
report() {
const reportData = this.getSummary();
// 觸發上報事件
this.onReport && this.onReport(reportData);
// 發送到服務器
if (this.options.reportUrl) {
this.sendToServer(reportData);
}
}
sendToServer(data) {
// 使用navigator.sendBeacon或fetch發送數據
const beaconData = JSON.stringify(data);
if (navigator.sendBeacon && this.options.reportUrl) {
navigator.sendBeacon(this.options.reportUrl, beaconData);
} else {
// 備用方案:使用fetch
fetch(this.options.reportUrl, {
method: 'POST',
body: beaconData,
headers: {
'Content-Type': 'application/json'
},
keepalive: true // 確保在頁面卸載時也能發送
}).catch(error => {
console.error('性能數據上報失敗:', error);
});
}
}
getSummary() {
const summary = {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
connection: this.getConnectionInfo(),
device: this.getDeviceInfo(),
vitals: this.getWebVitals(),
metrics: {}
};
// 計算各個指標的統計數據
if (this.data.fps.length > 0) {
const fpsValues = this.data.fps.map(entry => entry.value);
summary.metrics.fps = {
current: fpsValues[fpsValues.length – 1],
average: this.calculateAverage(fpsValues),
min: Math.min(…fpsValues),
max: Math.max(…fpsValues)
};
}
if (this.data.longTasks.length > 0) {
const durations = this.data.longTasks.map(task => task.duration);
summary.metrics.longTasks = {
count: this.data.longTasks.length,
averageDuration: this.calculateAverage(durations),
maxDuration: Math.max(…durations),
totalDuration: durations.reduce((sum, duration) => sum + duration, 0)
};
}
if (this.data.layoutShifts.length > 0) {
const clsValues = this.data.layoutShifts.map(shift => shift.cumulative);
summary.metrics.layoutShift = {
current: clsValues[clsValues.length – 1] || 0,
max: Math.max(…clsValues),
count: this.data.layoutShifts.length
};
}
if (this.data.resources.length > 0) {
const resourceDurations = this.data.resources.map(resource => resource.duration);
const resourceSizes = this.data.resources
.map(resource => resource.transferSize || 0)
.filter(size => size > 0);
summary.metrics.resources = {
count: this.data.resources.length,
averageLoadTime: this.calculateAverage(resourceDurations),
totalSize: resourceSizes.reduce((sum, size) => sum + size, 0),
byType: this.groupBy(this.data.resources, 'initiatorType')
};
}
if (this.data.images.length > 0) {
const loadedImages = this.data.images.filter(img => img.success);
const imageLoadTimes = loadedImages.map(img => img.loadTime);
summary.metrics.images = {
count: this.data.images.length,
loaded: loadedImages.length,
errors: this.data.images.filter(img => img.error).length,
averageLoadTime: this.calculateAverage(imageLoadTimes),
oversized: this.data.images.filter(img => img.oversized).length
};
}
// 添加導航計時數據
if (this.data.navigation) {
summary.metrics.navigation = this.data.navigation;
}
// 添加繪製計時數據
if (this.data.paint.length > 0) {
summary.metrics.paint = this.data.paint;
}
return summary;
}
calculateAverage(values) {
if (values.length === 0) return 0;
const sum = values.reduce((acc, val) => acc + val, 0);
return sum / values.length;
}
groupBy(array, key) {
return array.reduce((result, item) => {
const groupKey = item[key] || 'unknown';
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {});
}
getConnectionInfo() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!connection) {
return { type: 'unknown' };
}
return {
type: connection.type || 'unknown',
effectiveType: connection.effectiveType || 'unknown',
downlink: connection.downlink || 0,
rtt: connection.rtt || 0,
saveData: connection.saveData || false
};
}
getDeviceInfo() {
const ua = navigator.userAgent;
const mobile = /Mobi|Android|iPhone|iPad|iPod/i.test(ua);
return {
mobile: mobile,
userAgent: ua,
screen: {
width: window.screen.width,
height: window.screen.height,
availWidth: window.screen.availWidth,
availHeight: window.screen.availHeight,
colorDepth: window.screen.colorDepth,
pixelDepth: window.screen.pixelDepth
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
};
}
getWebVitals() {
const vitals = {};
// 計算LCP(最大內容繪製)
const lcpEntries = performance.getEntriesByName('largest-contentful-paint');
if (lcpEntries.length > 0) {
const lcpEntry = lcpEntries[lcpEntries.length – 1];
vitals.lcp = lcpEntry.startTime;
}
// 計算FID(首次輸入延遲)
// 需要通過PerformanceObserver監聽first-input
// 計算CLS(累積佈局偏移)
if (this.monitors.layoutShift) {
const clsStats = this.monitors.layoutShift.getCLSStats();
vitals.cls = clsStats.cumulative;
}
// 計算FCP(首次內容繪製)
const fcpEntries = performance.getEntriesByName('first-contentful-paint');
if (fcpEntries.length > 0) {
vitals.fcp = fcpEntries[0].startTime;
}
return vitals;
}
addCustomMetric(name, value) {
this.recordMetric('customMetrics', {
timestamp: Date.now(),
name: name,
value: value
});
}
finalize() {
// 停止所有監控器
Object.values(this.monitors).forEach(monitor => {
if (monitor.stop) monitor.stop();
if (monitor.disconnect) monitor.disconnect();
});
// 停止定時器
if (this.reportTimer) {
clearInterval(this.reportTimer);
}
// 發送最終報告
this.report();
}
destroy() {
this.finalize();
this.monitors = {};
this.data = {};
this.initialized = false;
}
}
// 使用示例
const performanceCollector = new PerformanceCollector({
samplingRate: 0.1, // 10%的採樣率
reportInterval: 30000, // 每30秒上報一次
reportUrl: '/api/performance-metrics'
});
performanceCollector.onReport = (reportData) => {
console.log('性能報告:', reportData);
};
performanceCollector.onMetricRecorded = (type, entry) => {
// 可以實時處理特定類型的性能數據
if (type === 'longTasks' && entry.duration > 100) {
console.warn('檢測到長任務:', entry);
}
};
4.2 性能數據可視化儀表板
創建一個實時性能監控儀表板,展示關鍵性能指標。
javascript
class PerformanceDashboard {
constructor(containerId, collector) {
this.container = document.getElementById(containerId);
this.collector = collector;
this.charts = {};
this.metrics = {};
if (!this.container) {
console.error(`容器元素未找到: #${containerId}`);
return;
}
this.init();
}
init() {
// 創建儀表板佈局
this.createLayout();
// 初始化圖表
this.initCharts();
// 開始更新數據
this.startUpdating();
}
createLayout() {
this.container.innerHTML = '';
this.container.className = 'performance-dashboard';
// 創建標題
const header = document.createElement('div');
header.className = 'dashboard-header';
header.innerHTML = '<h2>實時性能監控儀表板</h2>';
this.container.appendChild(header);
// 創建指標卡片容器
const metricsContainer = document.createElement('div');
metricsContainer.className = 'metrics-container';
this.container.appendChild(metricsContainer);
// 創建圖表容器
const chartsContainer = document.createElement('div');
chartsContainer.className = 'charts-container';
this.container.appendChild(chartsContainer);
// 創建詳細信息容器
const detailsContainer = document.createElement('div');
detailsContainer.className = 'details-container';
this.container.appendChild(detailsContainer);
// 保存引用
this.metricsContainer = metricsContainer;
this.chartsContainer = chartsContainer;
this.detailsContainer = detailsContainer;
// 創建指標卡片
this.createMetricCards();
// 創建圖表區域
this.createChartAreas();
}
createMetricCards() {
const metrics = [
{ id: 'fps', name: 'FPS', color: '#4CAF50', format: val => `${val} fps` },
{ id: 'cls', name: 'CLS', color: '#FF9800', format: val => val.toFixed(3) },
{ id: 'lcp', name: 'LCP', color: '#2196F3', format: val => `${val.toFixed(0)} ms` },
{ id: 'resources', name: '資源數量', color: '#9C27B0', format: val => `${val} 個` },
{ id: 'longTasks', name: '長任務', color: '#F44336', format: val => `${val} 個` },
{ id: 'memory', name: '內存', color: '#00BCD4', format: val => this.formatMemory(val) }
];
metrics.forEach(metric => {
const card = document.createElement('div');
card.className = 'metric-card';
card.innerHTML = `
<div class="metric-name">${metric.name}</div>
<div class="metric-value" id="metric-${metric.id}">–</div>
<div class="metric-trend" id="trend-${metric.id}"></div>
`;
// 設置顏色
card.style.borderLeft = `4px solid ${metric.color}`;
this.metricsContainer.appendChild(card);
// 保存metric配置
this.metrics[metric.id] = {
…metric,
element: document.getElementById(`metric-${metric.id}`),
trendElement: document.getElementById(`trend-${metric.id}`),
history: []
};
});
}
createChartAreas() {
const chartConfigs = [
{ id: 'fpsChart', title: 'FPS趨勢圖', type: 'line', metric: 'fps' },
{ id: 'resourceChart', title: '資源加載時間', type: 'bar', metric: 'resources' },
{ id: 'longTaskChart', title: '長任務分佈', type: 'bar', metric: 'longTasks' },
{ id: 'memoryChart', title: '內存使用情況', type: 'line', metric: 'memory' }
];
chartConfigs.forEach(config => {
const chartContainer = document.createElement('div');
chartContainer.className = 'chart-container';
chartContainer.innerHTML = `<h3>${config.title}</h3><canvas id="${config.id}"></canvas>`;
this.chartsContainer.appendChild(chartContainer);
});
}
initCharts() {
// 初始化FPS圖表
const fpsCanvas = document.getElementById('fpsChart');
if (fpsCanvas) {
this.charts.fps = this.createLineChart(fpsCanvas, 'FPS', ['#4CAF50', '#FF9800']);
}
// 初始化資源圖表
const resourceCanvas = document.getElementById('resourceChart');
if (resourceCanvas) {
this.charts.resource = this.createBarChart(resourceCanvas, '資源加載時間(ms)');
}
// 初始化長任務圖表
const longTaskCanvas = document.getElementById('longTaskChart');
if (longTaskCanvas) {
this.charts.longTask = this.createBarChart(longTaskCanvas, '任務時長(ms)');
}
// 初始化內存圖表
const memoryCanvas = document.getElementById('memoryChart');
if (memoryCanvas) {
this.charts.memory = this.createLineChart(memoryCanvas, '內存使用(MB)', ['#00BCD4', '#FF5722']);
}
}
createLineChart(canvas, label, colors) {
const ctx = canvas.getContext('2d');
// 設置Canvas尺寸
canvas.width = canvas.parentElement.clientWidth;
canvas.height = 200;
return {
ctx: ctx,
canvas: canvas,
data: {
labels: [],
datasets: [
{
label: label,
data: [],
borderColor: colors[0],
backgroundColor: colors[0] + '20',
borderWidth: 2,
fill: true
}
]
},
config: {
type: 'line',
data: {},
options: {
responsive: false,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
}
};
}
createBarChart(canvas, label) {
const ctx = canvas.getContext('2d');
// 設置Canvas尺寸
canvas.width = canvas.parentElement.clientWidth;
canvas.height = 200;
return {
ctx: ctx,
canvas: canvas,
data: {
labels: [],
datasets: [
{
label: label,
data: [],
backgroundColor: '#2196F3',
borderColor: '#1976D2',
borderWidth: 1
}
]
}
};
}
startUpdating() {
// 更新指標
this.updateMetrics();
// 定期更新
this.updateInterval = setInterval(() => {
this.updateMetrics();
}, 1000);
// 監聽性能數據
if (this.collector && this.collector.onMetricRecorded) {
const originalCallback = this.collector.onMetricRecorded;
this.collector.onMetricRecorded = (type, entry) => {
originalCallback && originalCallback(type, entry);
this.handleNewMetric(type, entry);
};
}
}
updateMetrics() {
if (!this.collector) return;
const summary = this.collector.getSummary();
// 更新FPS
if (this.metrics.fps && summary.metrics.fps) {
this.updateMetric('fps', summary.metrics.fps.current);
// 更新FPS圖表
if (this.charts.fps) {
this.updateChart(this.charts.fps, summary.metrics.fps.current);
}
}
// 更新CLS
if (this.metrics.cls && summary.metrics.layoutShift) {
this.updateMetric('cls', summary.metrics.layoutShift.current);
}
// 更新LCP
if (this.metrics.lcp && summary.vitals && summary.vitals.lcp) {
this.updateMetric('lcp', summary.vitals.lcp);
}
// 更新資源數量
if (this.metrics.resources && summary.metrics.resources) {
this.updateMetric('resources', summary.metrics.resources.count);
}
// 更新長任務數量
if (this.metrics.longTasks && summary.metrics.longTasks) {
this.updateMetric('longTasks', summary.metrics.longTasks.count);
}
// 更新內存使用情況
if (this.metrics.memory && performance.memory) {
const memoryMB = performance.memory.usedJSHeapSize / (1024 * 1024);
this.updateMetric('memory', memoryMB);
// 更新內存圖表
if (this.charts.memory) {
this.updateChart(this.charts.memory, memoryMB);
}
}
// 更新詳細信息
this.updateDetails(summary);
}
updateMetric(id, value) {
const metric = this.metrics[id];
if (!metric || !metric.element) return;
// 格式化值
const formattedValue = metric.format ? metric.format(value) : value;
// 更新顯示
metric.element.textContent = formattedValue;
// 更新歷史記錄
metric.history.push({
timestamp: Date.now(),
value: value
});
// 限制歷史記錄長度
if (metric.history.length > 20) {
metric.history.shift();
}
// 更新趨勢指示器
this.updateTrendIndicator(id);
}
updateTrendIndicator(id) {
const metric = this.metrics[id];
if (!metric || metric.history.length < 2) return;
const current = metric.history[metric.history.length – 1].value;
const previous = metric.history[metric.history.length – 2].value;
const trend = current – previous;
let trendText = '';
let trendClass = '';
if (Math.abs(trend) < 0.01) {
trendText = '→';
trendClass = 'neutral';
} else if (trend > 0) {
trendText = '↑';
trendClass = id === 'cls' || id === 'longTasks' ? 'negative' : 'positive';
} else {
trendText = '↓';
trendClass = id === 'cls' || id === 'longTasks' ? 'positive' : 'negative';
}
metric.trendElement.textContent = trendText;
metric.trendElement.className = `metric-trend ${trendClass}`;
}
updateChart(chart, value) {
if (!chart.data.labels) return;
// 添加新數據
const timestamp = new Date().toLocaleTimeString();
chart.data.labels.push(timestamp);
chart.data.datasets[0].data.push(value);
// 限制數據量
if (chart.data.labels.length > 20) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
// 重繪圖表
this.drawChart(chart);
}
drawChart(chart) {
const ctx = chart.ctx;
const width = chart.canvas.width;
const height = chart.canvas.height;
const data = chart.data.datasets[0].data;
if (data.length === 0) return;
// 清除畫布
ctx.clearRect(0, 0, width, height);
// 計算統計值
const max = Math.max(…data);
const min = Math.min(…data);
// 設置繪圖區域
const padding = 40;
const graphWidth = width – padding * 2;
const graphHeight = height – padding * 2;
// 繪製網格
ctx.strokeStyle = '#E0E0E0';
ctx.lineWidth = 1;
// 水平網格線
const gridLines = 5;
for (let i = 0; i <= gridLines; i++) {
const y = padding + (graphHeight / gridLines) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width – padding, y);
ctx.stroke();
// 標籤
const value = max – (max – min) * (i / gridLines);
ctx.fillStyle = '#666';
ctx.font = '10px Arial';
ctx.textAlign = 'right';
ctx.fillText(value.toFixed(1), padding – 5, y + 3);
}
// 繪製數據線(線圖)或柱狀圖
if (chart.config && chart.config.type === 'line') {
this.drawLineChart(ctx, data, padding, graphWidth, graphHeight, max, min);
} else {
this.drawBarChart(ctx, data, padding, graphWidth, graphHeight, max);
}
// 繪製標題
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(chart.data.datasets[0].label, width / 2, 15);
}
drawLineChart(ctx, data, padding, graphWidth, graphHeight, max, min) {
if (data.length < 2) return;
const valueRange = max – min || 1;
ctx.strokeStyle = '#4CAF50';
ctx.lineWidth = 2;
ctx.fillStyle = '#4CAF5020';
ctx.beginPath();
// 繪製數據點
data.forEach((value, index) => {
const x = padding + (index / (data.length – 1)) * graphWidth;
const y = padding + graphHeight – ((value – min) / valueRange) * graphHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// 填充區域
ctx.lineTo(padding + graphWidth, padding + graphHeight);
ctx.lineTo(padding, padding + graphHeight);
ctx.closePath();
ctx.fill();
// 繪製數據點
data.forEach((value, index) => {
const x = padding + (index / (data.length – 1)) * graphWidth;
const y = padding + graphHeight – ((value – min) / valueRange) * graphHeight;
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#4CAF50';
ctx.fill();
});
}
drawBarChart(ctx, data, padding, graphWidth, graphHeight, max) {
const barWidth = graphWidth / data.length;
const maxValue = max || 1;
data.forEach((value, index) => {
const x = padding + index * barWidth;
const barHeight = (value / maxValue) * graphHeight;
const y = padding + graphHeight – barHeight;
// 繪製柱狀圖
ctx.fillStyle = '#2196F3';
ctx.fillRect(x + 2, y, barWidth – 4, barHeight);
// 繪製數值標籤
if (barHeight > 15) {
ctx.fillStyle = '#FFF';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.fillText(value.toFixed(0), x + barWidth / 2, y + barHeight – 5);
}
});
}
updateDetails(summary) {
if (!this.detailsContainer) return;
let detailsHTML = '<h3>性能詳情</h3>';
// 網絡信息
if (summary.connection) {
detailsHTML += `
<div class="detail-section">
<h4>網絡連接</h4>
<p>類型: ${summary.connection.effectiveType}</p>
<p>下載速度: ${summary.connection.downlink} Mbps</p>
<p>RTT: ${summary.connection.rtt} ms</p>
</div>
`;
}
// 設備信息
if (summary.device) {
detailsHTML += `
<div class="detail-section">
<h4>設備信息</h4>
<p>設備類型: ${summary.device.mobile ? '移動設備' : '桌面設備'}</p>
<p>屏幕分辨率: ${summary.device.screen.width} × ${summary.device.screen.height}</p>
<p>視窗大小: ${summary.device.viewport.width} × ${summary.device.viewport.height}</p>
</div>
`;
}
// Web Vitals
if (summary.vitals) {
detailsHTML += `
<div class="detail-section">
<h4>核心網頁指標</h4>
<p>LCP: ${summary.vitals.lcp ? summary.vitals.lcp.toFixed(0) + ' ms' : 'N/A'}</p>
<p>CLS: ${summary.vitals.cls ? summary.vitals.cls.toFixed(3) : 'N/A'}</p>
<p>FCP: ${summary.vitals.fcp ? summary.vitals.fcp.toFixed(0) + ' ms' : 'N/A'}</p>
</div>
`;
}
// 資源統計
if (summary.metrics.resources) {
const resources = summary.metrics.resources;
detailsHTML += `
<div class="detail-section">
<h4>資源加載</h4>
<p>總資源數: ${resources.count}</p>
<p>平均加載時間: ${resources.averageLoadTime ? resources.averageLoadTime.toFixed(0) + ' ms' : 'N/A'}</p>
<p>總大小: ${this.formatBytes(resources.totalSize)}</p>
</div>
`;
}
// 長任務統計
if (summary.metrics.longTasks) {
const longTasks = summary.metrics.longTasks;
detailsHTML += `
<div class="detail-section">
<h4>長任務統計</h4>
<p>數量: ${longTasks.count}</p>
<p>最長任務: ${longTasks.maxDuration ? longTasks.maxDuration.toFixed(0) + ' ms' : 'N/A'}</p>
<p>總阻塞時間: ${longTasks.totalDuration ? longTasks.totalDuration.toFixed(0) + ' ms' : 'N/A'}</p>
</div>
`;
}
this.detailsContainer.innerHTML = detailsHTML;
}
formatMemory(mb) {
if (mb < 1024) {
return `${mb.toFixed(1)} MB`;
} else {
return `${(mb / 1024).toFixed(1)} GB`;
}
}
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
handleNewMetric(type, entry) {
// 根據metric類型處理新數據
switch (type) {
case 'longTasks':
this.handleNewLongTask(entry);
break;
case 'resources':
this.handleNewResource(entry);
break;
// 可以添加其他類型的處理
}
}
handleNewLongTask(task) {
// 在詳細信息中顯示新的長任務
const alert = document.createElement('div');
alert.className = 'alert alert-warning';
alert.textContent = `檢測到長任務: ${task.duration.toFixed(0)}ms`;
this.detailsContainer.prepend(alert);
// 5秒後移除
setTimeout(() => {
alert.remove();
}, 5000);
}
handleNewResource(resource) {
// 如果資源加載過慢,顯示警告
if (resource.duration > 2000) {
const alert = document.createElement('div');
alert.className = 'alert alert-info';
alert.textContent = `資源加載緩慢: ${this.getResourceName(resource.name)} (${resource.duration.toFixed(0)}ms)`;
this.detailsContainer.prepend(alert);
// 5秒後移除
setTimeout(() => {
alert.remove();
}, 5000);
}
}
getResourceName(url) {
try {
const urlObj = new URL(url);
return urlObj.pathname.split('/').pop() || urlObj.hostname;
} catch (e) {
return url.substring(0, 50) + (url.length > 50 ? '…' : '');
}
}
destroy() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
this.container.innerHTML = '';
}
}
// 使用示例
// 假設頁面上有一個id為"dashboard"的div元素
const collector = new PerformanceCollector();
const dashboard = new PerformanceDashboard('dashboard', collector);
4.3 CSS樣式(用於儀表板)
css
.performance-dashboard {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
margin-bottom: 20px;
text-align: center;
}
.dashboard-header h2 {
margin: 0;
color: #333;
font-size: 24px;
}
.metrics-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.metric-card {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.metric-name {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.metric-trend {
font-size: 18px;
font-weight: bold;
}
.metric-trend.positive {
color: #4CAF50;
}
.metric-trend.negative {
color: #F44336;
}
.metric-trend.neutral {
color: #FF9800;
}
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.chart-container h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: #333;
text-align: center;
}
.chart-container canvas {
width: 100%;
height: 200px;
}
.details-container {
background: white;
padding: 20px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.details-container h3 {
margin: 0 0 20px 0;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.detail-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.detail-section h4 {
margin: 0 0 10px 0;
color: #555;
font-size: 14px;
}
.detail-section p {
margin: 5px 0;
font-size: 13px;
color: #666;
}
.alert {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 4px;
font-size: 13px;
}
.alert-warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
第五章:性能優化策略與最佳實踐
5.1 基於監控數據的性能優化
根據性能監控數據,實施針對性的優化策略:
針對FPS過低:
-
減少複雜的CSS選擇器
-
使用transform和opacity實現動畫
-
避免在滾動事件中執行複雜計算
-
使用will-change屬性提示瀏覽器優化
針對長任務:
-
將耗時任務拆分為多個小任務
-
使用Web Workers處理計算密集型任務
-
使用requestIdleCallback執行低優先級任務
-
優化JavaScript代碼執行效率
針對CLS過高:
-
為圖片和媒體元素指定尺寸
-
預留廣告位空間
-
避免在現有內容上方插入新內容
-
使用CSS aspect-ratio屬性
針對資源加載緩慢:
-
實施資源懶加載
-
使用CDN加速靜態資源
-
壓縮圖片和資源文件
-
實施HTTP/2或HTTP/3
5.2 性能監控部署建議
生產環境部署:
-
使用採樣率控制數據量
-
設置合理的上報頻率
-
實施數據壓縮和批量上報
-
確保監控代碼不會影響正常業務
數據分析與可視化:
-
建立性能基線
-
設置性能告警閾值
-
創建性能趨勢圖表
-
關聯業務指標與性能數據
A/B測試與優化驗證:
-
對比優化前後的性能數據
-
驗證性能優化對業務指標的影響
-
建立持續的性能優化流程
第六章:高級主題與未來發展
6.1 服務端性能監控
javascript
// Node.js服務端性能監控示例
const serverPerformanceMonitor = {
startTime: null,
requests: [],
init() {
this.startTime = Date.now();
// 監控內存使用
setInterval(() => {
this.recordMemoryUsage();
}, 60000);
},
recordRequest(req, res, duration) {
const requestRecord = {
timestamp: Date.now(),
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
memoryUsage: process.memoryUsage()
};
this.requests.push(requestRecord);
// 限制記錄數量
if (this.requests.length > 1000) {
this.requests.shift();
}
},
recordMemoryUsage() {
const memory = process.memoryUsage();
console.log('內存使用情況:');
console.log(` RSS: ${Math.round(memory.rss / 1024 / 1024)} MB`);
console.log(` Heap Total: ${Math.round(memory.heapTotal / 1024 / 1024)} MB`);
console.log(` Heap Used: ${Math.round(memory.heapUsed / 1024 / 1024)} MB`);
// 內存使用過高告警
if (memory.heapUsed / memory.heapTotal > 0.8) {
console.warn('內存使用率超過80%,可能導致性能問題');
}
},
getStats() {
const now = Date.now();
const uptime = now – this.startTime;
// 計算請求統計
const requestDurations = this.requests.map(req => req.duration);
const avgDuration = requestDurations.length > 0 ?
requestDurations.reduce((a, b) => a + b, 0) / requestDurations.length : 0;
return {
uptime: uptime,
totalRequests: this.requests.length,
avgRequestDuration: avgDuration,
requestsPerMinute: this.calculateRPM(),
memoryUsage: process.memoryUsage()
};
},
calculateRPM() {
const now = Date.now();
const oneMinuteAgo = now – 60000;
const recentRequests = this.requests.filter(req => req.timestamp > oneMinuteAgo);
return recentRequests.length;
}
};
6.2 分佈式系統性能監控
在微服務架構中,需要實現跨服務的性能追蹤:
請求追蹤ID:為每個用戶請求生成唯一ID,在所有服務間傳遞
跨度追蹤:記錄請求在每個服務中的處理時間
依賴關係映射:可視化服務間的依賴關係和性能瓶頸
6.3 人工智能在性能監控中的應用
異常檢測:使用機器學習算法自動檢測性能異常
根因分析:自動分析性能問題的根本原因
預測性監控:預測潛在的性能問題
自動優化建議:基於性能數據提供優化建議
結論
性能監控是現代Web開發不可或缺的一部分。通過手動構建性能監控系統,開發者可以更深入地理解性能指標的含義,更精準地定位性能瓶頸,並實施有效的優化策略。本文介紹的監控系統涵蓋了從渲染性能到資源加載的各個方面,提供了完整的實現方案和實用建議。
隨著Web技術的發展,性能監控也在不斷進化。未來,我們可以期待更加智能化、自動化的監控解決方案,但核心原理和基礎技術將保持不變。掌握這些基礎知識,將幫助開發者更好地應對未來的性能挑戰。
性能優化是一個持續的過程,需要不斷監控、分析和改進。建立完善的性能監控體系,是實現卓越用戶體驗的關鍵一步。


