性能优化的核心指标

在开始优化之前,我们先要明确几个核心指标:

1. FP (First Paint) - 首次绘制

用户看到第一个像素的时间。

2. FCP (First Contentful Paint) - 首次内容绘制

用户看到第一个有意义内容的时间。

3. LCP (Largest Contentful Paint) - 最大内容绘制

页面最大内容元素渲染完成的时间。

4. FID (First Input Delay) - 首次输入延迟

用户第一次交互操作到浏览器实际开始处理的时间。

5. CLS (Cumulative Layout Shift) - 累积布局偏移

页面加载过程中布局变化的程度。

页面加载优化

资源压缩和合并

最基础也是最有效的优化手段就是资源压缩。

 1const TerserPlugin = require('terser-webpack-plugin');
 2const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
 3
 4module.exports = {
 5    optimization: {
 6        minimizer: [
 7            new TerserPlugin({
 8                terserOptions: {
 9                    compress: {
10                        drop_console: true, 
11                        drop_debugger: true, 
12                    },
13                },
14            }),
15            new CssMinimizerPlugin(),
16        ],
17        splitChunks: {
18            chunks: 'all',
19            cacheGroups: {
20                vendor: {
21                    test: /[\\/]node_modules[\\/]/,
22                    name: 'vendors',
23                    chunks: 'all',
24                },
25                common: {
26                    minChunks: 2,
27                    chunks: 'all',
28                    enforce: true,
29                },
30            },
31        },
32    },
33};

图片优化策略

图片往往是页面体积的大头,优化图片能带来显著的效果。

 1const ImageOptimizer = {
 2    
 3    getOptimalImageSrc(src, size) {
 4        const dpr = window.devicePixelRatio || 1;
 5        const optimalSize = Math.ceil(size * dpr);
 6        return `${src}?w=${optimalSize}`;
 7    },
 8    
 9    
10    lazyLoad() {
11        const images = document.querySelectorAll('img[data-src]');
12        const imageObserver = new IntersectionObserver((entries, observer) => {
13            entries.forEach(entry => {
14                if (entry.isIntersecting) {
15                    const img = entry.target;
16                    img.src = img.dataset.src;
17                    img.classList.remove('lazy');
18                    observer.unobserve(img);
19                }
20            });
21        });
22        
23        images.forEach(img => imageObserver.observe(img));
24    },
25    
26    
27    supportWebP() {
28        return new Promise(resolve => {
29            const webP = new Image();
30            webP.onload = webP.onerror = function () {
31                resolve(webP.height === 2);
32            };
33            webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
34        });
35    }
36};
37
38
39ImageOptimizer.supportWebP().then(supported => {
40    const imageFormat = supported ? 'webp' : 'jpg';
41    
42});

关键资源预加载

合理使用预加载技术可以显著提升用户体验。

 1<link rel="dns-prefetch" href="//api.example.com">
 2<link rel="dns-prefetch" href="//cdn.example.com">
 3
 4
 5<link rel="preconnect" href="https://fonts.googleapis.com">
 6
 7
 8<link rel="preload" href="/critical-styles.css" as="style">
 9<link rel="preload" href="/hero-image.jpg" as="image">
10
11
12<link rel="prefetch" href="/next-page.html">
 1const preloadManager = {
 2    
 3    preloadCriticalRoutes() {
 4        const criticalRoutes = ['/api/user', '/api/config'];
 5        criticalRoutes.forEach(url => {
 6            const link = document.createElement('link');
 7            link.rel = 'prefetch';
 8            link.href = url;
 9            document.head.appendChild(link);
10        });
11    },
12    
13    
14    preloadComponent(componentPath) {
15        return import( componentPath);
16    }
17};

渲染性能优化

虚拟滚动实现

