性能指标与优化:从 Core Web Vitals 到实战

我曾遇到这样的困惑:花了很多时间优化代码,减少了回流次数,设置了合理的缓存策略,但如何证明页面"变快了"?用户说"感觉还是有点慢",但我不知道具体慢在哪里。这让我开始思考:性能到底应该如何度量?哪些指标真正影响用户体验?

这篇文章是我对 Web 性能指标的学习总结,重点关注 Google 推出的 Core Web Vitals,以及如何将这些指标应用到实际优化中。

问题的起源

为什么需要性能指标?最直接的原因是:性能影响业务

根据 Google 的研究:

  • 页面加载时间从 1 秒增加到 3 秒,跳出率增加 32%
  • 页面加载时间从 1 秒增加到 5 秒,跳出率增加 90%
  • 移动端加载时间超过 3 秒,53% 的用户会放弃访问

但"快"是一个很主观的概念。同样的页面,不同用户的感受可能完全不同。性能指标的作用,就是把主观的"快慢"转化为客观的、可度量的数字。

核心概念探索

1. Core Web Vitals:Google 的三大核心指标

2020 年,Google 推出了 Core Web Vitals,包含三个核心指标:

LCP(Largest Contentful Paint):最大内容绘制

定义:页面主要内容(最大的图片或文本块)完成渲染的时间。

javascript 复制代码
// 环境:浏览器
// 场景:监测 LCP

// 使用 PerformanceObserver API
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  
  console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);
  console.log('LCP element:', lastEntry.element);
});

observer.observe({ entryTypes: ['largest-contentful-paint'] });

// LCP 测量的是什么?
// - <img> 元素
// - <svg> 内的 <image> 元素
// - <video> 元素的封面图
// - 带背景图的块级元素
// - 包含文本的块级元素

评分标准

  • 优秀(Good):< 2.5 秒
  • 需要改进(Needs Improvement):2.5 - 4.0 秒
  • 差(Poor):> 4.0 秒

常见问题与优化

html 复制代码
// 问题 1:大图片加载慢
// 解决方案:

// 1. 图片压缩
// 使用 WebP/AVIF 格式,压缩率更高
<img src="hero.webp" alt="Hero image">

// 2. 响应式图片
<img 
  srcset="hero-320w.jpg 320w,
          hero-640w.jpg 640w,
          hero-1280w.jpg 1280w"
  sizes="(max-width: 640px) 100vw, 50vw"
  src="hero-1280w.jpg"
  alt="Hero image"
>

// 3. 预加载关键图片
<link rel="preload" as="image" href="hero.jpg">

// 4. 使用 CDN
<img src="https://cdn.example.com/hero.jpg" alt="Hero image">
css 复制代码
/* 问题 2:自定义字体阻塞文本渲染 */
/* 解决方案:使用 font-display */

@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 先显示系统字体,字体加载完再替换 */
}

/* font-display 选项:
   - auto: 浏览器默认行为
   - block: 阻塞渲染(不推荐)
   - swap: 立即显示后备字体,加载完替换(推荐)
   - fallback: 短暂阻塞(100ms),超时显示后备字体
   - optional: 极短阻塞,网络慢时放弃加载自定义字体
*/

FID(First Input Delay)→ INP(Interaction to Next Paint)

注意:FID 已被 INP 取代(2024 年 3 月),但很多资料仍在讨论 FID。

FID 定义:用户首次与页面交互(点击、输入)到浏览器响应的延迟。

INP 定义:整个页面生命周期中,所有交互延迟的代表值(通常是 98 分位数)。

javascript 复制代码
// 环境:浏览器
// 场景:监测 INP(简化版)

let interactions = [];

// 监听所有交互事件
['pointerdown', 'click', 'keydown'].forEach(type => {
  document.addEventListener(type, (event) => {
    const startTime = performance.now();
    
    // 使用 requestAnimationFrame 测量到下一帧的时间
    requestAnimationFrame(() => {
      const delay = performance.now() - startTime;
      interactions.push(delay);
      console.log(`${type} delay:`, delay);
    });
  });
});

