欢迎光临
我们一直在努力

手搓HTML性能监控:實時檢測渲染性能與資源加載

手搓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技術的發展,性能監控也在不斷進化。未來,我們可以期待更加智能化、自動化的監控解決方案,但核心原理和基礎技術將保持不變。掌握這些基礎知識,將幫助開發者更好地應對未來的性能挑戰。

    性能優化是一個持續的過程,需要不斷監控、分析和改進。建立完善的性能監控體系,是實現卓越用戶體驗的關鍵一步。

    赞(0)
    未经允许不得转载:171主机测评 » 手搓HTML性能监控:實時檢測渲染性能與資源加載
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址