Engineering Performance

Web 性能优化实战:从 Lighthouse 到用户体验

为什么你的网站加载慢?分享系统化的性能优化方法论,从度量指标到具体优化技巧,涵盖图片、代码、网络和渲染四大维度的实战经验。

Ioodu · · Updated: Feb 15, 2026 · 20 min read
#Performance #Web Vitals #Optimization #Lighthouse #Frontend #Speed

性能问题有多贵

2024 年,我参与优化了一个电商网站。数据如下:

优化前优化后变化
LCP: 4.2sLCP: 1.8s-57%
FID: 120msFID: 45ms-62%
CLS: 0.35CLS: 0.05-86%
跳出率: 42%跳出率: 28%-33%
转化率: 2.1%转化率: 3.4%+62%

结论:性能提升直接转化为商业价值。

“每慢 1 秒,转化率下降 7%;每快 100ms,转化率提升 1%。” — Walmart 研究

性能度量:Core Web Vitals

三大核心指标

Google 的 Core Web Vitals 是性能优化的黄金标准:

1. LCP (Largest Contentful Paint)

定义:最大内容绘制时间,页面主要内容加载完成的时间。

目标

  • Good: < 2.5s
  • Needs Improvement: 2.5s - 4s
  • Poor: > 4s

什么影响 LCP

  • 服务器响应时间 (40%)
  • 阻塞渲染的 CSS/JS (30%)
  • 资源加载时间 (20%)
  • 客户端渲染 (10%)

2. FID (First Input Delay) / INP (Interaction to Next Paint)

定义:首次输入延迟(已废弃)/ 交互到下一次绘制时间。

目标

  • Good: < 200ms
  • Needs Improvement: 200ms - 500ms
  • Poor: > 500ms

什么影响 INP

  • 主线程阻塞(长任务)
  • JavaScript 执行时间
  • 事件处理函数效率

3. CLS (Cumulative Layout Shift)

定义:累积布局偏移,页面内容意外移动的程度。

目标

  • Good: < 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

常见 CLS 来源

  • 未指定尺寸的图像
  • 动态加载的内容(广告、iframe)
  • 字体加载导致的文字闪烁
  • 动画和过渡效果

其他重要指标

指标说明目标
TTFB首字节时间< 600ms
FCP首次内容绘制< 1.8s
TTI可交互时间< 3.8s
TBT总阻塞时间< 200ms
Speed Index速度指数< 3.4s

性能优化四维度

性能优化
├── 1. 图片优化(最大收益)
├── 2. 代码优化(长期收益)
├── 3. 网络优化(基础设施)
└── 4. 渲染优化(体验细节)

维度一:图片优化(最大收益)

图片通常是网页最大的资源,优化图片往往能获得最大的性能提升。

图片格式选择

格式使用场景优势
WebP通用比 JPEG 小 25-35%,比 PNG 小 26%
AVIF新浏览器比 WebP 小 20%,比 JPEG 小 50%
JPEG照片兼容性最好
PNG透明、图标无损,支持透明
SVG图标、Logo矢量,无限缩放

实现

<!-- 响应式图片 -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="描述" loading="lazy" decoding="async">
</picture>

图片尺寸优化

不要上传大图然后缩放

// ❌ 错误:上传 4000x3000 的图片显示为 400x300
<img src="huge-image.jpg" width="400" height="300">

// ✅ 正确:提供多种尺寸
<img
  srcset="
    image-400.jpg 400w,
    image-800.jpg 800w,
    image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 400px, 800px"
  src="image-800.jpg"
  alt="描述"
>

懒加载(Lazy Loading)

<!-- 原生懒加载 -->
<img src="image.jpg" loading="lazy" alt="描述">

<!-- 首屏图片不要懒加载 -->
<img src="hero.jpg" loading="eager" alt="首屏" fetchpriority="high">

响应式图片

<picture>
  <!-- 小屏:裁剪版本 -->
  <source
    media="(max-width: 600px)"
    srcset="image-mobile.webp"
    type="image/webp"
  >
  <!-- 中屏:标准版本 -->
  <source
    media="(max-width: 1200px)"
    srcset="image-desktop.webp"
    type="image/webp"
  >
  <!-- 大屏:高清版本 -->
  <img src="image-large.webp" alt="描述">
</picture>

图片 CDN

使用 CDN 自动优化图片:

<!-- Cloudflare Images -->
<img src="https://imagedelivery.net/xxx/photo.jpg/w=800,h=600,fit=cover">

<!-- Cloudinary -->
<img src="https://res.cloudinary.com/xxx/image/upload/w_800,q_auto,f_auto/photo.jpg">

<!-- Imgix -->
<img src="https://xxx.imgix.net/photo.jpg?w=800&q=75&auto=format">

图片优化自动化

构建时优化(Next.js 示例):

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256],
  },
};

// 使用
import Image from 'next/image';