// INP 计算(简化版,实际更复杂)
function calculateINP() {
  if (interactions.length === 0) return 0;
  
  // 取 98 分位数
  const sorted = interactions.sort((a, b) => a - b);
  const index = Math.floor(sorted.length * 0.98);
  return sorted[index];
}

评分标准

  • 优秀(Good):< 200 毫秒
  • 需要改进(Needs Improvement):200 - 500 毫秒
  • 差(Poor):> 500 毫秒

常见问题与优化

javascript 复制代码
// 问题 1:长任务阻塞主线程
// 主线程被 JavaScript 执行占用,无法响应用户交互

// ❌ 不好的做法:同步处理大量数据
function processLargeData(data) {
  const result = [];
  for (let i = 0; i < data.length; i++) {
    // 复杂计算
    result.push(expensiveOperation(data[i]));
  }
  return result;
}

// ✅ 好的做法 1:分片处理(Time Slicing)
async function processLargeDataInChunks(data, chunkSize = 100) {
  const result = [];
  
  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    
    // 处理一个分片
    for (const item of chunk) {
      result.push(expensiveOperation(item));
    }
    
    // 让出主线程,让浏览器有机会响应用户交互
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  
  return result;
}

// ✅ 好的做法 2:使用 Web Worker
// main.js
const worker = new Worker('worker.js');

worker.postMessage({ data: largeData });

worker.onmessage = (event) => {
  const result = event.data;
  console.log('Processing complete:', result);
};

// worker.js
self.onmessage = (event) => {
  const { data } = event.data;
  const result = data.map(item => expensiveOperation(item));
  self.postMessage(result);
};
javascript 复制代码
// 问题 2:事件处理函数执行时间过长

// ❌ 不好的做法:在 click 事件中执行耗时操作
button.addEventListener('click', () => {
  // 耗时操作
  const result = processLargeData(data);
  updateUI(result);
});

// ✅ 好的做法 1:防抖(Debounce)
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

input.addEventListener('input', debounce((event) => {
  search(event.target.value);
}, 300));

// ✅ 好的做法 2:节流(Throttle)
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  updateScrollPosition();
}, 100));

CLS(Cumulative Layout Shift):累积布局偏移

定义:页面生命周期内所有意外布局偏移的累积分数。

javascript 复制代码
// 环境:浏览器
// 场景:监测 CLS

let clsScore = 0;

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 只统计非用户交互引起的布局偏移
    if (!entry.hadRecentInput) {
      clsScore += entry.value;
      console.log('Layout shift:', entry.value);
      console.log('Cumulative CLS:', clsScore);
    }
  }
});

observer.observe({ entryTypes: ['layout-shift'] });

// 布局偏移分数计算:
// score = impact fraction × distance fraction
// - impact fraction: 移动元素占视口的比例
// - distance fraction: 移动距离占视口的比例

评分标准

  • 优秀(Good):< 0.1
  • 需要改进(Needs Improvement):0.1 - 0.25
  • 差(Poor):> 0.25

常见问题与优化

html 复制代码
<!-- 问题 1:图片、视频没有设置尺寸 -->

<!-- ❌ 不好的做法:没有指定尺寸 -->
<img src="hero.jpg" alt="Hero">
<!-- 图片加载完成前,浏览器不知道要预留多少空间
     图片加载完成后,下方内容会被推下去,造成布局偏移 -->

<!-- ✅ 好的做法 1:显式指定宽高 -->
<img src="hero.jpg" alt="Hero" width="1200" height="800">

<!-- ✅ 好的做法 2:使用 aspect-ratio(推荐) -->
<style>
  .hero-image {
    width: 100%;
    aspect-ratio: 16 / 9; /* 浏览器会自动计算高度 */
  }
</style>
<img src="hero.jpg" alt="Hero" class="hero-image">
html 复制代码
<!-- 问题 2:动态插入内容(广告、提示条) -->