对于长列表,虚拟滚动是必不可少的优化手段。

 1class VirtualList {
 2    constructor(container, options) {
 3        this.container = container;
 4        this.options = {
 5            itemHeight: 50,
 6            bufferSize: 5,
 7            ...options
 8        };
 9        this.data = [];
10        this.visibleStart = 0;
11        this.visibleEnd = 0;
12        this.scrollTop = 0;
13        this.init();
14    }
15    
16    init() {
17        this.container.style.overflow = 'auto';
18        this.container.style.position = 'relative';
19        
20        this.placeholder = document.createElement('div');
21        this.content = document.createElement('div');
22        
23        this.container.appendChild(this.placeholder);
24        this.container.appendChild(this.content);
25        
26        this.container.addEventListener('scroll', this.handleScroll.bind(this));
27    }
28    
29    setData(data) {
30        this.data = data;
31        this.updateVisibleRange();
32        this.render();
33    }
34    
35    handleScroll() {
36        this.scrollTop = this.container.scrollTop;
37        this.updateVisibleRange();
38        this.render();
39    }
40    
41    updateVisibleRange() {
42        const containerHeight = this.container.clientHeight;
43        const start = Math.floor(this.scrollTop / this.options.itemHeight);
44        const visibleCount = Math.ceil(containerHeight / this.options.itemHeight);
45        const buffer = this.options.bufferSize;
46        
47        this.visibleStart = Math.max(0, start - buffer);
48        this.visibleEnd = Math.min(
49            this.data.length, 
50            start + visibleCount + buffer
51        );
52    }
53    
54    render() {
55        const visibleData = this.data.slice(this.visibleStart, this.visibleEnd);
56        const totalHeight = this.data.length * this.options.itemHeight;
57        const offsetY = this.visibleStart * this.options.itemHeight;
58        
59        this.placeholder.style.height = `${totalHeight}px`;
60        this.content.style.transform = `translateY(${offsetY}px)`;
61        
62        this.content.innerHTML = visibleData
63            .map((item, index) => this.options.renderItem(
64                item, 
65                this.visibleStart + index
66            ))
67            .join('');
68    }
69}
70
71
72const virtualList = new VirtualList(document.getElementById('list'), {
73    itemHeight: 60,
74    renderItem: (item, index) => `
75        <div class="list-item" style="height: 60px;">
76            <span>${item.name}</span>
77            <span>${item.value}</span>
78        </div>
79    `
80});
81
82
83const largeData = Array.from({ length: 10000 }, (_, i) => ({
84    id: i,
85    name: `Item ${i}`,
86    value: Math.random()
87}));
88
89virtualList.setData(largeData);

防抖和节流

对于频繁触发的事件,防抖和节流是必备的优化手段。

 1function debounce(func, wait, immediate) {
 2    let timeout;
 3    return function executedFunction(...args) {
 4        const later = () => {
 5            timeout = null;
 6            if (!immediate) func.apply(this, args);
 7        };
 8        const callNow = immediate && !timeout;
 9        clearTimeout(timeout);
10        timeout = setTimeout(later, wait);
11        if (callNow) func.apply(this, args);
12    };
13}
14
15
16function throttle(func, limit) {
17    let inThrottle;
18    return function(...args) {
19        if (!inThrottle) {
20            func.apply(this, args);
21            inThrottle = true;
22            setTimeout(() => inThrottle = false, limit);
23        }
24    };
25}
26
27
28const searchInput = document.getElementById('search');
29const searchResults = document.getElementById('results');
30
31
32const debouncedSearch = debounce(async (query) => {
33    if (query.length < 2) return;
34    
35    try {
36        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
37        const data = await response.json();
38        renderSearchResults(data);
39    } catch (error) {
40        console.error('Search failed:', error);
41    }
42}, 300);
43
44searchInput.addEventListener('input', (e) => {
45    debouncedSearch(e.target.value);
46});
47
48
49const throttledScroll = throttle(() => {
50    const scrollTop = window.pageYOffset;
51    
52    updateScrollIndicator(scrollTop);
53}, 16); 
54
55window.addEventListener('scroll', throttledScroll);

网络请求优化

请求合并和缓存