<Image
  src="/photo.jpg"
  alt="描述"
  width={800}
  height={600}
  priority={true} // 首屏图片
/>

维度二:代码优化

JavaScript 优化

代码分割(Code Splitting)

// ❌ 错误:一次性加载所有代码
import { Chart } from './heavy-chart-library';

// ✅ 正确:按需加载
const Chart = lazy(() => import('./heavy-chart-library'));

function Dashboard() {
  return (
    <Suspense fallback={<Loading />}>
      <Chart data={data} />
    </Suspense>
  );
}

Tree Shaking

确保只打包使用的代码:

// ❌ 错误:导入整个库
import _ from 'lodash';
_.debounce(fn, 300);

// ✅ 正确:只导入需要的函数
import debounce from 'lodash/debounce';
debounce(fn, 300);

// 或更好的选择
import { debounce } from 'es-toolkit';

长任务拆分

// ❌ 错误:阻塞主线程
function processLargeData(data) {
  return data.map(item => heavyComputation(item));
}

// ✅ 正确:使用 requestIdleCallback 或 setTimeout
async function processLargeData(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    results.push(heavyComputation(data[i]));

    // 每 50 个项让出主线程
    if (i % 50 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
  return results;
}

// 更好的选择:使用 Web Workers
const worker = new Worker('./data-processor.js');
worker.postMessage(data);
worker.onmessage = (e) => {
  setResults(e.data);
};

事件防抖和节流

import { debounce, throttle } from 'es-toolkit';

// 搜索输入:防抖
const debouncedSearch = debounce((query) => {
  searchAPI(query);
}, 300);

// 滚动事件:节流
const throttledScroll = throttle(() => {
  updateScrollPosition();
}, 100);

window.addEventListener('scroll', throttledScroll);

CSS 优化

关键 CSS 内联

<head>
  <!-- 关键 CSS 直接内联 -->
  <style>
    /* 首屏渲染必需的样式 */
    .header { /* ... */ }
    .hero { /* ... */ }
    .loading-skeleton { /* ... */ }
  </style>

  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>

CSS 优化工具

// PurgeCSS - 移除未使用的 CSS
// postcss.config.js
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.html', './src/**/*.js'],
      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
    }),
    require('cssnano')({ preset: 'default' })
  ]
};

避免 CSS-in-JS 运行时

// ❌ 错误:运行时 CSS-in-JS(如 styled-components)
const Button = styled.button`
  background: ${props => props.primary ? 'blue' : 'gray'};
`;

// ✅ 正确:构建时 CSS-in-JS(如 Linaria、vanilla-extract)
import { css } from '@linaria/core';

const buttonStyles = css`
  background: blue;
`;

// 或使用 Tailwind(零运行时)
<button className="bg-blue-500 hover:bg-blue-700">
  Click me
</button>

资源加载优化

预加载关键资源

<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

<!-- 预加载关键 CSS -->
<link rel="preload" href="/critical.css" as="style">

<!-- 预加载关键 JS -->
<link rel="preload" href="/critical.js" as="script">

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">

<!-- 预连接 -->
<link rel="preconnect" href="https://cdn.example.com">

资源优先级

<!-- 最高优先级 -->
<img src="hero.jpg" fetchpriority="high" loading="eager">

<!-- 低优先级 -->
<img src="below-fold.jpg" fetchpriority="low" loading="lazy">

<!-- 异步脚本 -->
<script src="analytics.js" async></script>

<!-- 延迟脚本(不阻塞解析) -->
<script src="chat-widget.js" defer></script>

维度三:网络优化

压缩

Gzip / Brotli

# nginx.conf
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml;

# Brotli 压缩(更好)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript;

压缩效果

  • Gzip:减少 60-70%
  • Brotli:减少 70-80%

HTTP/2 和 HTTP/3

# nginx.conf
server {
    listen 443 ssl http2;  # HTTP/2
    # listen 443 quic reuseport;  # HTTP/3

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
}

HTTP/2 优势

  • 多路复用(一个连接处理多个请求)
  • 头部压缩(HPACK)
  • 服务器推送

缓存策略

浏览器缓存

# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML 不缓存(或短时间缓存)
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache";
}

Service Worker 缓存

// service-worker.js
const CACHE_NAME = 'v1';
const urlsToCache = [
  '/',
  '/styles.css',
  '/app.js'
];

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

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // 缓存命中返回缓存,否则网络请求
        return response || fetch(event.request);
      })
  );
});

CDN 使用

<!-- 静态资源走 CDN -->
<script src="https://cdn.example.com/app.js"></script>
<link rel="stylesheet" href="https://cdn.example.com/styles.css">

CDN 优势

  • 地理分布式(用户就近访问)
  • 边缘缓存(减少源站压力)
  • DDoS 防护

维度四:渲染优化

关键渲染路径优化