<!-- ❌ 不好的做法:直接插入,推开现有内容 -->
<div id="banner"></div>
<script>
  // 1 秒后加载广告
  setTimeout(() => {
    document.getElementById('banner').innerHTML = '<div class="ad">广告内容</div>';
  }, 1000);
  // 广告出现时,下方内容被推下去
</script>

<!-- ✅ 好的做法:预留空间 -->
<div id="banner" style="min-height: 100px;">
  <!-- 即使内容未加载,也会预留 100px 空间 -->
</div>

<!-- ✅ 更好的做法:使用骨架屏 -->
<div id="banner">
  <div class="skeleton" style="height: 100px; background: #f0f0f0;"></div>
</div>
css 复制代码
/* 问题 3:Web 字体加载导致文本跳动 */

/* ❌ 不好的做法:默认行为 */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
}

body {
  font-family: 'CustomFont', sans-serif;
}

/* 字体加载前显示后备字体,加载后切换,可能导致布局偏移 */

/* ✅ 好的做法:使用 font-display: optional */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: optional; /* 如果字体加载慢,就不使用,避免布局偏移 */
}

/* ✅ 更好的做法:调整后备字体以匹配自定义字体 */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 100%; /* 调整字体大小以匹配后备字体 */
}

2. 其他重要指标

TTFB(Time to First Byte):首字节时间

服务器响应第一个字节的时间。

javascript 复制代码
// 环境:浏览器
// 场景:测量 TTFB

const navigationTiming = performance.getEntriesByType('navigation')[0];
const ttfb = navigationTiming.responseStart - navigationTiming.requestStart;

console.log('TTFB:', ttfb);

// 影响 TTFB 的因素:
// 1. 服务器处理时间
// 2. 网络延迟
// 3. DNS 查询时间
// 4. TCP 连接时间
// 5. TLS 握手时间(HTTPS)

优化方向

  • 使用 CDN(减少网络延迟)
  • 服务端缓存(减少处理时间)
  • 数据库查询优化
  • 使用 HTTP/2 或 HTTP/3
  • DNS 预解析

FCP(First Contentful Paint):首次内容绘制

浏览器渲染第一个 DOM 内容的时间(文本、图片、Canvas等)。

javascript 复制代码
// 环境:浏览器
// 场景:测量 FCP

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  }
});

observer.observe({ entryTypes: ['paint'] });

// FCP vs LCP:
// FCP:第一个内容(可能是小图标、文字)
// LCP:最大内容(通常是主图、标题)
// FCP < LCP

TTI(Time to Interactive):可交互时间

页面完全可交互的时间(主线程空闲,没有长任务)。

arduino 复制代码
// TTI 的判定标准:
// 1. FCP 已完成
// 2. 最近 5 秒内没有长任务(执行时间 > 50ms)
// 3. 没有超过 2 个正在进行的网络请求

// 这个指标较难直接测量,通常使用工具(Lighthouse)

实际场景思考

场景 1:使用 Lighthouse 进行性能分析

Lighthouse 是 Chrome 内置的性能分析工具。

步骤:

  1. 打开 Chrome DevTools(F12)
  2. 切换到 "Lighthouse" 标签
  3. 选择 "Performance" 分析类型
  4. 选择设备(Desktop / Mobile)
  5. 点击 "Analyze page load"

Lighthouse 会给出:

  • Performance 分数(0-100)
  • Core Web Vitals 指标
  • 优化建议(按影响排序)
  • 诊断信息(资源加载瀑布图等)

解读 Lighthouse 报告

yaml 复制代码
// 示例报告:

Performance: 65/100  // 需要改进

Metrics:
  FCP: 1.8s  ✓ 优秀
  LCP: 4.2s  ✗ 差
  TBT: 350ms ⚠ 需要改进
  CLS: 0.05  ✓ 优秀
  SI:  3.2s  ⚠ 需要改进

Opportunities (优化建议):
  1. Eliminate render-blocking resources  
     → 节省 1.2s
     → 建议:使用 async/defer 加载 JS
  
  2. Properly size images
     → 节省 800ms
     → 建议:压缩图片,使用 WebP
  
  3. Reduce unused JavaScript
     → 节省 600ms
     → 建议:代码分割,移除未使用代码

