常见网站性能问题与排查
问题
网站常见的性能问题有哪些?可能的原因是什么?应该如何排查?
答案
网站性能问题主要分为加载性能、运行时性能、渲染性能和网络性能四大类。
一、加载性能问题
1.1 首屏白屏时间过长
现象:用户打开页面后长时间看到白屏
可能原因:
| 原因 | 说明 |
|---|---|
| JS 文件过大 | 单个 bundle 超过 500KB |
| JS 阻塞渲染 | 未使用 defer/async |
| CSS 阻塞渲染 | 关键 CSS 未内联 |
| 服务端响应慢 | TTFB 过高 |
| 资源未压缩 | 未启用 gzip/brotli |
| 未使用 CDN | 资源加载距离远 |
排查方法:
// 使用 Performance API 测量白屏时间
const paintEntries = performance.getEntriesByType('paint');
const fcp = paintEntries.find(entry => entry.name === 'first-contentful-paint');
console.log('FCP:', fcp?.startTime, 'ms');
// 测量 TTFB
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
const ttfb = navigation.responseStart - navigation.requestStart;
console.log('TTFB:', ttfb, 'ms');
Chrome DevTools 排查:
-
Network 面板:查看资源加载瀑布图
- 关注 TTFB(等待时间)
- 查看资源大小和加载时间
- 检查是否有阻塞请求
-
Performance 面板:
- 查看 FCP、LCP 时间点
- 分析 Main Thread 活动
-
Lighthouse:
- 运行性能审计
- 查看 "Reduce initial server response time"
- 查看 "Eliminate render-blocking resources"
1.2 资源加载失败
现象:页面部分内容无法显示,控制台报 404 或网络错误
可能原因:
- 资源路径错误
- CDN 节点故障
- 跨域问题
- 资源被拦截(广告拦截器)
排查方法:
// 监听资源加载错误
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
const target = event.target as HTMLImageElement | HTMLScriptElement;
console.error('资源加载失败:', {
tagName: target.tagName,
src: (target as HTMLImageElement).src || (target as HTMLScriptElement).src,
});
}
}, true);
// 使用 Resource Timing API
const resources = performance.getEntriesByType('resource');
const failedResources = resources.filter(
(r) => (r as PerformanceResourceTiming).transferSize === 0
);
二、运行时性能问题
2.1 页面卡顿/掉帧
现象:滚动、动画不流畅,操作有明显延迟
可能原因:
| 原因 | 说明 |
|---|---|
| 长任务阻塞 | 单个任务超过 50ms |
| 频繁重排重绘 | DOM 操作过多 |
| 事件处理过重 | scroll/resize 未节流 |
| 复杂计算在主线程 | 未使用 Web Worker |
排查方法:
// 使用 Long Tasks API 检测长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('检测到长任务:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
});
}
});
observer.observe({ entryTypes: ['longtask'] });
// 测量帧率
let lastTime = performance.now();
let frameCount = 0;
function measureFPS(): void {
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
console.log('FPS:', frameCount);
frameCount = 0;
lastTime = currentTime;
}
requestAnimationFrame(measureFPS);
}
measureFPS();
Chrome DevTools 排查:
-
Performance 面板:
- 录制页面操作
- 查看 Main Thread 火焰图
- 找出红色三角标记的长任务
- 分析 Scripting、Rendering、Painting 时间占比
-
Performance Monitor:
- 实时查看 CPU 使用率
- 监控 JS 堆内存大小
- 观察 DOM Nodes 数量
帧率标准
- 60 FPS = 每帧 16.67ms
- 低于 30 FPS 用户会感知到明显卡顿
2.2 内存泄漏
现象:页面使用时间越长越卡,内存持续增长不释放
常见原因:
// 1. 未清除的定时器
function BadComponent(): void {
setInterval(() => {
console.log('泄漏!');
}, 1000);
// 组件销毁时未清除
}
// 2. 未移除的事件监听
function addListener(): void {
window.addEventListener('resize', handleResize);
// 忘记 removeEventListener
}
// 3. 闭包持有大对象
function createClosure(): () => void {
const largeData = new Array(1000000).fill('x');
return () => {
// largeData 无法被回收
console.log(largeData.length);
};
}
// 4. DOM 引用未释放
let detachedElement: HTMLElement | null = null;
function removeElement(): void {
const el = document.getElementById('target');
if (el) {
el.remove();
detachedElement = el; // 仍然持有引用
}
}
// 5. Map/Set 存储对象未清理
const cache = new Map<object, string>();
function addToCache(obj: object): void {
cache.set(obj, 'data');
// 对象销毁后,Map 仍持有引用
}
排查方法:
-
Memory 面板 - Heap Snapshot:
- 多次快照对比
- 查看 "Objects allocated between Snapshot 1 and Snapshot 2"
- 搜索 "Detached" 查找游离 DOM
-
Memory 面板 - Allocation Timeline:
- 录制内存分配过程
- 蓝色条 = 分配,灰色条 = 已回收
- 持续蓝色条 = 潜在泄漏
-
Performance Monitor:
- 持续观察 JS Heap Size
- 如果只增不减,可能有泄漏
// 代码中检测内存
if (performance.memory) {
const memory = (performance as any).memory;
console.log({
usedJSHeapSize: (memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
totalJSHeapSize: (memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
});
}
内存泄漏排查技巧
- 在怀疑有泄漏的操作前后各拍一次快照
- 对比两次快照,按 "Retained Size" 排序
- 查找增长最大的对象类型
- 在 "Retainers" 中查看谁持有引用
三、渲染性能问题
3.1 重排重绘频繁
现象:页面滚动或交互时出现闪烁、抖动
触发重排的操作:
// 这些操作会触发重排(Reflow)
element.offsetWidth; // 读取布局属性
element.getBoundingClientRect();
element.style.width = '100px'; // 修改几何属性
element.className = 'new-class';
window.getComputedStyle(element);
排查方法:
// 强制同步布局检测
function detectForcedReflow(): void {
const originalGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = function (...args) {
console.trace('getComputedStyle 被调用');
return originalGetComputedStyle.apply(this, args);
};
}
Chrome DevTools 排查:
-
Performance 面板:
- 查看紫色的 "Layout" 事件
- 点击查看触发重排的代码位置
- 查看 "Recalculate Style" 事件
-
Rendering 面板:
- 开启 "Paint flashing"(绿色闪烁显示重绘区域)
- 开启 "Layout Shift Regions"(蓝色显示布局偏移)
优化方案:
// ❌ 错误:读写交替,触发多次重排
function badLayout(): void {
const el = document.getElementById('box')!;
el.style.width = el.offsetWidth + 10 + 'px'; // 读 -> 写
el.style.height = el.offsetHeight + 10 + 'px'; // 读 -> 写
}
// ✅ 正确:批量读,批量写
function goodLayout(): void {
const el = document.getElementById('box')!;
// 先批量读
const width = el.offsetWidth;
const height = el.offsetHeight;
// 再批量写
el.style.width = width + 10 + 'px';
el.style.height = height + 10 + 'px';
}
// ✅ 更好:使用 requestAnimationFrame
function betterLayout(): void {
const el = document.getElementById('box')!;
requestAnimationFrame(() => {
const width = el.offsetWidth;
const height = el.offsetHeight;
requestAnimationFrame(() => {
el.style.width = width + 10 + 'px';
el.style.height = height + 10 + 'px';
});
});
}
3.2 动画卡顿
现象:CSS 动画或 JS 动画不流畅
可能原因:
- 动画属性触发重排(width、height、top、left)
- 未使用 GPU 加速
- 动画帧率不稳定
排查方法:
-
Layers 面板:
- 查看哪些元素创建了独立图层
- 检查图层数量是否过多
-
Performance 面板:
- 查看动画期间的帧率
- 检查是否有 "Compositor" 以外的工作
优化方案:
/* ❌ 差:触发重排 */
.bad-animation {
animation: move-bad 1s infinite;
}
@keyframes move-bad {
from { left: 0; }
to { left: 100px; }
}
/* ✅ 好:使用 transform,只触发合成 */
.good-animation {
animation: move-good 1s infinite;
will-change: transform;
}
@keyframes move-good {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
四、网络性能问题
4.1 请求数量过多
现象:页面加载时发起大量 HTTP 请求
排查方法:
// 统计请求数量
const resourceCount = performance.getEntriesByType('resource').length;
console.log('资源请求数:', resourceCount);
// 按类型分组
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
const byType = resources.reduce((acc, r) => {
const type = r.initiatorType;
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.table(byType);
Chrome DevTools 排查:
- Network 面板:
- 查看请求总数和总大小
- 按类型筛选(JS、CSS、Img 等)
- 检查是否有重复请求
优化方案:
- 合并小文件(雪碧图、CSS/JS 合并)
- 使用 HTTP/2 多路复用
- 内联小资源(Base64 图片、内联 CSS)
- 延迟加载非首屏资源
4.2 资源体积过大
现象:单个文件下载时间过长
排查方法:
// 找出最大的资源
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
const sorted = resources
.filter(r => r.transferSize > 0)
.sort((a, b) => b.transferSize - a.transferSize)
.slice(0, 10);
console.table(sorted.map(r => ({
name: r.name.split('/').pop(),
size: (r.transferSize / 1024).toFixed(2) + ' KB',
duration: r.duration.toFixed(0) + ' ms',
})));
Chrome DevTools 排查:
- Network 面板:按 Size 排序
- Coverage 面板:查看 JS/CSS 使用率
五、核心性能指标(Web Vitals)
5.1 指标说明
| 指标 | 全称 | 含义 | 良好标准 |
|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制 | < 2.5s |
| FID | First Input Delay | 首次输入延迟 | < 100ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | < 0.1 |
| FCP | First Contentful Paint | 首次内容绘制 | < 1.8s |
| TTFB | Time to First Byte | 首字节时间 | < 800ms |
| INP | Interaction to Next Paint | 交互到下一帧 | < 200ms |
5.2 测量代码
import { onLCP, onFID, onCLS, onFCP, onTTFB, onINP } from 'web-vitals';
// 测量所有核心指标
function measureWebVitals(): void {
onLCP((metric) => {
console.log('LCP:', metric.value, metric.rating);
});
onFID((metric) => {
console.log('FID:', metric.value, metric.rating);
});
onCLS((metric) => {
console.log('CLS:', metric.value, metric.rating);
});
onFCP((metric) => {
console.log('FCP:', metric.value, metric.rating);
});
onTTFB((metric) => {
console.log('TTFB:', metric.value, metric.rating);
});
onINP((metric) => {
console.log('INP:', metric.value, metric.rating);
});
}
measureWebVitals();
5.3 常见问题与优化
六、性能排查工具汇总
6.1 Chrome DevTools
| 面板 | 用途 |
|---|---|
| Network | 网络请求分析、瀑布图、资源大小 |
| Performance | 运行时性能、帧率、长任务、火焰图 |
| Memory | 内存分析、堆快照、内存泄漏 |
| Lighthouse | 综合性能审计、优化建议 |
| Coverage | JS/CSS 代码使用率 |
| Layers | 图层分析、GPU 加速检查 |
| Rendering | 重绘区域、布局偏移可视化 |
6.2 在线工具
| 工具 | 网址 | 用途 |
|---|---|---|
| PageSpeed Insights | web.dev/measure | Google 官方性能测试 |
| WebPageTest | webpagetest.org | 多地点、多设备测试 |
| GTmetrix | gtmetrix.com | 性能评分和建议 |
| Bundlephobia | bundlephobia.com | npm 包体积分析 |
6.3 性能监控 SDK
// 简单的性能监控上报
interface PerformanceData {
url: string;
timestamp: number;
fcp: number | undefined;
lcp: number | undefined;
cls: number | undefined;
ttfb: number | undefined;
resources: number;
longTasks: number;
}
class PerformanceMonitor {
private data: Partial<PerformanceData> = {
url: location.href,
timestamp: Date.now(),
longTasks: 0,
};
constructor() {
this.observePaint();
this.observeLongTasks();
this.observeResources();
}
private observePaint(): void {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.data.fcp = entry.startTime;
}
if (entry.entryType === 'largest-contentful-paint') {
this.data.lcp = entry.startTime;
}
}
});
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
}
private observeLongTasks(): void {
const observer = new PerformanceObserver((list) => {
this.data.longTasks = (this.data.longTasks || 0) + list.getEntries().length;
});
observer.observe({ entryTypes: ['longtask'] });
}
private observeResources(): void {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
this.data.ttfb = navigation.responseStart - navigation.requestStart;
this.data.resources = performance.getEntriesByType('resource').length;
// 页面加载完成后上报
this.report();
});
}
private report(): void {
// 上报到监控平台
console.log('性能数据:', this.data);
// 实际项目中使用 navigator.sendBeacon
// navigator.sendBeacon('/api/performance', JSON.stringify(this.data));
}
}
// 使用
new PerformanceMonitor();
七、排查流程总结
常见面试问题
Q1: 如何分析一个网站的性能?
- 使用 Lighthouse 进行整体评估
- Network 面板分析加载性能
- Performance 面板分析运行时性能
- 关注 Web Vitals 核心指标
- 使用 Coverage 检查代码使用率
Q2: 页面白屏怎么排查?
- 检查 TTFB 是否过高(服务端问题)
- 检查 JS/CSS 体积是否过大
- 检查是否有阻塞渲染的资源
- 检查关键路径上的资源加载顺序
- 使用 Performance 面板查看 FCP 时间点
Q3: 如何检测内存泄漏?
- Memory 面板多次拍摄堆快照
- 对比快照,查看 Delta 列
- 按 Retained Size 排序找大对象
- 在 Retainers 中查看引用链
- 搜索 Detached 找游离 DOM
Q4: 什么操作会触发重排?
- 读取布局属性(offsetWidth、getBoundingClientRect)
- 修改几何属性(width、height、margin、padding)
- 修改 DOM 结构(appendChild、removeChild)
- 修改样式类名
- 窗口大小变化
面试要点
- 熟练使用 DevTools 各个面板
- 理解性能指标的含义和优化方向
- 能够系统地排查问题,而不是盲目优化
- 了解浏览器渲染原理,理解重排重绘的代价