性能优化的核心指标
在开始优化之前,我们先要明确几个核心指标:
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);
结语:性能优化是一场持久战
前端性能优化不是一蹴而就的事情,而是一场需要持续投入的持久战。从最初的资源压缩,到现在的全链路优化,每一个细节都可能影响用户体验。
在这个过程中,我深刻体会到几个要点:
- 数据驱动:优化必须基于真实的数据,而不是主观臆断
- 用户视角:始终从用户的角度思考问题,而不是从技术的角度
- 持续改进:性能优化是一个持续的过程,需要不断地监控和改进
- 平衡取舍:在性能和功能之间找到平衡点,避免过度优化
个人笔记记录 2021 ~ 2025