Diagnostics (诊断信息):
  - Minimize main-thread work (主线程工作时间: 3.5s)
  - Reduce JavaScript execution time (JS 执行: 2.1s)

场景 2:实现性能监控

在生产环境中,我们需要持续监控真实用户的性能数据(RUM: Real User Monitoring)。

javascript 复制代码
// 环境:浏览器
// 场景:收集并上报性能数据

class PerformanceMonitor {
  constructor() {
    this.metrics = {};
  }
  
  // 收集 Core Web Vitals
  collectWebVitals() {
    // LCP
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      const lastEntry = entries[entries.length - 1];
      this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
    }).observe({ entryTypes: ['largest-contentful-paint'] });
    
    // FID (已废弃,这里仅作示例)
    new PerformanceObserver((list) => {
      const entries = list.getEntries();
      this.metrics.fid = entries[0].processingStart - entries[0].startTime;
    }).observe({ entryTypes: ['first-input'] });
    
    // CLS
    let clsScore = 0;
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsScore += entry.value;
        }
      }
      this.metrics.cls = clsScore;
    }).observe({ entryTypes: ['layout-shift'] });
  }
  
  // 收集导航时间
  collectNavigationTiming() {
    const timing = performance.getEntriesByType('navigation')[0];
    
    this.metrics.ttfb = timing.responseStart - timing.requestStart;
    this.metrics.domContentLoaded = timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart;
    this.metrics.loadComplete = timing.loadEventEnd - timing.loadEventStart;
  }
  
  // 上报数据
  report() {
    // 页面即将卸载时上报
    window.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.sendMetrics();
      }
    });
  }
  
  sendMetrics() {
    // 使用 sendBeacon 确保数据发送成功(即使页面关闭)
    const data = JSON.stringify({
      url: location.href,
      metrics: this.metrics,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
    });
    
    navigator.sendBeacon('/api/performance', data);
  }
}

// 使用
const monitor = new PerformanceMonitor();
monitor.collectWebVitals();
monitor.collectNavigationTiming();
monitor.report();

场景 3:优化首屏 LCP

一个实际的优化案例:

arduino 复制代码
// 问题:电商网站的商品详情页 LCP 达到 5.2 秒

// 排查步骤:

// 1. 运行 Lighthouse,发现 LCP 元素是商品主图(1.5MB)
// 2. 检查 Network 面板,发现:
//    - 图片从第三方 CDN 加载(500ms TTFB)
//    - 图片格式是 JPG,未压缩
//    - 图片加载优先级较低

// 优化方案:

// 方案 1:图片优化
// - 压缩:1.5MB → 300KB
// - 格式转换:JPG → WebP
// - 响应式图片:不同设备加载不同尺寸
html 复制代码
<!-- 优化前 -->
<img src="https://cdn.example.com/product-large.jpg" alt="Product">

<!-- 优化后 -->
<picture>
  <source 
    type="image/webp"
    srcset="https://cdn.example.com/product-320.webp 320w,
            https://cdn.example.com/product-640.webp 640w,
            https://cdn.example.com/product-1280.webp 1280w"
    sizes="(max-width: 640px) 100vw, 50vw"
  >
  <img 
    src="https://cdn.example.com/product-1280.jpg"
    alt="Product"
    width="1280"
    height="960"
  >
</picture>
html 复制代码
<!-- 方案 2:预加载关键图片 -->
<head>
  <link rel="preload" as="image" href="https://cdn.example.com/product-1280.webp" fetchpriority="high">
</head>
javascript 复制代码
// 方案 3:图片懒加载(非 LCP 图片)
// 只懒加载首屏以外的图片,首屏图片立即加载

// 环境:浏览器
// 场景:原生懒加载

// LCP 图片(首屏主图):不懒加载
<img src="hero.jpg" alt="Hero" loading="eager">

