前端性能优化踩坑记录
最近负责的项目用户反馈页面加载慢,老板催得紧,只能硬着头皮开始性能优化。从完全不懂到勉强能用,踩了不少坑,这里记录一下过程和解决方案。
性能问题排查
Core Web Vitals 监控
老实说,一开始完全不知道怎么排查性能问题。网上搜了一圈,发现Google有个Core Web Vitals的东西,据说很重要。简单来说就是三个指标:
- LCP (Largest Contentful Paint):最大内容绘制,页面主要内容加载完的时间
- FID (First Input Delay):首次输入延迟,用户点击到页面响应的时间
- CLS (Cumulative Layout Shift):累积布局偏移,页面元素乱跳的程度
刚开始看这些英文缩写一头雾水,后来发现浏览器有现成的API可以监控:
// Web Vitals 监控类
class WebVitalsMonitor {
constructor() {
this.metrics = {};
this.initObservers();
}
initObservers() {
this.observeLCP();
this.observeFID();
this.observeCLS();
}
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = {
value: lastEntry.startTime,
rating: this.getRating('lcp', lastEntry.startTime)
};
console.log('LCP:', lastEntry.startTime);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
}
observeFID() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
this.metrics.fid = {
value: entry.processingStart - entry.startTime,
rating: this.getRating('fid', entry.processingStart - entry.startTime)
};
console.log('FID:', entry.processingStart - entry.startTime);
});
});
observer.observe({ entryTypes: ['first-input'] });
}
observeCLS() {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.cls = {
value: clsValue,
rating: this.getRating('cls', clsValue)
};
console.log('CLS:', clsValue);
});
observer.observe({ entryTypes: ['layout-shift'] });
}
getRating(metric, value) {
const thresholds = {
lcp: { good: 2500, needsImprovement: 4000 },
fid: { good: 100, needsImprovement: 300 },
cls: { good: 0.1, needsImprovement: 0.25 }
};
const threshold = thresholds[metric];
if (value <= threshold.good) return 'good';
if (value <= threshold.needsImprovement) return 'needs-improvement';
return 'poor';
}
getMetrics() {
return this.metrics;
}
}
// 初始化监控
const monitor = new WebVitalsMonitor();跑了一下发现我们的LCP竟然有4秒多,难怪用户说慢。接下来就是找原因了。
长任务监控
发现一个很坑的事情,我们的页面有很多"长任务"(超过50ms的JavaScript执行),这些会卡住页面响应。写了个监控来抓这些任务:
class LongTaskMonitor {
constructor() {
this.longTasks = [];
this.init();
}
init() {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
const task = {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution || []
};
this.longTasks.push(task);
this.reportLongTask(task);
});
});
observer.observe({ entryTypes: ['longtask'] });
}
}
reportLongTask(task) {
console.warn(`长任务检测: ${task.duration.toFixed(2)}ms`, task);
this.sendToAnalytics({
type: 'long-task',
duration: task.duration,
attribution: task.attribution.map(attr => ({
name: attr.name,
containerType: attr.containerType,
containerSrc: attr.containerSrc
}))
});
}
getLongTasks() {
return this.longTasks;
}
sendToAnalytics(data) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics', JSON.stringify(data));
}
}
}资源加载优化
资源预加载策略
刚开始不懂,看网上说预加载好,就把所有东西都预加载了,结果更慢了。后来才知道要分轻重缓急:
class ResourceOptimizer {
constructor() {
this.criticalResources = new Set();
this.preloadQueue = [];
}
// 预加载关键资源
preloadCriticalResources() {
const criticalUrls = [
'/fonts/main.woff2',
'/css/critical.css',
'/js/core.js'
];
criticalUrls.forEach(url => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = url;
if (url.endsWith('.woff2')) {
link.as = 'font';
link.type = 'font/woff2';
link.crossOrigin = 'anonymous';
} else if (url.endsWith('.css')) {
link.as = 'style';
} else if (url.endsWith('.js')) {
link.as = 'script';
}
document.head.appendChild(link);
this.criticalResources.add(url);
});
}
// 动态加载非关键资源
async loadNonCriticalResource(url, type = 'script') {
return new Promise((resolve, reject) => {
let element;
if (type === 'script') {
element = document.createElement('script');
element.src = url;
element.async = true;
} else if (type === 'style') {
element = document.createElement('link');
element.rel = 'stylesheet';
element.href = url;
}
element.onload = () => resolve(element);
element.onerror = () => reject(new Error(`Failed to load ${url}`));
document.head.appendChild(element);
});
}
// 懒加载模块
async lazyLoadModule(modulePath) {
try {
const module = await import(modulePath);
return module;
} catch (error) {
console.error(`Failed to lazy load module: ${modulePath}`, error);
throw error;
}
}
}
// 使用示例
const optimizer = new ResourceOptimizer();
optimizer.preloadCriticalResources();
// 懒加载示例
document.getElementById('load-chart')?.addEventListener('click', async () => {
try {
const { Chart } = await optimizer.lazyLoadModule('./chart.js');
new Chart(document.getElementById('chart-container'));
} catch (error) {
console.error('图表加载失败:', error);
}
});图片优化实践
我们网站图片特别多,而且都很大。一开始想着用懒加载就行了,结果发现还有很多坑要踩:
class ImageOptimizer {
constructor() {
this.observer = null;
this.imageQueue = new Map();
this.init();
}
init() {
// 使用 Intersection Observer 实现懒加载
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
rootMargin: '50px 0px',
threshold: 0.01
}
);
this.observeImages();
}
observeImages() {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => this.observer.observe(img));
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}
async loadImage(img) {
const src = img.dataset.src;
if (!src) return;
try {
// 预加载图片
const image = new Image();
image.src = this.getOptimizedImageUrl(src, img);
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
});
// 平滑过渡
img.style.transition = 'opacity 0.3s';
img.style.opacity = '0';
img.src = image.src;
img.removeAttribute('data-src');
img.onload = () => {
img.style.opacity = '1';
};
} catch (error) {
console.error('图片加载失败:', src, error);
// 显示占位图
img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPkltYWdlIEVycm9yPC90ZXh0Pjwvc3ZnPg==';
}
}
getOptimizedImageUrl(src, img) {
const devicePixelRatio = window.devicePixelRatio || 1;
const containerWidth = img.clientWidth || img.offsetWidth;
const optimalWidth = Math.ceil(containerWidth * devicePixelRatio);
// 如果使用 CDN,可以动态调整尺寸
if (src.includes('example-cdn.com')) {
return `${src}?w=${optimalWidth}&f=webp&q=80`;
}
return src;
}
// 预加载关键图片
preloadCriticalImages(urls) {
urls.forEach(url => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = url;
document.head.appendChild(link);
});
}
}
// 使用示例
const imageOptimizer = new ImageOptimizer();
// 预加载首屏关键图片
imageOptimizer.preloadCriticalImages([
'/images/hero-banner.webp',
'/images/logo.svg'
]);JavaScript 性能优化
防抖节流优化
我们页面有个搜索框,用户一输入就去搜索,结果每打一个字就发一次请求,服务器都要被干崩了。后来学会了防抖节流:
class PerformanceUtils {
// 防抖:等用户停止输入再执行
static debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
}
// 节流:固定频率执行
static throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 空闲时执行
static idle(func, options = {}) {
if ('requestIdleCallback' in window) {
return requestIdleCallback(func, options);
} else {
return setTimeout(func, 1);
}
}
// 动画帧优化
static rafThrottle(func) {
let isRunning = false;
return function(...args) {
if (isRunning) return;
isRunning = true;
requestAnimationFrame(() => {
func.apply(this, args);
isRunning = false;
});
};
}
}
// 使用示例
const searchInput = document.getElementById('search');
// 搜索防抖
const debouncedSearch = PerformanceUtils.debounce((query) => {
console.log('搜索:', query);
// 执行搜索逻辑
}, 300);
searchInput?.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// 滚动节流
const throttledScroll = PerformanceUtils.throttle(() => {
console.log('滚动位置:', window.scrollY);
// 执行滚动处理逻辑
}, 100);
window.addEventListener('scroll', throttledScroll);
// RAF 优化的滚动动画
const rafScrollHandler = PerformanceUtils.rafThrottle(() => {
const scrollProgress = window.scrollY / (document.body.scrollHeight - window.innerHeight);
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = `${scrollProgress * 100}%`;
}
});
window.addEventListener('scroll', rafScrollHandler);内存管理优化
class MemoryManager {
constructor() {
this.observers = new Set();
this.timers = new Set();
this.eventListeners = new Map();
}
// 安全的事件监听器添加
addEventListener(element, event, handler, options) {
const key = `${element.constructor.name}-${event}`;
if (!this.eventListeners.has(key)) {
this.eventListeners.set(key, new Set());
}
this.eventListeners.get(key).add({ element, handler, options });
element.addEventListener(event, handler, options);
}
// 清理事件监听器
removeEventListener(element, event, handler) {
const key = `${element.constructor.name}-${event}`;
const listeners = this.eventListeners.get(key);
if (listeners) {
listeners.forEach(listener => {
if (listener.element === element && listener.handler === handler) {
element.removeEventListener(event, handler);
listeners.delete(listener);
}
});
}
}
// 安全的定时器管理
setTimeout(callback, delay) {
const timerId = setTimeout(() => {
callback();
this.timers.delete(timerId);
}, delay);
this.timers.add(timerId);
return timerId;
}
setInterval(callback, interval) {
const intervalId = setInterval(callback, interval);
this.timers.add(intervalId);
return intervalId;
}
clearTimer(timerId) {
clearTimeout(timerId);
clearInterval(timerId);
this.timers.delete(timerId);
}
// Observer 管理
addObserver(observer) {
this.observers.add(observer);
return observer;
}
// 内存使用监控
monitorMemoryUsage() {
if ('memory' in performance) {
const memory = performance.memory;
console.log({
used: `${(memory.usedJSHeapSize / 1048576).toFixed(2)} MB`,
total: `${(memory.totalJSHeapSize / 1048576).toFixed(2)} MB`,
limit: `${(memory.jsHeapSizeLimit / 1048576).toFixed(2)} MB`
});
}
}
// 清理所有资源
cleanup() {
// 清理事件监听器
this.eventListeners.forEach((listeners, key) => {
listeners.forEach(({ element, handler }) => {
element.removeEventListener(key.split('-')[1], handler);
});
});
this.eventListeners.clear();
// 清理定时器
this.timers.forEach(timerId => {
this.clearTimer(timerId);
});
this.timers.clear();
// 清理 Observers
this.observers.forEach(observer => {
if (observer.disconnect) {
observer.disconnect();
}
});
this.observers.clear();
}
}
// 使用示例
const memoryManager = new MemoryManager();
// 页面卸载时清理资源
window.addEventListener('beforeunload', () => {
memoryManager.cleanup();
});
// 定期监控内存使用
setInterval(() => {
memoryManager.monitorMemoryUsage();
}, 30000);CSS 性能优化
关键 CSS 内联
/* 关键 CSS - 内联在 HTML 中 */
.critical-above-fold {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
color: #333;
}
.hero-section {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* 使用 CSS 变量减少重复 */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--border-radius: 0.375rem;
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
/* 优化选择器性能 - 避免过深嵌套 */
.card {
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
}
/* 使用 transform 和 opacity 做动画 */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 避免重排重绘的属性 */
.optimized-animation {
will-change: transform, opacity;
}
.optimized-animation:hover {
transform: translateY(-2px) scale(1.02);
}异步 CSS 加载
// 异步加载非关键 CSS
function loadCSS(href, before, media) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.media = media || 'all';
if (before) {
before.parentNode.insertBefore(link, before);
} else {
document.head.appendChild(link);
}
return link;
}
// 预加载 CSS
function preloadCSS(href) {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = href;
link.onload = function() {
this.onload = null;
this.rel = 'stylesheet';
};
document.head.appendChild(link);
}
// 使用示例
// 异步加载非关键样式
loadCSS('/css/components.css');
loadCSS('/css/utilities.css');
// 预加载下一页可能需要的样式
preloadCSS('/css/product.css');性能监控与测量
性能报告生成
class PerformanceReporter {
constructor() {
this.metrics = {};
this.startTime = performance.now();
}
// 收集所有性能指标
async collectMetrics() {
// Navigation Timing
const navigation = performance.getEntriesByType('navigation')[0];
this.metrics.navigation = {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.navigationStart,
loadComplete: navigation.loadEventEnd - navigation.navigationStart,
domInteractive: navigation.domInteractive - navigation.navigationStart,
firstByte: navigation.responseStart - navigation.navigationStart
};
// Resource Timing
const resources = performance.getEntriesByType('resource');
this.metrics.resources = {
count: resources.length,
totalSize: resources.reduce((size, resource) => {
return size + (resource.transferSize || 0);
}, 0),
slowestResource: resources.reduce((slowest, resource) => {
return resource.duration > (slowest?.duration || 0) ? resource : slowest;
}, null)
};
// Web Vitals
this.metrics.webVitals = await this.getWebVitals();
return this.metrics;
}
async getWebVitals() {
return new Promise((resolve) => {
const vitals = {};
let collected = 0;
const total = 3;
const checkComplete = () => {
collected++;
if (collected >= total) {
resolve(vitals);
}
};
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
vitals.lcp = entries[entries.length - 1]?.startTime;
checkComplete();
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID
new PerformanceObserver((list) => {
const entries = list.getEntries();
vitals.fid = entries[0]?.processingStart - entries[0]?.startTime;
checkComplete();
}).observe({ entryTypes: ['first-input'] });
// CLS
let clsValue = 0;
new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
vitals.cls = clsValue;
checkComplete();
}).observe({ entryTypes: ['layout-shift'] });
// 超时处理
setTimeout(() => resolve(vitals), 5000);
});
}
// 生成报告
generateReport() {
const report = {
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
metrics: this.metrics,
recommendations: this.generateRecommendations()
};
console.table(this.metrics.navigation);
console.log('性能报告:', report);
return report;
}
generateRecommendations() {
const recommendations = [];
const nav = this.metrics.navigation;
if (nav?.firstByte > 500) {
recommendations.push('服务器响应时间过长,考虑使用 CDN 或优化后端性能');
}
if (nav?.domContentLoaded > 3000) {
recommendations.push('DOM 解析时间过长,考虑减少阻塞资源或使用代码分割');
}
if (this.metrics.resources?.totalSize > 2 * 1024 * 1024) {
recommendations.push('资源总大小过大,考虑压缩图片和启用 gzip');
}
return recommendations;
}
// 发送报告到分析服务
sendReport(endpoint = '/api/performance') {
const report = this.generateReport();
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, JSON.stringify(report));
} else {
fetch(endpoint, {
method: 'POST',
body: JSON.stringify(report),
headers: {
'Content-Type': 'application/json'
}
}).catch(console.error);
}
}
}
// 使用示例
const reporter = new PerformanceReporter();
// 页面加载完成后收集指标
window.addEventListener('load', async () => {
await reporter.collectMetrics();
reporter.sendReport();
});
// 页面卸载时发送最终报告
window.addEventListener('beforeunload', () => {
reporter.sendReport();
});总结与经验
这次性能优化折腾了差不多一个月,从 LCP 4秒多优化到1.5秒,效果还是很明显的。
主要经验
- 先测量后优化 - 不要凭感觉,数据说话
- 抓大放小 - 先解决影响最大的问题
- 循序渐进 - 一次改一个地方,出问题好排查
- 持续监控 - 上线后要持续关注性能数据
踩坑总结
- 预加载策略:不是越多越好,关键资源优先
- 图片优化:收益很大,但坑也多,需要综合考虑懒加载、格式、尺寸等
- JavaScript 性能:问题往往是最隐蔽的,长任务监控很重要
- CSS 动画:尽量用
transform和opacity,避免重排重绘
最后的话
希望这些踩坑经验能帮到遇到类似问题的同学。性能优化确实是个技术活,但掌握了套路后还是很有成就感的。
记住:性能优化是一个持续的过程,不是一蹴而就的。要根据实际业务场景和用户需求,选择最合适的优化策略。
Last updated on