合理合并请求和使用缓存能显著减少网络开销。

 1class RequestManager {
 2    constructor() {
 3        this.cache = new Map();
 4        this.pendingRequests = new Map();
 5        this.batchQueue = [];
 6        this.batchTimer = null;
 7    }
 8    
 9    
10    async get(url, options = {}) {
11        const cacheKey = `${url}_${JSON.stringify(options)}`;
12        const cached = this.cache.get(cacheKey);
13        
14        
15        if (cached && Date.now() - cached.timestamp < (options.cacheTime || 300000)) {
16            return cached.data;
17        }
18        
19        
20        if (this.pendingRequests.has(cacheKey)) {
21            return this.pendingRequests.get(cacheKey);
22        }
23        
24        const requestPromise = fetch(url, {
25            method: 'GET',
26            ...options
27        }).then(response => response.json())
28          .then(data => {
29              
30              this.cache.set(cacheKey, {
31                  data,
32                  timestamp: Date.now()
33              });
34              this.pendingRequests.delete(cacheKey);
35              return data;
36          })
37          .catch(error => {
38              this.pendingRequests.delete(cacheKey);
39              throw error;
40          });
41        
42        this.pendingRequests.set(cacheKey, requestPromise);
43        return requestPromise;
44    }
45    
46    
47    batchRequest(requests, maxBatchSize = 10) {
48        return new Promise((resolve, reject) => {
49            
50            this.batchQueue.push(...requests.map((req, index) => ({
51                ...req,
52                originalIndex: index
53            })));
54            
55            
56            if (this.batchQueue.length >= maxBatchSize) {
57                this.flushBatchQueue(resolve, reject);
58            } else {
59                
60                clearTimeout(this.batchTimer);
61                this.batchTimer = setTimeout(() => {
62                    this.flushBatchQueue(resolve, reject);
63                }, 50);
64            }
65        });
66    }
67    
68    flushBatchQueue(resolve, reject) {
69        if (this.batchQueue.length === 0) return;
70        
71        const batch = this.batchQueue.splice(0, 10);
72        const urls = batch.map(req => req.url);
73        
74        fetch('/api/batch', {
75            method: 'POST',
76            headers: {
77                'Content-Type': 'application/json'
78            },
79            body: JSON.stringify({ urls })
80        })
81        .then(response => response.json())
82        .then(results => {
83            const resultMap = {};
84            results.forEach((result, index) => {
85                resultMap[batch[index].originalIndex] = result;
86            });
87            resolve(resultMap);
88        })
89        .catch(reject);
90    }
91}
92
93
94const requestManager = new RequestManager();
95
96
97const userIds = [1, 2, 3, 4, 5];
98const requests = userIds.map(id => ({
99    url: `/api/users/${id}`,
100    method: 'GET'
101}));
102
103requestManager.batchRequest(requests).then(results => {
104    console.log('Batch results:', results);
105});

连接池管理

对于需要频繁请求的场景,连接池能有效提升性能。

 1class ConnectionPool {
 2    constructor(maxConnections = 6) {
 3        this.maxConnections = maxConnections;
 4        this.connections = [];
 5        this.pendingRequests = [];
 6        this.activeConnections = 0;
 7    }
 8    
 9    async request(url, options = {}) {
10        return new Promise((resolve, reject) => {
11            this.pendingRequests.push({
12                url,
13                options,
14                resolve,
15                reject
16            });
17            this.processQueue();
18        });
19    }
20    
21    processQueue() {
22        if (this.pendingRequests.length === 0) return;
23        if (this.activeConnections >= this.maxConnections) return;
24        
25        const request = this.pendingRequests.shift();
26        this.activeConnections++;
27        
28        fetch(request.url, request.options)
29            .then(response => {
30                this.activeConnections--;
31                request.resolve(response);
32                this.processQueue(); 
33            })
34            .catch(error => {
35                this.activeConnections--;
36                request.reject(error);
37                this.processQueue();
38            });
39    }
40}

交互响应优化

Web Workers的应用