// 其他图片(首屏以下):懒加载
<img src="detail-1.jpg" alt="Detail" loading="lazy">

// 或者使用 Intersection Observer 实现更精细的控制
const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

优化结果

  • LCP:5.2s → 2.1s
  • 图片大小:1.5MB → 300KB(WebP)
  • Performance 分数:45 → 78

场景 4:优化 INP(减少交互延迟)

javascript 复制代码
// 问题:数据表格的排序功能响应慢

// ❌ 原始代码:同步排序 10000 条数据
function sortTable(data, key) {
  const sorted = data.sort((a, b) => a[key] - b[key]);
  renderTable(sorted);
}

button.addEventListener('click', () => {
  sortTable(largeData, 'price');
});

// 问题:
// - 排序操作阻塞主线程(~500ms)
// - 用户点击按钮后,需要等待 500ms 才看到反馈

// ✅ 优化方案 1:立即反馈 + 异步处理
button.addEventListener('click', async () => {
  // 立即显示加载状态
  showLoading();
  
  // 让出主线程
  await new Promise(resolve => setTimeout(resolve, 0));
  
  // 执行排序
  const sorted = largeData.sort((a, b) => a.price - b.price);
  
  // 更新 UI
  renderTable(sorted);
  hideLoading();
});

// ✅ 优化方案 2:使用 Web Worker
// main.js
button.addEventListener('click', () => {
  showLoading();
  
  const worker = new Worker('sort-worker.js');
  worker.postMessage({ data: largeData, key: 'price' });
  
  worker.onmessage = (event) => {
    renderTable(event.data);
    hideLoading();
    worker.terminate();
  };
});

// sort-worker.js
self.onmessage = (event) => {
  const { data, key } = event.data;
  const sorted = data.sort((a, b) => a[key] - b[key]);
  self.postMessage(sorted);
};

// ✅ 优化方案 3:虚拟滚动(如果是长列表)
// 只渲染可见部分,减少 DOM 操作

场景 5:优化 CLS(减少布局偏移)

html 复制代码
// 问题:新闻网站的文章页,广告加载导致内容跳动

// ❌ 原始代码
<div class="article">
  <h1>文章标题</h1>
  <div id="ad-slot"></div>  <!-- 广告位 -->
  <p>文章内容...</p>
</div>

<script>
  // 2 秒后加载广告
  setTimeout(() => {
    const ad = document.getElementById('ad-slot');
    ad.innerHTML = '<img src="ad.jpg" width="728" height="90">';
  }, 2000);
  // 问题:广告出现时,文章内容被推下去
</script>

// ✅ 优化方案 1:预留空间
<div class="article">
  <h1>文章标题</h1>
  <div id="ad-slot" style="min-height: 90px; width: 728px;">
    <!-- 预留广告位空间 -->
  </div>
  <p>文章内容...</p>
</div>

// ✅ 优化方案 2:使用 CSS aspect-ratio
<style>
  .ad-container {
    width: 100%;
    max-width: 728px;
    aspect-ratio: 728 / 90; /* 自动计算高度 */
    background: #f0f0f0; /* 占位背景色 */
  }
</style>

<div class="article">
  <h1>文章标题</h1>
  <div class="ad-container" id="ad-slot"></div>
  <p>文章内容...</p>
</div>

// ✅ 优化方案 3:使用骨架屏
<div class="article">
  <h1>文章标题</h1>
  <div class="ad-skeleton" id="ad-slot">
    <div class="skeleton-box" style="width: 728px; height: 90px;"></div>
  </div>
  <p>文章内容...</p>
</div>

