常见网站性能问题与排查
问题
网站常见的性能问题有哪些?可能的原因是什么?应该如何排查?
线上常见的性能问题有哪些? 按阶段分四类,每类都有典型表现:
- 加载慢:首屏白屏久、资源 404、第三方脚本拖累。
- 运行卡顿:滚动掉帧、点击没反应、长任务(Long Task > 50ms)。
- 内存泄漏:SPA 越用越卡,最后页面崩溃。
- 网络异常:接口慢、CDN 抖动、HTTPS 握手耗时。
怎么排查这些问题? 按「指标 → 工具 → 定位」的套路走:
- 白屏长:先用
PerformanceNavigationTiming和 Paint Timing 看TTFB、FCP,再拆 LCP 的资源发现、下载和渲染阶段。TTFB 高优先查服务端/边缘节点;TTFB 正常但 FCP/LCP 高,再看渲染阻塞资源和主线程。 - 运行卡顿:DevTools Performance 面板录一段,找红色长任务(Long Task),看主线程在干嘛——通常是大列表渲染、复杂计算或频繁 setState。
- 内存泄漏:Memory 面板做 3 次 Heap Snapshot,对比 Detached DOM 和闭包引用,常见祸首是定时器、事件监听、Vuex/Redux 缓存。
- 走 Lighthouse 拿到完整报告作为切入点,再用
web-vitals库做线上 RUM 上报,区分 P75/P95 用户。
答案
网站性能问题主要分为加载性能、运行时性能、渲染性能和网络性能四大类。
一、加载性能问题
1.1 首屏白屏时间过长
现象:用户打开页面后长时间看到白屏
可能原因:
| 原因 | 说明 |
|---|---|
| JS 文件过大 | 例如在中端移动设备和普通 4G 场景下,首屏仍需下载、解析数百 KB JavaScript |
| 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;
// 从当前导航的 time origin 起算,覆盖重定向、Service Worker 等前置阶段
const ttfb = navigation.responseStart - navigation.startTime;
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');
// 注意:Resource Timing 通常不会暴露真正失败的请求;跨域未设置
// Timing-Allow-Origin、命中缓存也可能让 transferSize 为 0。
const opaqueOrCachedResources = resources.filter(
(r) => (r as PerformanceResourceTiming).transferSize === 0
);
// 资源失败应以捕获阶段的 error 事件、Service Worker 日志和服务端/CDN 日志为准。
console.log('需要进一步确认的资源:', opaqueOrCachedResources);
二、运行时性能问题
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'] });
// 支持时再观察 Long Animation Frames,它能把脚本执行与一整帧的渲染关联起来
if (PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
const loafObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('长动画帧:', entry.duration, entry);
}
});
loafObserver.observe({ type: 'long-animation-frame', buffered: true });
}
// 测量帧率
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 数量
- 在 60Hz 屏幕上,一帧约 16.67ms;在 120Hz 屏幕上约 8.33ms,且浏览器自身也需要时间完成合成与呈现。
- 用户感知取决于帧间隔是否稳定、输入延迟、动画类型和设备,不能用“低于 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 |
| INP | Interaction to Next Paint | 交互到下一帧 | ≤ 200ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | ≤ 0.1 |
| FCP | First Contentful Paint | 首次内容绘制 | ≤ 1.8s(辅助指标) |
| TTFB | Time to First Byte | 首字节时间 | ≤ 800ms(辅助指标) |
上述阈值用于分别评估移动端和桌面端页面访问样本的 P75。FID 已于 2024 年退出 Core Web Vitals,旧报告中仍可能看到它,但新系统应采集 INP。
5.2 测量代码
import { onCLS, onFCP, onINP, onLCP, onTTFB } from 'web-vitals/attribution';
// 测量所有核心指标
function measureWebVitals(): void {
onLCP((metric) => {
console.log('LCP:', 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, metric.attribution);
});
}
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.startTime;
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();
七、从“指标异常”定位到根因
7.1 LCP 按四个阶段拆解
| 阶段 | 常见根因 | 证据 | 优化方向 |
|---|---|---|---|
| TTFB | 应用服务器、数据库、边缘节点慢 | Navigation Timing、Server-Timing | 缓存、并行取数、减少服务端计算 |
| 资源加载延迟 | LCP 资源发现太晚或优先级低 | Network waterfall、LCP discovery insight | 初始 HTML 可发现、选择性 preload/fetchpriority |
| 资源下载时长 | 文件过大、CDN/网络慢 | Resource Timing、CDN 日志 | 响应式图片、压缩、CDN、缓存 |
| 元素渲染延迟 | JS 阻塞、字体、主线程渲染慢 | Performance trace | 减少首屏 JS、拆任务、缩小布局/绘制范围 |
7.2 INP 按三个阶段拆解
| 阶段 | 先查什么 | 常见处理 |
|---|---|---|
| Input Delay | 交互前正在运行的长任务和第三方脚本 | 延迟非关键工作、拆分任务、减少启动期 JS |
| Processing Duration | 事件回调、状态更新和同步计算 | 提前反馈、减少同步工作、必要时使用 Worker |
| Presentation Delay | style/layout/paint、DOM 规模、框架提交阶段 | 缩小更新范围、虚拟化、避免布局抖动 |
7.3 把现场数据和实验室证据串起来
一次可复现的排查至少保留:路由模板、构建版本、设备/网络档位、指标 ID、归因元素或交互目标、traceId。服务端可以通过 Server-Timing 暴露网关、缓存、数据库等阶段,让浏览器瀑布图与后端 trace 对齐。
Server-Timing: edge;dur=18, app;dur=96, db;dur=42
先用 RUM 判断“哪些用户、哪些页面、哪个版本慢”,再在相同设备和用户路径下录制 Performance trace。Lighthouse 适合发现加载期问题,但不能代替真实交互和长会话排查。
八、排查流程总结
常见面试问题
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 各个面板
- 理解性能指标的含义和优化方向
- 能够系统地排查问题,而不是盲目优化
- 了解浏览器渲染原理,理解重排重绘的代价