跳到主要内容
预计阅读 53 分钟

页面性能优化 — 当监控大盘亮起红灯之后

凌晨两点,你被一条报警消息惊醒:“首页 LCP 从 1.8s 飙升到 4.7s,CLS 达到 0.38,影响 30% 用户。“你打开 Grafana 看到指标曲线一路走高,而前一天刚上线了一版新的首页改版。这个场景每个前端团队都会遇到——页面性能不是”做完就完”的一次性工作,而是需要持续监控、系统治理的长期工程。

📋 开篇自测:你已经知道多少?

  1. Core Web Vitals 包含哪三个指标?每个指标衡量的是用户体验的哪个维度?
  2. 你能说出至少三种减少 LCP 的优化手段吗?
  3. 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, DevToolsChrome 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 + 性能预算

思考题

  1. 你的团队上线了一个新功能——在文章列表页顶部添加一条公告横幅,上线后 CLS 从 0.05 飙升到 0.32。请分析原因并给出至少两种修复方案。

  2. 一位同事在 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 的任务。当检测到长任务时,可以记录其 durationstartTime 等信息,上报到监控系统进行分析。


下一章预告:性能优化让页面更快、更流畅,但还有一个维度同样重要——安全。下一章我们将深入浏览器的安全机制,理解同源策略如何保护用户数据,拆解 XSS、CSRF、点击劫持的攻防手法,掌握 CSP、沙箱隔离与站点隔离的底层原理。

购买课程解锁全部内容

前端进阶第一课:11 章掌握浏览器核心

¥29.90