<style>
  .skeleton-box {
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: loading 1.5s infinite;
  }
  
  @keyframes loading {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
</style>

延伸与发散

1. 实验室数据 vs 真实用户数据

arduino 复制代码
// 实验室数据(Lab Data):
// - 工具:Lighthouse、WebPageTest
// - 环境:固定的设备、网络条件
// - 优点:可重复、可控
// - 缺点:不代表真实用户体验

// 真实用户数据(RUM: Real User Monitoring):
// - 来源:真实用户的浏览器
// - 环境:各种设备、网络条件
// - 优点:反映真实情况
// - 缺点:数据噪音大、难以复现问题

// 最佳实践:两者结合
// 1. 开发阶段:使用 Lighthouse 发现问题
// 2. 上线后:使用 RUM 监控真实性能
// 3. 发现问题:使用 Lighthouse 重现和调试

2. 性能预算(Performance Budget)

javascript 复制代码
// 性能预算是一种约束机制
// 为项目设定性能目标,超出目标时自动报警或阻止部署

// 示例:package.json 中的性能预算配置
{
  "budgets": [
    {
      "type": "bundle",
      "name": "main",
      "baseline": "500KB",
      "warning": "600KB",
      "error": "800KB"
    },
    {
      "type": "initial",
      "baseline": "1MB",
      "warning": "1.2MB",
      "error": "1.5MB"
    }
  ]
}

// Webpack 插件:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
    }),
  ],
  performance: {
    maxAssetSize: 500000, // 500KB
    maxEntrypointSize: 800000, // 800KB
    hints: 'error', // 超出预算时报错
  },
};

3. 移动端性能优化

arduino 复制代码
// 移动端的特殊考虑:

// 1. 网络条件差
// - 使用 3G/4G 网络测试
// - 减少资源大小
// - 使用 HTTP/2 多路复用

// 2. CPU 性能弱
// - 减少 JavaScript 执行时间
// - 避免复杂的 CSS 选择器
// - 使用 GPU 加速(transform, opacity)

// 3. 电池续航
// - 减少网络请求
// - 避免频繁的动画
// - 使用 Intersection Observer 代替 scroll 事件

// Chrome DevTools 中模拟移动设备:
// 1. 打开 DevTools → 切换到移动设备模式(Ctrl+Shift+M)
// 2. 选择设备(iPhone, Pixel 等)
// 3. 设置网络条件(Fast 3G, Slow 3G)
// 4. 设置 CPU 限制(4x slowdown)

4. 渐进式 Web 应用(PWA)与性能

javascript 复制代码
// PWA 的性能优势:

// 1. Service Worker 缓存
// - 离线可用
// - 瞬时加载

// 2. App Shell 架构
// - 缓存应用外壳
// - 动态加载内容

// 示例:缓存应用外壳
// service-worker.js
const CACHE_NAME = 'app-shell-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png',
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

知识点快速回顾

(30 秒版本)

Q: 什么是 Core Web Vitals?包含哪些指标?

A: Core Web Vitals 是 Google 推出的三个核心性能指标:

  • LCP(Largest Contentful Paint) :最大内容绘制时间,测量加载性能,目标 < 2.5s
  • INP(Interaction to Next Paint) :交互响应时间,测量交互性能,目标 < 200ms(之前是 FID)
  • CLS(Cumulative Layout Shift) :累积布局偏移,测量视觉稳定性,目标 < 0.1

Q: 如何测量网站性能?

A:

  • 实验室数据:使用 Lighthouse、Chrome DevTools Performance 面板、WebPageTest
  • 真实用户数据:使用 Performance API 收集指标,上报到后端分析
  • 两者结合:开发阶段用 Lighthouse,上线后监控真实用户数据

(2 分钟版本)

Q: 如何优化 LCP?

A: 常见优化手段:

  1. 图片优化:压缩、使用 WebP/AVIF、响应式图片、CDN
  2. 预加载关键资源<link rel="preload"> 提高优先级
  3. 减少阻塞资源:CSS 内联关键样式、JS 使用 defer/async
  4. 服务端优化:减少 TTFB、使用 SSR/SSG
  5. 字体优化font-display: swap、字体子集化

Q: 什么情况会导致 CLS?如何避免?

A: 常见原因:

  • 图片/视频没有设置尺寸
  • 动态插入内容(广告、提示条)
  • Web 字体加载导致文本重排