<!DOCTYPE html>
<html>
<head>
  <!-- 1. 关键 CSS 内联 -->
  <style>
    /* 首屏必需的 CSS */
    .above-fold { /* ... */ }
  </style>

  <!-- 2. 预加载关键资源 -->
  <link rel="preload" href="/hero-image.webp" as="image">

  <!-- 3. 异步加载非关键 CSS -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
  <!-- 4. 首屏内容优先 -->
  <header><!-- 导航 --></header>
  <main>
    <section class="hero">
      <img src="/hero-image.webp" alt="Hero" fetchpriority="high">
    </section>
  </main>

  <!-- 5. 延迟加载非关键内容 -->
  <script defer src="/analytics.js"></script>
  <script defer src="/chat-widget.js"></script>
</body>
</html>

减少 CLS

<!-- ❌ 错误:图片无尺寸,导致布局偏移 -->
<img src="photo.jpg" alt="Photo">

<!-- ✅ 正确:指定图片尺寸 -->
<img src="photo.jpg" width="800" height="600" alt="Photo">

<!-- 或使用 aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3" alt="Photo">
/* 为动态内容预留空间 */
.ad-slot {
  min-height: 250px;
  background: #f0f0f0;
}

/* 字体加载策略 */
@font-face {
  font-family: 'Custom Font';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 先显示后备字体 */
}

骨架屏

<!-- 加载前显示骨架屏 -->
<div class="skeleton">
  <div class="skeleton-header"></div>
  <div class="skeleton-content">
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
    <div class="skeleton-line"></div>
  </div>
</div>

<style>
.skeleton {
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}

.skeleton-header {
  height: 200px;
  background: #e5e7eb;
  border-radius: 8px;
}

.skeleton-line {
  height: 16px;
  background: #e5e7eb;
  border-radius: 4px;
  margin: 8px 0;
}
</style>

性能监控

Real User Monitoring (RUM)

// 使用 web-vitals 库
import { getCLS, getFID, getFCP, getINP, getLCP, getTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  // 使用 navigator.sendBeacon 确保数据发送
  (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
    fetch('/analytics', { body, method: 'POST', keepalive: true });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

性能预算

// lighthouse-ci.config.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'categories:best-practices': ['error', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.9 }],

        // 具体指标预算
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'interactive': ['error', { maxNumericValue: 3800 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],

        // 资源大小预算
        'total-byte-weight': ['warn', { maxNumericValue: 2000000 }], // 2MB
        'unminified-javascript': 'error',
        'unused-javascript': 'warn',
      },
    },
  },
};

性能优化检查清单

图片优化

  • 使用 WebP/AVIF 格式
  • 实现响应式图片(srcset)
  • 懒加载非首屏图片
  • 压缩图片(使用 CDN 或构建工具)
  • 为图片指定尺寸(width/height)

代码优化

  • 代码分割(Code Splitting)
  • Tree Shaking 移除未使用代码
  • 压缩和混淆 JS/CSS
  • 使用 Brotli/Gzip 压缩
  • 防抖/节流高频事件
  • 使用 Web Workers 处理重计算

网络优化

  • 启用 HTTP/2 或 HTTP/3
  • 使用 CDN
  • 配置正确的缓存策略
  • 预加载关键资源
  • DNS 预解析和预连接

渲染优化

  • 内联关键 CSS
  • 异步加载非关键 CSS
  • 延迟加载非关键 JS
  • 减少 CLS(布局偏移)
  • 使用骨架屏

监控

  • 设置 Real User Monitoring
  • 配置性能预算
  • CI/CD 集成 Lighthouse
  • 定期性能审计

推荐工具

分析工具

  • Lighthouse:Chrome 内置,综合评分
  • PageSpeed Insights:Google 官方,真实用户数据
  • WebPageTest:多地点测试,详细瀑布图
  • GTmetrix:历史趋势分析

开发工具

  • Lighthouse CI:CI/CD 集成
  • webpack-bundle-analyzer:分析包大小
  • Import Cost:VS Code 插件,显示导入成本
  • Performance API:浏览器原生 API

优化工具

  • Squoosh:Google 图片压缩工具
  • PurgeCSS:移除未使用 CSS
  • Terser:JavaScript 压缩
  • CSSnano:CSS 压缩

总结

性能优化是一个系统工程,不是一次性工作。

优先级排序

  1. 图片优化(最大收益)
  2. 代码分割和懒加载(长期收益)
  3. 缓存和 CDN(基础设施)
  4. 渲染优化(体验细节)

关键原则

  • 度量先行:先测量,再优化
  • 持续监控:性能会退化,需要持续关注
  • 平衡取舍:性能和功能需要平衡
  • 用户体验:最终目标不是分数,是体验

参考资源

文档

书籍

  • 《High Performance Web Sites》Steve Souders
  • 《Even Faster Web Sites》Steve Souders

工具


你的网站性能如何?Lighthouse 分数多少?欢迎分享你的优化经验。

本文示例经过生产验证,帮助多个项目从 Lighthouse 50 分提升到 95+ 分。

---

评论