对于计算密集型任务,使用Web Workers避免阻塞主线程。

 1class WorkerManager {
 2    constructor() {
 3        this.workers = new Map();
 4    }
 5    
 6    createWorker(name, workerScript) {
 7        const worker = new Worker(workerScript);
 8        this.workers.set(name, worker);
 9        return worker;
10    }
11    
12    async runTask(workerName, data) {
13        const worker = this.workers.get(workerName);
14        if (!worker) {
15            throw new Error(`Worker ${workerName} not found`);
16        }
17        
18        return new Promise((resolve, reject) => {
19            const messageId = Date.now() + Math.random();
20            
21            const handleMessage = (event) => {
22                if (event.data.messageId === messageId) {
23                    worker.removeEventListener('message', handleMessage);
24                    if (event.data.error) {
25                        reject(new Error(event.data.error));
26                    } else {
27                        resolve(event.data.result);
28                    }
29                }
30            };
31            
32            worker.addEventListener('message', handleMessage);
33            
34            worker.postMessage({
35                messageId,
36                data
37            });
38        });
39    }
40}
41
42
43self.addEventListener('message', async (event) => {
44    const { messageId, data } = event.data;
45    
46    try {
47        
48        const result = await performHeavyComputation(data);
49        self.postMessage({
50            messageId,
51            result
52        });
53    } catch (error) {
54        self.postMessage({
55            messageId,
56            error: error.message
57        });
58    }
59});
60
61function performHeavyComputation(data) {
62    
63    let result = 0;
64    for (let i = 0; i < data.iterations; i++) {
65        result += Math.sin(i) * Math.cos(i);
66    }
67    return result;
68}
69
70
71const workerManager = new WorkerManager();
72const computationWorker = workerManager.createWorker(
73    'computation', 
74    'heavy-computation-worker.js'
75);
76
77
78async function handleComplexCalculation(iterations) {
79    try {
80        showLoading(true);
81        const result = await workerManager.runTask('computation', {
82            iterations
83        });
84        updateResult(result);
85    } catch (error) {
86        console.error('Calculation failed:', error);
87    } finally {
88        showLoading(false);
89    }
90}

内存泄漏检测和处理

内存泄漏是性能优化中的隐形杀手。

 1class MemoryMonitor {
 2    constructor() {
 3        this.observers = new Set();
 4        this.timers = new Set();
 5        this.intervals = new Set();
 6        this.eventListeners = new Map();
 7        this.startMonitoring();
 8    }
 9    
10    
11    startMonitoring() {
12        if (performance.memory) {
13            setInterval(() => {
14                const memory = performance.memory;
15                const usage = {
16                    used: Math.round(memory.usedJSHeapSize / 1048576),
17                    total: Math.round(memory.totalJSHeapSize / 1048576),
18                    limit: Math.round(memory.jsHeapSizeLimit / 1048576)
19                };
20                
21                if (usage.used > usage.total * 0.8) {
22                    console.warn('High memory usage detected:', usage);
23                    this.notifyObservers('high_memory', usage);
24                }
25            }, 5000);
26        }
27    }
28    
29    
30    addObserver(callback) {
31        this.observers.add(callback);
32    }
33    
34    
35    notifyObservers(type, data) {
36        this.observers.forEach(callback => {
37            try {
38                callback(type, data);
39            } catch (error) {
40                console.error('Observer callback error:', error);
41            }
42        });
43    }
44    
45    
46    setTimeout(callback, delay, ...args) {
47        const timerId = setTimeout(() => {
48            this.timers.delete(timerId);
49            callback(...args);
50        }, delay);
51        
52        this.timers.add(timerId);
53        return timerId;
54    }
55    
56    
57    setInterval(callback, interval, ...args) {
58        const intervalId = setInterval(callback, interval, ...args);
59        this.intervals.add(intervalId);
60        return intervalId;
61    }
62    
63    
64    addEventListener(element, event, callback, options) {
65        element.addEventListener(event, callback, options);
66        
67        const key = `${element.constructor.name}_${event}`;
68        if (!this.eventListeners.has(key)) {
69            this.eventListeners.set(key, new Set());
70        }
71        this.eventListeners.get(key).add({
72            element,
73            event,
74            callback,
75            options
76        });
77    }
78    
79    
80    cleanup() {
81        
82        this.timers.forEach(timerId => clearTimeout(timerId));
83        this.timers.clear();
84        
85        
86        this.intervals.forEach(intervalId => clearInterval(intervalId));
87        this.intervals.clear();
88        
89        
90        this.eventListeners.forEach(listeners => {
91            listeners.forEach(({ element, event, callback, options }) => {
92                element.removeEventListener(event, callback, options);
93            });
94        });
95        this.eventListeners.clear();
96        
97        
98        this.observers.clear();
99    }
100}
101
102
103const memoryMonitor = new MemoryMonitor();
104
105
106class Component {
107    constructor() {
108        this.memoryMonitor = memoryMonitor;
109        this.setupEventListeners();
110    }
111    
112    setupEventListeners() {
113        this.memoryMonitor.addEventListener(
114            document, 
115            'click', 
116            this.handleClick.bind(this)
117        );
118    }
119    
120    handleClick(event) {
121        
122    }
123    
124    destroy() {
125        
126        this.memoryMonitor.cleanup();
127    }
128}