优化方案:

  • 为图片设置 widthheight 属性
  • 使用 aspect-ratio CSS 属性
  • 为动态内容预留空间(骨架屏)
  • 使用 font-display: optional 避免字体切换

Q: 如何优化 INP(减少交互延迟)?

A:

  1. 减少主线程阻塞:分片处理大任务、使用 Web Worker
  2. 优化事件处理:防抖(debounce)、节流(throttle)
  3. 代码分割:按需加载,减少初始 JavaScript 大小
  4. 避免长任务 :单个任务不超过 50ms,使用 requestIdleCallback

Q: Lighthouse 分数如何计算?各指标权重是多少?

A: Lighthouse Performance 分数(2024 年权重):

  • LCP: 25%
  • TBT (Total Blocking Time): 30%
  • CLS: 25%
  • FCP: 10%
  • Speed Index: 10%

分数计算:每个指标有各自的评分曲线,加权平均后得到总分(0-100)。

有关性能指标与优化的高频关键概念

面试时,回答中尽量涵盖这些关键词:

  • Core Web Vitals
  • LCP / FCP / TTFB
  • INP / FID
  • CLS
  • Lighthouse / Performance API
  • 长任务(Long Task)
  • 主线程阻塞(Main Thread Blocking)
  • 预加载(Preload)
  • 懒加载(Lazy Loading)
  • 骨架屏(Skeleton Screen)
  • 性能预算(Performance Budget)
  • RUM(Real User Monitoring)

容易踩的坑

  1. 过度优化单一指标:盯着 LCP 优化,忽略了 CLS,导致用户体验变差
  2. 只关注实验室数据:Lighthouse 分数很高,但真实用户反馈慢(网络环境差异)
  3. 滥用预加载 :给太多资源添加 preload,反而延迟了其他关键资源
  4. 忽略移动端:桌面端性能好,移动端很差(CPU、网络限制)
  5. 懒加载首屏图片:LCP 元素不应该懒加载,否则加载更慢

性能优化优先级

markdown 复制代码
1. 影响 Core Web Vitals 的问题(优先级最高)
   ├─ LCP > 4s → 立即优化
   ├─ INP > 500ms → 立即优化
   └─ CLS > 0.25 → 立即优化

2. 其他性能问题
   ├─ TTFB > 1s → 服务端优化
   ├─ FCP > 3s → 减少阻塞资源
   └─ Bundle 过大 → 代码分割

3. 体验优化
   ├─ 添加加载状态
   ├─ 骨架屏
   └─ 渐进式渲染

小结

性能指标是连接主观体验和客观数据的桥梁。理解 Core Web Vitals(LCP、INP、CLS)的含义、测量方法和优化手段,能帮助我们系统性地提升 Web 应用性能。

这篇文章主要探讨了:

  • Core Web Vitals 三大核心指标
  • 其他重要性能指标(TTFB、FCP、TTI)
  • 使用 Lighthouse 进行性能分析
  • 实际优化案例(LCP、INP、CLS)
  • 性能监控的实现
  • 性能预算的概念

参考资料

相关推荐
简单不容易2 小时前
vue一次解决监听H5软键盘弹出和收起的兼容问题
javascript·vue.js
Oneslide2 小时前
flex布局实现水平和垂直对齐
前端
滕青山2 小时前
在线图片压缩工具核心JS实现
前端·javascript·vue.js
进击的尘埃2 小时前
低代码组件通信:`EventBus`和响应式数据流,到底该选哪个
javascript
好事发生2 小时前
Elpis-core 学习
前端
代码煮茶2 小时前
Pinia 状态管理实战 | 从 0 到 1 搭建 Vue3 项目状态层(附模块化 / 持久化)
前端·vue.js
angerdream2 小时前
https://editor.csdn.net/md/?articleId=159120195
javascript·vue.js
siger2 小时前
花式玩转TypeScript类型-我使用swagger的描述文件自动生成类型的npm包供前端使用
前端·typescript·npm
用户81274828151202 小时前
kill只是杀进程吗?信号部分实战--系统开发必学linux基础知识
前端