跳到主要内容

常见网站性能问题与排查

问题

网站常见的性能问题有哪些?可能的原因是什么?应该如何排查?

答案

网站性能问题主要分为加载性能运行时性能渲染性能网络性能四大类。


一、加载性能问题

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 排查

  1. Network 面板:查看资源加载瀑布图

    • 关注 TTFB(等待时间)
    • 查看资源大小和加载时间
    • 检查是否有阻塞请求
  2. Performance 面板

    • 查看 FCP、LCP 时间点
    • 分析 Main Thread 活动
  3. 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 排查

  1. Performance 面板

    • 录制页面操作
    • 查看 Main Thread 火焰图
    • 找出红色三角标记的长任务
    • 分析 Scripting、Rendering、Painting 时间占比
  2. 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 仍持有引用
}

排查方法

  1. Memory 面板 - Heap Snapshot

    • 多次快照对比
    • 查看 "Objects allocated between Snapshot 1 and Snapshot 2"
    • 搜索 "Detached" 查找游离 DOM
  2. Memory 面板 - Allocation Timeline

    • 录制内存分配过程
    • 蓝色条 = 分配,灰色条 = 已回收
    • 持续蓝色条 = 潜在泄漏
  3. 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',
});
}
内存泄漏排查技巧
  1. 在怀疑有泄漏的操作前后各拍一次快照
  2. 对比两次快照,按 "Retained Size" 排序
  3. 查找增长最大的对象类型
  4. 在 "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 排查

  1. Performance 面板

    • 查看紫色的 "Layout" 事件
    • 点击查看触发重排的代码位置
    • 查看 "Recalculate Style" 事件
  2. 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 加速
  • 动画帧率不稳定

排查方法

  1. Layers 面板

    • 查看哪些元素创建了独立图层
    • 检查图层数量是否过多
  2. 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 排查

  1. 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 排查

  1. Network 面板:按 Size 排序
  2. Coverage 面板:查看 JS/CSS 使用率

五、核心性能指标(Web Vitals)

5.1 指标说明

指标全称含义良好标准
LCPLargest Contentful Paint最大内容绘制< 2.5s
FIDFirst Input Delay首次输入延迟< 100ms
CLSCumulative Layout Shift累积布局偏移< 0.1
FCPFirst Contentful Paint首次内容绘制< 1.8s
TTFBTime to First Byte首字节时间< 800ms
INPInteraction 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综合性能审计、优化建议
CoverageJS/CSS 代码使用率
Layers图层分析、GPU 加速检查
Rendering重绘区域、布局偏移可视化

6.2 在线工具

工具网址用途
PageSpeed Insightsweb.dev/measureGoogle 官方性能测试
WebPageTestwebpagetest.org多地点、多设备测试
GTmetrixgtmetrix.com性能评分和建议
Bundlephobiabundlephobia.comnpm 包体积分析

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: 如何分析一个网站的性能?

  1. 使用 Lighthouse 进行整体评估
  2. Network 面板分析加载性能
  3. Performance 面板分析运行时性能
  4. 关注 Web Vitals 核心指标
  5. 使用 Coverage 检查代码使用率

Q2: 页面白屏怎么排查?

  1. 检查 TTFB 是否过高(服务端问题)
  2. 检查 JS/CSS 体积是否过大
  3. 检查是否有阻塞渲染的资源
  4. 检查关键路径上的资源加载顺序
  5. 使用 Performance 面板查看 FCP 时间点

Q3: 如何检测内存泄漏?

  1. Memory 面板多次拍摄堆快照
  2. 对比快照,查看 Delta
  3. Retained Size 排序找大对象
  4. Retainers 中查看引用链
  5. 搜索 Detached 找游离 DOM

Q4: 什么操作会触发重排?

  • 读取布局属性(offsetWidth、getBoundingClientRect)
  • 修改几何属性(width、height、margin、padding)
  • 修改 DOM 结构(appendChild、removeChild)
  • 修改样式类名
  • 窗口大小变化
面试要点
  1. 熟练使用 DevTools 各个面板
  2. 理解性能指标的含义和优化方向
  3. 能够系统地排查问题,而不是盲目优化
  4. 了解浏览器渲染原理,理解重排重绘的代价

相关链接