每天一个高级前端知识 - Day 16
今日主题:性能监控与优化 - 从用户体验到Core Web Vitals的完整实践
核心概念:性能不是优化的结果,而是监控的起点
无法衡量的性能无法优化。建立完整的性能监控体系,是持续优化用户体验的基础。
📊 Core Web Vitals 深度解析
Core Web Vitals (Google排名因子)
├── LCP (Largest Contentful Paint) - 加载性能
│ └── 目标: ≤ 2.5秒
├── INP (Interaction to Next Paint) - 交互响应
│ └── 目标: ≤ 200毫秒 (替代FID)
└── CLS (Cumulative Layout Shift) - 视觉稳定
└── 目标: ≤ 0.1
🔍 完整性能监控系统
// 1. 性能监控类
class PerformanceMonitor {
constructor(config = {}) {
this.config = {
sampleRate: 0.1, // 10%采样
endpoint: '/api/performance',
appId: 'your-app-id',
...config
};
this.metrics = [];
this.observers = [];
}
// 初始化所有监控
init() {
// 采样控制
if (Math.random() > this.config.sampleRate) return;
this.observeLCP();
this.observeINP();
this.observeCLS();
this.observeFCP();
this.observeTTFB();
this.observeLongTasks();
this.observeMemory();
this.observeNetwork();
this.observeResourceTiming();
// 页面关闭时上报
window.addEventListener('beforeunload', () => this.flush());
window.addEventListener('pagehide', () => this.flush());
// 定期上报
setInterval(() => this.flush(), 30000);
}
// LCP监控
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.push({
name: 'LCP',
value: lastEntry.renderTime || lastEntry.loadTime,
rating: this.getRating('LCP', lastEntry.renderTime || lastEntry.loadTime),
url: lastEntry.url,
element: lastEntry.element?.tagName,
timestamp: Date.now()
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
this.observers.push(observer);
}
// INP监控 (Interaction to Next Paint)
observeINP() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId) {
this.metrics.push({
name: 'INP',
value: entry.processingEnd - entry.startTime,
rating: this.getRating('INP', entry.processingEnd - entry.startTime),
interactionType: entry.name,
target: entry.target?.id || entry.target?.className,
timestamp: Date.now()
});
}
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
this.observers.push(observer);
}
// CLS监控
observeCLS() {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
this.metrics.push({
name: 'CLS',
value: clsValue,
rating: this.getRating('CLS', clsValue),
sources: entry.sources?.map(s => ({
node: s.node?.tagName,
previousRect: s.previousRect,
currentRect: s.currentRect
})),
timestamp: Date.now()
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
this.observers.push(observer);
}
// FCP监控 (First Contentful Paint)
observeFCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
if (fcp) {
this.metrics.push({
name: 'FCP',
value: fcp.startTime,
rating: this.getRating('FCP', fcp.startTime),
timestamp: Date.now()
});
}
});
observer.observe({ type: 'paint', buffered: true });
this.observers.push(observer);
}
// TTFB监控 (Time To First Byte)
observeTTFB() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const navigation = entries.find(e => e.entryType === 'navigation');
if (navigation) {
this.metrics.push({
name: 'TTFB',
value: navigation.responseStart - navigation.requestStart,
rating: this.getRating('TTFB', navigation.responseStart - navigation.requestStart),
timestamp: Date.now()
});
}
});
observer.observe({ type: 'navigation', buffered: true });
this.observers.push(observer);
}
// 长任务监控
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.metrics.push({
name: 'LongTask',
value: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution?.[0]?.containerType,
scriptUrl: entry.attribution?.[0]?.scriptUrl,
timestamp: Date.now()
});
// 发送告警
this.alertLongTask(entry);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
this.observers.push(observer);
}
// 内存监控
observeMemory() {
if (performance.memory) {
setInterval(() => {
const memory = performance.memory;
this.metrics.push({
name: 'Memory',
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
usage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100,
timestamp: Date.now()
});
// 内存告警
if (memory.usedJSHeapSize / memory.jsHeapSizeLimit > 0.8) {
this.alertHighMemory();
}
}, 60000);
}
}
// 网络监控
observeNetwork() {
if ('connection' in navigator) {
const connection = navigator.connection;
const updateNetworkInfo = () => {
this.metrics.push({
name: 'NetworkInfo',
effectiveType: connection.effectiveType,
rtt: connection.rtt,
downlink: connection.downlink,
saveData: connection.saveData,
timestamp: Date.now()
});
};
updateNetworkInfo();
connection.addEventListener('change', updateNetworkInfo);
}
}
// 资源加载监控
observeResourceTiming() {
const observer = new PerformanceObserver((list) => {
const slowResources = list.getEntries().filter(entry =>
entry.duration > 1000 || entry.transferSize > 1000000
);
for (const resource of slowResources) {
this.metrics.push({
name: 'SlowResource',
url: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: resource.initiatorType,
timestamp: Date.now()
});
}
});
observer.observe({ type: 'resource', buffered: true });
this.observers.push(observer);
}
// 自定义指标
markCustomMetric(name, value) {
performance.mark(`${name}-start`);
// 执行操作
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measure = performance.getEntriesByName(name)[0];
this.metrics.push({
name: `Custom_${name}`,
value: measure.duration,
customValue: value,
timestamp: Date.now()
});
}
// 评分规则
getRating(metric, value) {
const thresholds = {
LCP: { good: 2500, needImprovement: 4000 },
INP: { good: 200, needImprovement: 500 },
CLS: { good: 0.1, needImprovement: 0.25 },
FCP: { good: 1800, needImprovement: 3000 },
TTFB: { good: 800, needImprovement: 1800 }
};
const threshold = thresholds[metric];
if (!threshold) return 'unknown';
if (value <= threshold.good) return 'good';
if (value <= threshold.needImprovement) return 'needs-improvement';
return 'poor';
}
// 性能评分
calculatePerformanceScore() {
const metrics = this.metrics.filter(m =>
['LCP', 'INP', 'CLS', 'FCP', 'TTFB'].includes(m.name)
);
const latest = {};
for (const metric of metrics) {
if (!latest[metric.name] || metric.timestamp > latest[metric.name].timestamp) {
latest[metric.name] = metric;
}
}
const weights = { LCP: 0.25, INP: 0.25, CLS: 0.15, FCP: 0.2, TTFB: 0.15 };
let score = 0;
for (const [name, weight] of Object.entries(weights)) {
const metric = latest[name];
if (metric) {
const ratingScore = { good: 1, 'needs-improvement': 0.5, poor: 0 };
score += ratingScore[metric.rating] * weight;
}
}
return Math.round(score * 100);
}
// 告警
alertLongTask(task) {
if (task.duration > 200) {
this.sendAlert({
type: 'LONG_TASK',
severity: 'high',
message: `检测到长任务 ${task.duration}ms`,
data: task
});
}
}
alertHighMemory() {
this.sendAlert({
type: 'HIGH_MEMORY',
severity: 'medium',
message: '内存使用超过80%',
data: performance.memory
});
}
sendAlert(alert) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/alerts', JSON.stringify(alert));
}
}
// 上报数据
flush() {
if (this.metrics.length === 0) return;
const payload = {
appId: this.config.appId,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
metrics: this.metrics,
score: this.calculatePerformanceScore()
};
// 使用sendBeacon确保页面关闭时也能发送
if (navigator.sendBeacon) {
navigator.sendBeacon(this.config.endpoint, JSON.stringify(payload));
} else {
fetch(this.config.endpoint, {
method: 'POST',
body: JSON.stringify(payload),
keepalive: true
});
}
this.metrics = [];
}
// 断开所有观察者
disconnect() {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
}
// 使用
const monitor = new PerformanceMonitor({
sampleRate: 0.5,
endpoint: 'https://analytics.company.com/api/vitals',
appId: 'production-web'
});
monitor.init();
// 自定义业务指标
monitor.markCustomMetric('search-response', searchTime);
monitor.markCustomMetric('cart-load', cartLoadTime);
🚀 性能优化实战
// 1. 图片优化策略
class ImageOptimizer {
// 响应式图片
static getResponsiveSrc(src, width) {
return `${src}?w=${width}&q=80&format=auto`;
}
// 懒加载
static lazyLoad() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.srcset = img.dataset.srcset;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
}
// WebP支持检测
static async supportsWebP() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
const data = ctx.getImageData(0, 0, 1, 1);
return canvas.toDataURL('image/webp').indexOf('image/webp') === 5;
}
// 使用现代格式
static async getOptimalFormat() {
const supports = {
webp: await this.supportsWebP(),
avif: await this.supportsAVIF()
};
if (supports.avif) return 'avif';
if (supports.webp) return 'webp';
return 'jpeg';
}
}
// 2. 代码分割与动态导入
const routes = {
dashboard: () => import(/* webpackChunkName: "dashboard" */ './routes/Dashboard'),
profile: () => import(/* webpackChunkName: "profile" */ './routes/Profile'),
settings: () => import(/* webpackChunkName: "settings" */ './routes/Settings')
};
// 预加载策略
function preloadRoute(routeName) {
if (requestIdleCallback) {
requestIdleCallback(() => {
routes[routeName]();
});
}
}
// 3. 资源提示
function addResourceHints() {
// DNS预解析
const dnsPrefetch = document.createElement('link');
dnsPrefetch.rel = 'dns-prefetch';
dnsPrefetch.href = 'https://api.example.com';
document.head.appendChild(dnsPrefetch);
// 预连接
const preconnect = document.createElement('link');
preconnect.rel = 'preconnect';
preconnect.href = 'https://cdn.example.com';
preconnect.crossOrigin = 'anonymous';
document.head.appendChild(preconnect);
// 预加载关键资源
const preload = document.createElement('link');
preload.rel = 'preload';
preload.as = 'font';
preload.href = '/fonts/main.woff2';
preload.crossOrigin = 'anonymous';
document.head.appendChild(preload);
}
// 4. 虚拟滚动(处理大数据列表)
class VirtualScroller {
constructor(container, options) {
this.container = container;
this.itemHeight = options.itemHeight;
this.renderItem = options.renderItem;
this.totalItems = options.totalItems;
this.bufferSize = options.bufferSize || 5;
this.scrollTop = 0;
this.startIndex = 0;
this.endIndex = 0;
this.setupDOM();
this.bindEvents();
this.render();
}
setupDOM() {
this.viewport = document.createElement('div');
this.viewport.style.height = '100%';
this.viewport.style.overflow = 'auto';
this.viewport.style.position = 'relative';
this.content = document.createElement('div');
this.content.style.position = 'absolute';
this.content.style.top = '0';
this.content.style.left = '0';
this.content.style.width = '100%';
this.container.appendChild(this.viewport);
this.viewport.appendChild(this.content);
// 设置总高度
this.viewport.style.height = `${this.container.clientHeight}px`;
this.content.style.height = `${this.totalItems * this.itemHeight}px`;
}
bindEvents() {
this.viewport.addEventListener('scroll', () => {
this.scrollTop = this.viewport.scrollTop;
this.updateVisibleRange();
});
window.addEventListener('resize', () => {
this.viewport.style.height = `${this.container.clientHeight}px`;
this.updateVisibleRange();
});
}
updateVisibleRange() {
const viewportHeight = this.viewport.clientHeight;
const start = Math.floor(this.scrollTop / this.itemHeight);
const visibleCount = Math.ceil(viewportHeight / this.itemHeight);
this.startIndex = Math.max(0, start - this.bufferSize);
this.endIndex = Math.min(
this.totalItems - 1,
start + visibleCount + this.bufferSize
);
this.render();
}
render() {
const fragment = document.createDocumentFragment();
for (let i = this.startIndex; i <= this.endIndex; i++) {
const item = this.renderItem(i);
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.left = '0';
item.style.width = '100%';
item.style.height = `${this.itemHeight}px`;
fragment.appendChild(item);
}
this.content.innerHTML = '';
this.content.appendChild(fragment);
}
}
📈 性能预算
// performance-budget.js
const budget = {
// 加载预算
loading: {
LCP: 2500,
TTFB: 800,
FCP: 1800,
totalSize: 500000, // 500KB
jsSize: 200000, // 200KB
cssSize: 50000, // 50KB
imageSize: 250000 // 250KB
},
// 交互预算
interactivity: {
INP: 200,
TTI: 5000,
FID: 100
},
// 视觉稳定
visual: {
CLS: 0.1
}
};
// 构建时检查
class PerformanceBudgetChecker {
constructor(budget) {
this.budget = budget;
}
async checkBundle() {
const stats = await this.getBundleStats();
const violations = [];
if (stats.js > this.budget.loading.jsSize) {
violations.push(`JS大小超限: ${stats.js} > ${this.budget.loading.jsSize}`);
}
if (stats.css > this.budget.loading.cssSize) {
violations.push(`CSS大小超限: ${stats.css} > ${this.budget.loading.cssSize}`);
}
if (violations.length > 0) {
console.error('性能预算违规:', violations);
if (process.env.NODE_ENV === 'production') {
process.exit(1);
}
}
return violations;
}
}
🎯 今日挑战
实现一个完整的性能监控面板,要求:
- 实时显示Core Web Vitals (LCP, INP, CLS)
- 显示资源加载瀑布图
- 显示长任务列表和堆栈
- 提供优化建议(基于Lighthouse规则)
- 导出性能报告(PDF/JSON)
- 对比多个版本的性能变化
// 使用示例
const dashboard = new PerformanceDashboard({
target: document.getElementById('dashboard'),
autoRefresh: true,
refreshInterval: 5000,
historyLength: 100
});
dashboard.start();
dashboard.on('metric-update', (metrics) => {
console.log('新指标:', metrics);
});
dashboard.exportReport('pdf');
明日预告:前端安全 - XSS、CSRF、点击劫持等攻击的防御实战
💡 性能箴言:"用户不会抱怨快,只会抱怨慢"——把性能目标设为"感觉不到等待"!