性能监控和分析

前端性能监控系统

建立完善的性能监控系统是持续优化的基础。

 1class PerformanceMonitor {
 2    constructor() {
 3        this.metrics = {
 4            pageLoadTime: 0,
 5            domContentLoaded: 0,
 6            firstPaint: 0,
 7            firstContentfulPaint: 0,
 8            largestContentfulPaint: 0,
 9            firstInputDelay: 0,
10            cumulativeLayoutShift: 0
11        };
12        this.init();
13    }
14    
15    init() {
16        
17        if (performance.timing) {
18            this.measurePageLoadTime();
19        }
20        
21        
22        if (typeof PerformancePaintTiming !== 'undefined') {
23            this.measurePaintTimings();
24        }
25        
26        
27        this.measureFirstInputDelay();
28        
29        
30        this.measureLayoutShift();
31        
32        
33        window.addEventListener('beforeunload', () => {
34            this.reportMetrics();
35        });
36    }
37    
38    measurePageLoadTime() {
39        const timing = performance.timing;
40        this.metrics.pageLoadTime = timing.loadEventEnd - timing.navigationStart;
41        this.metrics.domContentLoaded = timing.domContentLoadedEventEnd - timing.navigationStart;
42    }
43    
44    measurePaintTimings() {
45        performance.getEntriesByType('paint').forEach(entry => {
46            if (entry.name === 'first-paint') {
47                this.metrics.firstPaint = entry.startTime;
48            } else if (entry.name === 'first-contentful-paint') {
49                this.metrics.firstContentfulPaint = entry.startTime;
50            }
51        });
52    }
53    
54    measureFirstInputDelay() {
55        const observer = new PerformanceObserver((list) => {
56            list.getEntries().forEach(entry => {
57                if (entry.entryType === 'first-input') {
58                    this.metrics.firstInputDelay = entry.processingStart - entry.startTime;
59                }
60            });
61        });
62        
63        observer.observe({ entryTypes: ['first-input'] });
64    }
65    
66    measureLayoutShift() {
67        let cls = 0;
68        const observer = new PerformanceObserver((list) => {
69            list.getEntries().forEach(entry => {
70                if (!entry.hadRecentInput) {
71                    cls += entry.value;
72                }
73            });
74            this.metrics.cumulativeLayoutShift = cls;
75        });
76        
77        observer.observe({ entryTypes: ['layout-shift'] });
78    }
79    
80    
81    measureCustomMetric(name, startTime, endTime) {
82        const duration = endTime - startTime;
83        console.log(`Custom metric ${name}: ${duration}ms`);
84        
85    }
86    
87    
88    reportMetrics() {
89        
90        fetch('/api/performance', {
91            method: 'POST',
92            headers: {
93                'Content-Type': 'application/json'
94            },
95            body: JSON.stringify({
96                url: window.location.href,
97                userAgent: navigator.userAgent,
98                metrics: this.metrics,
99                timestamp: Date.now()
100            })
101        }).catch(error => {
102            console.error('Failed to report performance metrics:', error);
103        });
104    }
105}
106
107
108const perfMonitor = new PerformanceMonitor();
109
110
111const startTime = performance.now();
112
113const endTime = performance.now();
114perfMonitor.measureCustomMetric('custom_operation', startTime, endTime);

结语:性能优化是一场持久战

前端性能优化不是一蹴而就的事情,而是一场需要持续投入的持久战。从最初的资源压缩,到现在的全链路优化,每一个细节都可能影响用户体验。

在这个过程中,我深刻体会到几个要点:

  1. 数据驱动:优化必须基于真实的数据,而不是主观臆断
  2. 用户视角:始终从用户的角度思考问题,而不是从技术的角度
  3. 持续改进:性能优化是一个持续的过程,需要不断地监控和改进
  4. 平衡取舍:在性能和功能之间找到平衡点,避免过度优化
个人笔记记录 2021 ~ 2025