页面性能优化 — 当监控大盘亮起红灯之后
凌晨两点,你被一条报警消息惊醒:“首页 LCP 从 1.8s 飙升到 4.7s,CLS 达到 0.38,影响 30% 用户。“你打开 Grafana 看到指标曲线一路走高,而前一天刚上线了一版新的首页改版。这个场景每个前端团队都会遇到——页面性能不是”做完就完”的一次性工作,而是需要持续监控、系统治理的长期工程。
📋 开篇自测:你已经知道多少?
- Core Web Vitals 包含哪三个指标?每个指标衡量的是用户体验的哪个维度?
- 你能说出至少三种减少 LCP 的优化手段吗?
- CLS 的分数是如何计算的?为什么动态插入的广告横幅会导致高 CLS?
一、Core Web Vitals:用指标量化用户体验
1.1 三大核心指标
Google 在 2020 年提出 Core Web Vitals,将用户体验量化为三个可测量的指标:
Core Web Vitals 全景
用户感知 指标 衡量维度 达标阈值
─────────────────────────────────────────────────────────
"页面多久能看到 LCP 加载性能 ≤ 2.5s
主要内容?" (Largest 最大内容元素
Contentful 渲染完成时间
Paint)
"页面稳不稳定? CLS 视觉稳定性 ≤ 0.1
内容会不会跳?" (Cumulative 所有意外布局
Layout 偏移的累积分
Shift)
"页面响应快不快? INP 交互响应性 ≤ 200ms
点击有没有延迟?" (Interaction 从输入到下一帧
to Next 渲染的最大延迟
Paint)
─────────────────────────────────────────────────────────
注: INP 已于 2024年3月取代 FID 成为正式的 Core Web Vital
1.2 指标采集的两种方式
| 维度 | 实验室数据 (Lab Data) | 真实用户数据 (Field Data) |
|---|---|---|
| 采集工具 | Lighthouse, DevTools | Chrome UX Report, web-vitals 库 |
| 数据来源 | 开发者本地模拟 | 真实用户浏览器上报 |
| 网络条件 | 预设的节流参数 | 用户实际网络 |
| 代表性 | 可复现但不代表真实用户 | 代表真实体验但有波动 |
| 用途 | 开发阶段发现问题 | 上线后监控和回归 |
// 使用 web-vitals 库采集真实用户指标
import { onLCP, onCLS, onINP } from 'web-vitals';
function reportToAnalytics({ name, value, id, attribution }) {
const payload = {
metric: name,
value: Math.round(name === 'CLS' ? value * 1000 : value),
page: window.location.pathname,
deviceType: navigator.userAgentData?.mobile ? 'mobile' : 'desktop',
connectionType: navigator.connection?.effectiveType || 'unknown',
timestamp: Date.now(),
};
navigator.sendBeacon('/api/performance', JSON.stringify(payload));
}
onLCP(reportToAnalytics);
onCLS(reportToAnalytics);
onINP(reportToAnalytics);
二、加载性能优化:让页面更快可见
2.1 关键渲染路径分析
从浏览器收到 HTML 到首次渲染,要经历一条关键路径。优化的核心是缩短这条路径:
关键渲染路径
HTML下载 CSS下载
│ │
▼ ▼
HTML解析 ──→ DOM树 CSS解析 ──→ CSSOM树
│ │
└────────┬────────────┘
▼
渲染树 (Render Tree)
│
▼
布局 (Layout)
│
▼
绘制 (Paint)
│
▼
首次内容渲染 (FCP)
阻塞点:
1. CSS 是渲染阻塞资源 — CSSOM 未就绪则无法构建渲染树
2. <head> 中的同步 JS 是解析阻塞资源 — 阻塞 DOM 构建
3. 字体文件未加载 — 文本可能不可见 (FOIT) 或闪烁 (FOUT)
2.2 资源加载优化
资源加载优先级策略
┌─────────────────────────────────────────────────────────┐
│ 优化策略矩阵 │
│ │
│ ┌─ 预连接 ──────────────────────────────────────────┐ │
│ │ <link rel="preconnect" href="https://cdn.app.com">│ │
│ │ 提前完成 DNS + TCP + TLS, 节省 100~300ms │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ 预加载 ──────────────────────────────────────────┐ │
│ │ <link rel="preload" href="hero.webp" as="image"> │ │
│ │ 告诉浏览器"这个资源很重要, 马上下载" │ │
│ │ 适合: LCP 图片、关键字体、首屏 CSS │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ 异步加载 JS ─────────────────────────────────────┐ │
│ │ <script defer> : 并行下载, DOM解析完后按序执行 │ │
│ │ <script async> : 并行下载, 下载完立即执行(不保序) │ │
│ │ 动态 import() : 需要时才加载 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ 图片优化 ────────────────────────────────────────┐ │
│ │ 格式: WebP/AVIF 比 PNG/JPEG 小 25~50% │ │
│ │ 懒加载: <img loading="lazy"> │ │
│ │ 响应式: srcset + sizes 按设备加载合适尺寸 │ │
│ │ 优先级: fetchpriority="high" (LCP图片) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.3 代码分割实战
// 路由级代码分割 (React + React.lazy)
import { lazy, Suspense } from 'react';
// 首页直接打包
import HomePage from './pages/HomePage';
// 其他页面按需加载
const OrderDetail = lazy(() => import('./pages/OrderDetail'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/order/:id" element={<OrderDetail />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
);
}
代码分割前后对比
分割前:
bundle.js (2.4MB)
┌──────────────────────────────────────────────────┐
│ 首页 │ 订单详情 │ 用户中心 │ 管理后台 │ 第三方库 │
└──────────────────────────────────────────────────┘
用户访问首页也要下载全部 2.4MB
分割后:
main.js (380KB) ← 首页 + 公共代码
┌────────────────┐
│ 首页 │ 公共库 │
└────────────────┘
order.chunk.js (210KB) ← 访问订单页时才加载
profile.chunk.js (95KB) ← 访问用户中心时才加载
admin.chunk.js (680KB) ← 访问管理后台时才加载
首次加载: 2.4MB → 380KB (减少 84%)
三、LCP 优化:让最大内容更快渲染
3.1 什么元素会成为 LCP
LCP 候选元素通常是页面中面积最大的可见内容:
LCP 候选元素类型
┌─────────────────────────────────────────────┐
│ 页面首屏 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ <img> 大图/封面图 │ │ ← LCP 候选
│ │ (最常见的 LCP 元素) │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ <h1> 大标题文字 │ ← LCP 候选
│ │
│ <video poster="..."> 视频封面 │ ← LCP 候选
│ │
│ background-image 背景图 │ ← LCP 候选
│ │
└─────────────────────────────────────────────┘
3.2 系统性优化 LCP
// 优化1: LCP 图片使用 fetchpriority="high" + preload
// 在 HTML <head> 中:
// <link rel="preload" as="image" href="/images/hero-banner.webp"
// fetchpriority="high">
// 优化2: 服务端渲染 (SSR) 减少白屏时间
// Next.js 示例
export async function getServerSideProps() {
const featuredProducts = await fetchFeaturedProducts();
return {
props: { featuredProducts }, // 数据在服务端获取, HTML 直出
};
}
function StoreFront({ featuredProducts }) {
return (
<main>
{/* LCP 元素: 首屏大图, 直接由 SSR 输出 HTML */}
<img
src={featuredProducts[0].coverImage}
alt={featuredProducts[0].title}
width={1200}
height={630}
fetchpriority="high"
/>
</main>
);
}
LCP 优化策略清单
┌──────────────────────────────────────────────────────┐
│ 策略 │ 预期收益 │ 实施难度 │
├──────────────────────────────────────────────────────┤
│ preload LCP 资源 │ -200~500ms │ 低 │
│ fetchpriority="high" │ -100~300ms │ 低 │
│ SSR / SSG │ -500~2000ms │ 高 │
│ CDN + HTTP/2 │ -100~500ms │ 中 │
│ 图片格式 WebP/AVIF │ -200~800ms │ 低 │
│ 消除渲染阻塞 CSS/JS │ -300~1000ms │ 中 │
│ 内联关键 CSS │ -100~300ms │ 中 │
│ 字体 font-display:swap │ -100~500ms │ 低 │
└──────────────────────────────────────────────────────┘
四、CLS 优化:让页面不再抖动
4.1 CLS 的计算方式
CLS 衡量的是页面生命周期中所有意外布局偏移的累积分数:
CLS 计算公式
布局偏移分数 = 影响比例 (Impact Fraction) x 距离比例 (Distance Fraction)
示例: 一个占视口 50% 的元素向下偏移了视口高度的 25%
┌─────────────────────┐ 视口
│ ┌───────────────┐ │
│ │ │ │ 元素原位置 (占视口50%)
│ │ 元素(之前) │ │
│ │ │ │
│ └───┬───────────┘ │
│ │ 偏移25% │
│ ┌───▼───────────┐ │
│ │ 元素(之后) │ │
│ │ │ │
│ └───────────────┘ │
└─────────────────────┘
影响比例 = 0.50 + 0.25 = 0.75 (元素前后位置覆盖的总区域)
距离比例 = 0.25 (偏移距离 / 视口高度)
布局偏移分数 = 0.75 x 0.25 = 0.1875
4.2 常见的 CLS 元凶
CLS 元凶排行
1. 无尺寸的图片/视频
加载前: [ 文字内容 ]
加载后: [ 图片(突然撑开) ]
[ 文字内容被推下去 ] ← 布局偏移!
2. 动态插入的横幅/广告
初始: [ 页面内容 ]
2秒后: [ 广告横幅(突然插入) ]
[ 页面内容被推下去 ] ← 布局偏移!
3. 异步加载的字体
初始: [用系统字体渲染的标题] 宽度 300px
字体到达: [用Web字体渲染的标题] 宽度 340px ← 偏移!
4. 动态注入的 DOM 元素
初始: [列表项1] [列表项2] [列表项3]
Ajax后: [列表项0(插入)] [列表项1] [列表项2] [列表项3] ← 偏移!
4.3 修复 CLS
// 修复1: 为图片和视频预留空间
// 方案A: 使用 width 和 height 属性 (浏览器自动计算 aspect-ratio)
<img src="product.webp" width="800" height="450" alt="商品图" />
// 方案B: CSS aspect-ratio
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
background: #f0f0f0; /* 占位底色 */
}
// 修复2: 为动态内容预留容器高度
.ad-slot {
min-height: 250px; /* 广告位预留高度 */
background: #fafafa;
}
// 修复3: 字体加载策略
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand.woff2') format('woff2');
font-display: optional; /* 超时未加载则使用系统字体, 避免闪烁 */
}
// 修复4: 使用 CSS transform 代替改变布局属性的动画
// 坏: 触发布局偏移
.notification {
animation: slideDown 0.3s;
}
@keyframes slideDown {
from { margin-top: -50px; }
to { margin-top: 0; }
}
// 好: transform 不触发布局偏移
.notification {
animation: slideDownSafe 0.3s;
}
@keyframes slideDownSafe {
from { transform: translateY(-50px); }
to { transform: translateY(0); }
}
五、INP 优化:让交互响应更快
5.1 INP 的测量原理
INP(Interaction to Next Paint)衡量的是从用户输入(点击、按键、触摸)到浏览器完成下一帧渲染之间的延迟。它使用一种近似算法来选取代表性的最差交互:交互次数较少的页面取最差值,交互次数较多时取接近最差的值(近似第 98 百分位)——既能反映最差体验,又不会被极端个例拉偏。
INP 时间分解
用户点击按钮
│
├── Input Delay (输入延迟)
│ 主线程正在执行其他任务, 事件处理器排队等待
│
├── Processing Time (处理耗时)
│ 事件处理器执行 (你的 onClick 代码)
│
├── Presentation Delay (渲染延迟)
│ 样式计算 + 布局 + 绘制 + 合成
│
▼
浏览器渲染下一帧
│←──────────── INP ──────────────→│
优化目标: 整个过程 ≤ 200ms
5.2 减少输入延迟
// 问题: 长任务阻塞主线程, 导致输入延迟
function handleSearchInput(keyword) {
// 同步过滤 50000 条商品数据 — 耗时 300ms
const results = allProducts.filter(product =>
product.title.includes(keyword) ||
product.tags.some(tag => tag.includes(keyword))
);
renderSearchResults(results);
}
// 优化: 使用 scheduler.yield() 让出主线程
// 注: scheduler.yield() 是较新的 Scheduler API (Chrome 129+ 支持),其他浏览器可能尚未支持
// 可用 await new Promise(r => setTimeout(r, 0)) 作为 fallback
async function handleSearchInputOptimized(keyword) {
const chunkSize = 5000;
const results = [];
for (let i = 0; i < allProducts.length; i += chunkSize) {
const chunk = allProducts.slice(i, i + chunkSize);
const matched = chunk.filter(product =>
product.title.includes(keyword) ||
product.tags.some(tag => tag.includes(keyword))
);
results.push(...matched);
// 每处理一批, 让出主线程, 允许浏览器处理待处理的用户输入
if (i + chunkSize < allProducts.length) {
await scheduler.yield();
}
}
renderSearchResults(results);
}
5.3 减少处理耗时
// 问题: 事件处理器中做了太多同步工作
submitButton.addEventListener('click', () => {
const formData = collectFormData(); // 5ms
const validated = validateAll(formData); // 20ms
const encrypted = encryptPayload(formData);// 150ms ← 瓶颈!
sendToServer(encrypted); // 异步, 不计入
showConfirmation(); // 5ms
trackAnalyticsEvent(); // 10ms
});
// 总处理时间: ~190ms
// 优化: 将非关键工作推迟
submitButton.addEventListener('click', async () => {
const formData = collectFormData();
const validated = validateAll(formData);
// 立即显示反馈 (让用户感到响应快)
showLoadingState();
// 耗时工作推迟到下一个任务
setTimeout(() => {
const encrypted = encryptPayload(formData);
sendToServer(encrypted);
showConfirmation();
}, 0);
// 分析上报更低优先级
requestIdleCallback(() => {
trackAnalyticsEvent();
});
});
// 用户感知到的处理时间: ~30ms
六、渲染性能优化:避免不必要的重排与重绘
6.1 哪些操作触发重排
触发重排 (Layout / Reflow) 的常见操作
┌───────────────────────────────────────────────────────┐
│ 几何属性变更 │ DOM 结构变更 │
│ · width / height │ · appendChild │
│ · padding / margin │ · removeChild │
│ · top / left / right │ · innerHTML │
│ · font-size │ · textContent │
│ · display │ · 元素可见性切换 │
├───────────────────────────────────────────────────────┤
│ 读取布局信息 (强制同步布局) │
│ · offsetWidth / offsetHeight │
│ · clientWidth / clientHeight │
│ · scrollTop / scrollLeft │
│ · getComputedStyle() │
│ · getBoundingClientRect() │
│ │
│ ⚠️ 读取这些属性会强制浏览器立即执行布局计算! │
└───────────────────────────────────────────────────────┘
6.2 强制同步布局与布局抖动
// 布局抖动 (Layout Thrashing) — 在循环中交替读写布局属性
function resizeAllCards(cards) {
for (let i = 0; i < cards.length; i++) {
// 读取 → 触发强制布局
const parentWidth = cards[i].parentElement.offsetWidth;
// 写入 → 使布局失效
cards[i].style.width = (parentWidth * 0.8) + 'px';
// 下一次循环再读取 → 又触发强制布局!
}
// 1000 个卡片 = 1000 次强制布局! 可能耗时数百毫秒
}
// 优化: 批量读取, 批量写入
function resizeAllCardsOptimized(cards) {
// 第一步: 批量读取 (只触发一次布局)
const widths = cards.map(card => card.parentElement.offsetWidth);
// 第二步: 批量写入 (布局只在最后一次性计算)
cards.forEach((card, i) => {
card.style.width = (widths[i] * 0.8) + 'px';
});
}
布局抖动 vs 批量操作
布局抖动:
[读→布局→写→无效][读→布局→写→无效][读→布局→写→无效]...
↑ ↑ ↑
强制布局 强制布局 强制布局 × N次
批量操作:
[读][读][读]...[写][写][写]...[布局]
↑
只触发一次布局 × 1次
6.3 合成层优化
利用 GPU 合成层避免重排重绘
CPU 处理 GPU 处理
┌─────────────────────────┐ ┌──────────────────┐
│ JS → 样式 → 布局 → 绘制 │ → │ 合成 → 显示 │
└─────────────────────────┘ └──────────────────┘
重排重绘 (expensive) 合成 (cheap)
触发重排的属性: 触发合成的属性:
width, height, margin transform
padding, top, left opacity
font-size, display will-change
→ 整条流水线重新走 → 跳过布局和绘制!
// 坏: 用 top/left 做动画 → 每帧触发重排+重绘
.sliding-panel {
position: absolute;
transition: left 0.3s ease;
}
.sliding-panel.open {
left: 0; /* 触发重排 */
}
.sliding-panel.closed {
left: -300px; /* 触发重排 */
}
// 好: 用 transform 做动画 → 仅触发合成
.sliding-panel {
transition: transform 0.3s ease;
will-change: transform; /* 提示浏览器提升为合成层 */
}
.sliding-panel.open {
transform: translateX(0); /* 仅合成 */
}
.sliding-panel.closed {
transform: translateX(-300px); /* 仅合成 */
}
七、性能监控体系搭建
7.1 监控架构
前端性能监控体系
┌──────────────────────────────────────────────────────────┐
│ 用户浏览器 │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ web-vitals 库 + Performance API + PerformanceObserver │ │
│ │ │ │
│ │ 采集: LCP / CLS / INP / FCP / TTFB │ │
│ │ 采集: 长任务 (Long Tasks > 50ms) │ │
│ │ 采集: 资源加载耗时 │ │
│ └────────────────────┬──────────────────────────────┘ │
│ │ sendBeacon / fetch │
└───────────────────────┼──────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ 数据收集服务 (Kafka / API Gateway) │
│ │ │
│ ▼ │
│ 数据处理 (按页面/设备/网络/地区分组) │
│ │ │
│ ┌────────┼────────┐ │
│ ▼ ▼ ▼ │
│ 存储层 报警系统 可视化 │
│ (ClickHouse) (阈值触发) (Grafana) │
└──────────────────────────────────────────────────────────┘
7.2 使用 PerformanceObserver 采集数据
// 监控长任务
const longTaskObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (entry.duration > 100) {
console.warn(`[长任务警告] 耗时 ${entry.duration.toFixed(0)}ms`, {
startTime: entry.startTime.toFixed(0),
name: entry.name,
});
}
}
});
longTaskObserver.observe({ type: 'longtask', buffered: true });
// 监控资源加载
const resourceObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (entry.duration > 3000) {
console.warn(`[慢资源] ${entry.name} 加载耗时 ${entry.duration.toFixed(0)}ms`);
}
}
});
resourceObserver.observe({ type: 'resource', buffered: true });
// 监控布局偏移
const clsObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput && entry.value > 0.05) {
console.warn(`[CLS警告] 偏移 ${entry.value.toFixed(4)}`, {
sources: entry.sources?.map(s => s.node?.nodeName),
});
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
7.3 性能预算
性能预算示例
┌────────────────────────────────────────────────────┐
│ 性能预算 (Performance Budget) │
├──────────────────┬────────────┬────────────────────┤
│ 指标 │ 预算阈值 │ 报警阈值 (P75) │
├──────────────────┼────────────┼────────────────────┤
│ LCP │ ≤ 2.5s │ > 3.0s │
│ CLS │ ≤ 0.1 │ > 0.15 │
│ INP │ ≤ 200ms │ > 300ms │
│ FCP │ ≤ 1.8s │ > 2.5s │
│ TTFB │ ≤ 800ms │ > 1200ms │
│ JS Bundle Size │ ≤ 300KB │ > 400KB (gzip后) │
│ 首屏图片总大小 │ ≤ 500KB │ > 800KB │
│ 长任务(>50ms)次数│ ≤ 3次 │ > 5次 │
└──────────────────┴────────────┴────────────────────┘
在 CI/CD 中集成:
- Lighthouse CI: 每次 MR 自动跑分, 低于预算则阻断合入
- Bundle Analyzer: 监控包体积变化, 超出预算则报警
八、本章知识脉络总结
页面性能优化知识地图
性能优化
├── 度量体系
│ ├── Core Web Vitals: LCP / CLS / INP
│ ├── 辅助指标: FCP / TTFB / TTI
│ ├── 实验室数据: Lighthouse / DevTools
│ └── 真实用户数据: web-vitals / CrUX
│
├── 加载优化
│ ├── 关键渲染路径: 消除阻塞资源
│ ├── 资源提示: preconnect / preload / prefetch
│ ├── 代码分割: 路由级 + 组件级懒加载
│ ├── 图片: WebP/AVIF, 懒加载, 响应式
│ └── 缓存: HTTP缓存 / Service Worker
│
├── LCP 优化
│ ├── preload LCP 资源 + fetchpriority
│ ├── SSR / SSG 直出首屏
│ ├── 内联关键CSS, 延迟非关键CSS
│ └── 字体 font-display: swap/optional
│
├── CLS 优化
│ ├── 图片/视频预留尺寸
│ ├── 动态内容预留容器高度
│ ├── 字体加载策略
│ └── transform 代替布局属性动画
│
├── INP 优化
│ ├── 拆分长任务: scheduler.yield()
│ ├── 延迟非关键工作: setTimeout / rIC
│ └── 减少事件处理器中的同步计算
│
├── 渲染优化
│ ├── 避免强制同步布局
│ ├── 批量读写DOM, 防止布局抖动
│ ├── 合成层: transform / opacity / will-change
│ └── 虚拟列表: 大数据集渲染
│
└── 监控体系
├── PerformanceObserver API
├── 数据上报: sendBeacon
├── 可视化: Grafana 看板
└── CI 集成: Lighthouse CI + 性能预算
思考题
-
你的团队上线了一个新功能——在文章列表页顶部添加一条公告横幅,上线后 CLS 从 0.05 飙升到 0.32。请分析原因并给出至少两种修复方案。
-
一位同事在 React 组件的
useEffect中做了一次大量 DOM 测量(读取 200 个元素的offsetHeight),然后根据测量结果批量修改这些元素的style.height。他反馈页面交互时有明显卡顿。从浏览器渲染原理的角度,分析卡顿的原因,并给出优化建议。
结尾自测
Q1: Core Web Vitals 包含哪三个指标?各自的达标阈值是多少?
A: LCP (Largest Contentful Paint, 不超过 2.5 秒)、CLS (Cumulative Layout Shift, 不超过 0.1)、INP (Interaction to Next Paint, 不超过 200ms)。LCP 衡量加载性能,CLS 衡量视觉稳定性,INP 衡量交互响应性。
Q2: <script defer> 和 <script async> 有什么区别?各适合什么场景?
A: defer 并行下载,在 DOM 解析完成后按文档中出现的顺序执行,适合有依赖关系的业务脚本。async 并行下载,下载完立即执行(不保证顺序),适合独立的第三方脚本(如统计代码)。两者都不阻塞 HTML 解析。
Q3: 为什么 transform 做动画比 left/top 更高效?
A: left/top 改变元素的几何属性,会触发布局(重排)和绘制(重绘),整条渲染流水线都要重新走。transform 不改变元素在布局中的位置,仅在合成阶段由 GPU 处理,跳过了布局和绘制步骤,性能开销极小。
Q4: 什么是布局抖动(Layout Thrashing)?如何避免?
A: 布局抖动是在一次 JavaScript 执行中交替读取布局属性和写入样式,导致浏览器在每次读取时被迫同步执行布局计算。避免方法是将所有读取操作集中在前面,写入操作集中在后面(批量读、批量写),或者使用 requestAnimationFrame 将写入推迟到下一帧。
Q5: 如何使用 PerformanceObserver 监控页面中的长任务?长任务的阈值是多少?
A: 使用 new PerformanceObserver(callback).observe({ type: 'longtask', buffered: true }) 监听长任务。长任务的定义是执行时间超过 50ms 的任务。当检测到长任务时,可以记录其 duration、startTime 等信息,上报到监控系统进行分析。
下一章预告:性能优化让页面更快、更流畅,但还有一个维度同样重要——安全。下一章我们将深入浏览器的安全机制,理解同源策略如何保护用户数据,拆解 XSS、CSRF、点击劫持的攻防手法,掌握 CSP、沙箱隔离与站点隔离的底层原理。
购买课程解锁全部内容
前端进阶第一课:11 章掌握浏览器核心
¥29.90