Skip to content

网页性能优化指标与实践

目录


1. 加载性能指标

1.1 首字节时间 (TTFB - Time To First Byte)

指标说明

TTFB是指从浏览器发起请求到接收到第一个字节的时间,包括DNS解析、TCP连接、SSL握手、服务器处理等时间。TTFB过长通常表示服务器响应慢或网络延迟高。

理想阈值: ≤800ms

优化案例

案例1:CDN加速配置

nginx
# Nginx CDN回源配置
upstream origin {
    server origin.example.com:443;
}

server {
    listen 443 ssl http2;
    server_name cdn.example.com;

    # HTTP/2 多路复用
    http2_push_preload on;

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
        proxy_pass https://origin;
        proxy_cache_valid 200 304 1y;
        proxy_cache_key "$host$request_uri";
        add_header Cache-Control "public, max-age=31536000, immutable";
        expires 1y;
    }

    # HTML不缓存
    location ~* \.html$ {
        proxy_pass https://origin;
        proxy_cache_valid 200 5m;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
}

案例2:Node.js Gzip压缩与响应优化

javascript
const express = require('express')
const compression = require('compression')
const helmet = require('helmet')
const app = express()

// 安全头配置
app.use(helmet())

// 智能压缩配置
app.use(
  compression({
    level: 6,
    threshold: 1024,
    filter: (req, res) => {
      const type = req.headers['accept-encoding']
      if (!type) return false
      if (req.headers['x-no-compression']) return false
      return compression.filter(req, res)
    }
  })
)

// 数据库连接池优化
const mysql = require('mysql2/promise')
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'mydb',
  waitForConnections: true,
  connectionLimit: 20,
  queueLimit: 0
})

// 查询缓存示例
const LRU = require('lru-cache')
const cache = new LRU({ max: 500, ttl: 60000 })

app.get('/api/data', async (req, res) => {
  const cacheKey = `data:${req.query.id}`
  if (cache.has(cacheKey)) {
    return res.json(cache.get(cacheKey))
  }

  const [rows] = await pool.execute('SELECT * FROM data WHERE id = ?', [
    req.query.id
  ])
  cache.set(cacheKey, rows)
  res.json(rows)
})

案例3:Redis缓存与数据库优化

sql
-- 添加复合索引
CREATE INDEX idx_user_created_status ON users(created_at, status);

-- 查询优化
EXPLAIN ANALYZE
SELECT * FROM users
WHERE created_at > '2024-01-01'
AND status = 'active';

-- 分区表(大数据量场景)
CREATE TABLE orders (
    id INT,
    order_date DATE,
    amount DECIMAL(10,2)
)
PARTITION BY RANGE (TO_DAYS(order_date)) (
    PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
    PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
    PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01'))
);
javascript
// Redis 缓存示例
const redis = require('redis')
const client = redis.createClient()

async function getProductData(productId) {
  const cacheKey = `product:${productId}`

  let data = await client.get(cacheKey)
  if (data) {
    return JSON.parse(data)
  }

  data = await db.query('SELECT * FROM products WHERE id = ?', [productId])
  await client.setEx(cacheKey, 3600, JSON.stringify(data))

  return data
}

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

指标说明

FCP是指从页面加载开始到浏览器渲染出第一个DOM元素(文本、图片、canvas等)的时间。它标志着用户开始看到页面内容。

理想阈值: ≤1.8s

优化案例

案例1:内联关键CSS

html
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- 内联首屏关键CSS -->
  <style>
    /* Reset */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    /* 字体系统 */
    body {
      font-family:
        -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui,
        sans-serif;
      line-height: 1.5;
      color: #333;
    }

    /* 头部样式 */
    .header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 1rem 2rem;
      position: sticky;
      top: 0;
      z-index: 100;
    }

    .nav {
      display: flex;
      justify-content: space-between;
      align-items: center;
      max-width: 1200px;
      margin: 0 auto;
    }

    /* Hero区域 */
    .hero {
      min-height: 60vh;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      padding: 4rem 2rem;
      background: #f8fafc;
    }

    .hero-title {
      font-size: 3rem;
      font-weight: 700;
      margin-bottom: 1rem;
    }

    .hero-subtitle {
      font-size: 1.25rem;
      color: #64748b;
      margin-bottom: 2rem;
    }

    /* 按钮样式 */
    .btn {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.75rem 2rem;
      background: #667eea;
      color: white;
      text-decoration: none;
      border-radius: 8px;
      font-weight: 600;
      transition:
        transform 0.2s,
        box-shadow 0.2s;
    }

    .btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
    }
  </style>

  <!-- 预加载关键资源 -->
  <link
    rel="preload"
    href="/fonts/inter.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
  <link rel="preload" href="/images/hero-bg.jpg" as="image" />

  <!-- 非关键CSS延迟加载 -->
  <link
    rel="preload"
    href="/styles/main.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript><link rel="stylesheet" href="/styles/main.css" /></noscript>
</head>

案例2:资源预加载优化

html
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<link rel="dns-prefetch" href="//analytics.example.com" />

<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://api.example.com" />

<!-- 预加载 -->
<link rel="preload" href="/critical.js" as="script" />
<link rel="preload" href="/hero.webp" as="image" />
<link rel="preload" href="/styles/critical.css" as="style" />

<!-- 预获取(用于下一页可能需要的资源) -->
<link rel="prefetch" href="/page2.html" as="document" />
<link rel="prefetch" href="/page2-styles.css" as="style" />

案例3:Web字体优化

css
/* 字体加载优化 */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src:
    url('/fonts/inter-regular.woff2') format('woff2'),
    url('/fonts/inter-regular.woff') format('woff');
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/fonts/inter-semibold.woff2') format('woff2');
}

/* 字体子集化 - 只包含所需字符 */
@font-face {
  font-family: 'IconFont';
  font-display: block;
  src: url('/fonts/icons.woff2') format('woff2');
  unicode-range: U+F000-F0FF;
}
javascript
// 字体加载监控
if ('fonts' in document) {
  document.fonts.ready.then(function () {
    document.documentElement.classList.add('fonts-loaded')
  })
}

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

指标说明

LCP是指视口中最大的内容元素(通常是图片或文本块)完成渲染的时间,是Core Web Vitals的核心指标之一,直接影响用户对页面加载速度的感知。

理想阈值: ≤2.5s 需要改进: 2.5s - 4s 较差: >4s

优化案例

案例1:响应式图片与现代格式

html
<!-- 使用srcset和sizes提供响应式图片 -->
<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg   400w,
    hero-800.jpg   800w,
    hero-1200.jpg 1200w,
    hero-1600.jpg 1600w
  "
  sizes="(max-width: 480px) 400px,
           (max-width: 768px) 800px,
           (max-width: 1200px) 1200px,
           1600px"
  alt="Hero banner"
  loading="eager"
  fetchpriority="high"
  width="1600"
  height="900"
/>

<!-- 使用picture元素支持多种格式 -->
<picture>
  <source
    media="(min-width: 1200px)"
    srcset="hero-large.webp 1x, hero-large@2x.webp 2x"
    type="image/webp"
  />
  <source
    media="(min-width: 768px)"
    srcset="hero-medium.webp 1x, hero-medium@2x.webp 2x"
    type="image/webp"
  />
  <source
    srcset="hero-small.webp 1x, hero-small@2x.webp 2x"
    type="image/webp"
  />
  <source
    media="(min-width: 1200px)"
    srcset="hero-large.jpg 1x, hero-large@2x.jpg 2x"
    type="image/jpeg"
  />
  <img
    src="hero-small.jpg"
    alt="Hero banner"
    loading="eager"
    fetchpriority="high"
    width="800"
    height="450"
  />
</picture>

案例2:Next.js Image组件优化

jsx
import Image from 'next/image'

function Hero() {
  return (
    <section className='hero'>
      <Image
        src='/images/hero.jpg'
        alt='Hero banner'
        fill
        priority
        placeholder='blur'
        blurDataURL='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...'
        sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
        quality={85}
      />
      <div className='hero-content'>
        <h1>Welcome to Our Site</h1>
        <p>Discover amazing products</p>
      </div>
    </section>
  )
}

案例3:SSR/SSG优化LCP内容

javascript
// Next.js 服务端渲染
export async function getServerSideProps(context) {
  const [productRes, bannerRes] = await Promise.all([
    fetch('https://api.example.com/products/featured'),
    fetch('https://api.example.com/banners/home')
  ])

  const product = await productRes.json()
  const banner = await bannerRes.json()

  return {
    props: {
      product,
      banner
    }
  }
}

export default function Home({ product, banner }) {
  return (
    <main>
      <section className='banner'>
        <img
          src={banner.image}
          alt={banner.title}
          width={banner.width}
          height={banner.height}
          loading='eager'
        />
        <h1>{banner.title}</h1>
      </section>

      <section className='featured-product'>
        <h2>Featured Product</h2>
        <h3>{product.name}</h3>
        <p>{product.description}</p>
      </section>
    </main>
  )
}

1.4 首屏加载时间

指标说明

首屏加载时间是指页面首屏内容(用户无需滚动即可看到的内容)全部加载完成的时间,是用户感知最明显的指标之一。

理想阈值: ≤3s

优化案例

案例1:React代码分割与懒加载

jsx
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'

// 首屏组件直接导入
import Header from './components/Header'
import Hero from './components/Hero'
import Footer from './components/Footer'

// 非首屏组件懒加载
const ProductList = lazy(() => import('./components/ProductList'))
const FeatureSection = lazy(() => import('./components/FeatureSection'))
const Testimonials = lazy(() => import('./components/Testimonials'))
const BlogSection = lazy(() => import('./components/BlogSection'))

// 加载占位组件
function LoadingSkeleton() {
  return (
    <div className='loading-skeleton'>
      <div className='skeleton-header'></div>
      <div className='skeleton-grid'>
        {[1, 2, 3].map(i => (
          <div key={i} className='skeleton-card'></div>
        ))}
      </div>
    </div>
  )
}

function App() {
  return (
    <Router>
      <Header />
      <Hero />

      <Suspense fallback={<LoadingSkeleton />}>
        <ProductList />
      </Suspense>

      <Suspense fallback={<LoadingSkeleton />}>
        <FeatureSection />
      </Suspense>

      <Suspense fallback={<LoadingSkeleton />}>
        <Testimonials />
      </Suspense>

      <Suspense fallback={<LoadingSkeleton />}>
        <BlogSection />
      </Suspense>

      <Footer />
    </Router>
  )
}

export default App

案例2:图片懒加载实现

html
<!-- 原生懒加载(现代浏览器支持) -->
<img
  src="product-1.jpg"
  loading="lazy"
  alt="Product 1"
  width="400"
  height="400"
/>

<!-- Intersection Observer 懒加载(兼容性更好) -->
<img
  data-src="product-2.jpg"
  class="lazy"
  alt="Product 2"
  width="400"
  height="400"
/>

<script>
  document.addEventListener('DOMContentLoaded', function () {
    const lazyImages = document.querySelectorAll('img.lazy')

    if ('IntersectionObserver' in window) {
      const imageObserver = new IntersectionObserver(
        (entries, observer) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              const img = entry.target
              img.src = img.dataset.src

              img.onload = function () {
                img.classList.add('loaded')
              }

              observer.unobserve(img)
            }
          })
        },
        {
          rootMargin: '200px 0px',
          threshold: 0.01
        }
      )

      lazyImages.forEach(img => {
        imageObserver.observe(img)
      })
    } else {
      lazyImages.forEach(img => {
        img.src = img.dataset.src
      })
    }
  })
</script>

<style>
  img.lazy {
    background: #f0f0f0;
    min-height: 200px;
    transition: opacity 0.3s;
    opacity: 0;
  }

  img.lazy.loaded {
    opacity: 1;
  }
</style>

案例3:骨架屏实现

html
<!-- 骨架屏组件 -->
<div class="product-card skeleton">
  <div class="skeleton-image"></div>
  <div class="skeleton-content">
    <div class="skeleton-title"></div>
    <div class="skeleton-text"></div>
    <div class="skeleton-text"></div>
    <div class="skeleton-price"></div>
  </div>
</div>

<style>
  /* 骨架屏动画 */
  @keyframes skeleton-pulse {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0.5;
    }
  }

  .skeleton {
    animation: skeleton-pulse 1.5s ease-in-out infinite;
  }

  .skeleton-image {
    width: 100%;
    height: 200px;
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.5s infinite;
    border-radius: 8px;
  }

  @keyframes skeleton-shimmer {
    0% {
      background-position: 200% 0;
    }
    100% {
      background-position: -200% 0;
    }
  }

  .skeleton-title {
    height: 24px;
    width: 80%;
    background: #f0f0f0;
    border-radius: 4px;
    margin-bottom: 12px;
  }

  .skeleton-text {
    height: 16px;
    width: 100%;
    background: #f0f0f0;
    border-radius: 4px;
    margin-bottom: 8px;
  }

  .skeleton-price {
    height: 20px;
    width: 40%;
    background: #f0f0f0;
    border-radius: 4px;
    margin-top: 16px;
  }
</style>

1.5 DOMContentLoaded时间

指标说明

DOMContentLoaded事件在初始HTML文档被完全加载和解析后触发,不等待样式表、图片和子框架加载完成。

理想阈值: ≤2s

优化案例

案例1:JavaScript异步加载策略

html
<!-- async - 异步加载,下载完立即执行,不保证顺序 -->
<script src="analytics.js" async></script>

<!-- defer - 延迟加载,在DOMContentLoaded前按顺序执行 -->
<script src="lib.js" defer></script>
<script src="app.js" defer></script>

<!-- 动态加载脚本 -->
<script>
  document.addEventListener('DOMContentLoaded', function () {
    function loadScript(src, callback) {
      const script = document.createElement('script')
      script.src = src
      script.onload = callback
      document.body.appendChild(script)
    }

    loadScript('analytics.js')
    loadScript('chat-widget.js')
    loadScript('heatmap.js')
  })
</script>

<!-- type="module" 自动延迟执行 -->
<script type="module" src="app.js"></script>

案例2:CSS加载优化

html
<head>
  <!-- 关键CSS内联 -->
  <style>
    body {
      font-family: system-ui;
      margin: 0;
    }
    .header {
      background: #333;
      color: white;
      padding: 1rem;
    }
  </style>

  <!-- 首屏CSS -->
  <link rel="stylesheet" href="critical.css" media="all" />

  <!-- 非首屏CSS - 使用media="print" trick -->
  <link
    rel="stylesheet"
    href="non-critical.css"
    media="print"
    onload="this.media='all'"
  />
  <noscript><link rel="stylesheet" href="non-critical.css" /></noscript>

  <!-- 预加载方式 -->
  <link
    rel="preload"
    href="styles.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript><link rel="stylesheet" href="styles.css" /></noscript>
</head>

1.6 Load时间

指标说明

Load事件在所有资源(包括图片、样式表、脚本、iframe等)都加载完成后触发。

理想阈值: ≤4s

优化案例

案例1:Webpack资源压缩配置

javascript
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin')

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024
          }
        }
      }
    ]
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true
          },
          format: {
            comments: false
          }
        },
        extractComments: false
      }),
      new CssMinimizerPlugin(),
      new ImageMinimizerPlugin({
        minimizer: {
          implementation: ImageMinimizerPlugin.sharpMinify,
          options: {
            encodeOptions: {
              jpeg: { quality: 80 },
              png: { quality: 80 },
              webp: { quality: 80 }
            }
          }
        }
      })
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css'
    })
  ]
}

2. 渲染性能指标

2.1 首次有意义绘制 (FMP - First Meaningful Paint)

指标说明

FMP是指页面主要内容出现在屏幕上的时间。虽然已被LCP替代,但仍有参考价值。

理想阈值: ≤2s

优化案例

案例1:Next.js服务端渲染

jsx
// pages/index.js
import React from 'react'
import Head from 'next/head'

export default function Home({ posts, featuredProduct }) {
  return (
    <div>
      <Head>
        <title>My E-commerce Site</title>
      </Head>

      <main>
        <section className='hero'>
          <h1>Welcome to Our Store</h1>
          <img
            src={featuredProduct.image}
            alt={featuredProduct.name}
            width={600}
            height={400}
            priority
          />
          <h2>{featuredProduct.name}</h2>
          <p>{featuredProduct.description}</p>
        </section>

        <section className='blog-posts'>
          <h2>Latest Posts</h2>
          {posts.map(post => (
            <article key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.excerpt}</p>
            </article>
          ))}
        </section>
      </main>
    </div>
  )
}

export async function getServerSideProps() {
  const [postsRes, productRes] = await Promise.all([
    fetch('https://api.example.com/posts?limit=3'),
    fetch('https://api.example.com/products/featured')
  ])

  const posts = await postsRes.json()
  const featuredProduct = await productRes.json()

  return {
    props: {
      posts,
      featuredProduct
    }
  }
}

案例2:静态生成 (SSG)

jsx
// pages/products/[id].js
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()

  const paths = products.map(product => ({
    params: { id: product.id.toString() }
  }))

  return { paths, fallback: 'blocking' }
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`)
  const product = await res.json()

  return {
    props: { product },
    revalidate: 60
  }
}

2.2 可交互时间 (TTI - Time To Interactive)

指标说明

TTI是指页面变得完全可交互的时间,即主线程长期空闲,用户输入能得到及时响应。

理想阈值: ≤3s

优化案例

案例1:Web Worker处理耗时任务

javascript
// worker.js - 单独文件
self.onmessage = function (e) {
  const { type, data } = e.data

  switch (type) {
    case 'PROCESS_DATA':
      const result = heavyComputation(data)
      self.postMessage({ type: 'DATA_PROCESSED', result })
      break
    case 'SORT_ARRAY':
      const sorted = data.sort((a, b) => a.value - b.value)
      self.postMessage({ type: 'ARRAY_SORTED', sorted })
      break
  }
}

function heavyComputation(data) {
  let result = 0
  for (let i = 0; i < 10000000; i++) {
    result += Math.sqrt(data * i)
  }
  return result
}

// main.js
class DataProcessor {
  constructor() {
    this.worker = new Worker('/worker.js')
    this.callbacks = new Map()
    this.callbackId = 0

    this.worker.onmessage = e => {
      const { callbackId, ...result } = e.data
      const callback = this.callbacks.get(callbackId)
      if (callback) {
        callback(result)
        this.callbacks.delete(callbackId)
      }
    }
  }

  process(data, callback) {
    const id = this.callbackId++
    this.callbacks.set(id, callback)
    this.worker.postMessage({ type: 'PROCESS_DATA', data, callbackId: id })
  }
}

const processor = new DataProcessor()
processor.process(100, result => {
  console.log('处理完成:', result)
})

案例2:使用requestIdleCallback延迟非关键任务

javascript
function scheduleIdleTasks() {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(
      deadline => {
        while (deadline.timeRemaining() > 0 && tasks.length > 0) {
          const task = tasks.shift()
          task()
        }

        if (tasks.length > 0) {
          scheduleIdleTasks()
        }
      },
      { timeout: 2000 }
    )
  } else {
    setTimeout(() => {
      tasks.forEach(task => task())
    }, 1000)
  }
}

const tasks = [
  () => loadAnalytics(),
  () => loadChatWidget(),
  () => loadThirdPartyAds(),
  () => prefetchNextPageData(),
  () => initializeNonCriticalComponents()
]

window.addEventListener('load', () => {
  scheduleIdleTasks()
})

案例3:长任务拆分

javascript
function processLargeArray(array, processItem, onComplete) {
  const chunkSize = 100
  let index = 0
  const results = []

  function processChunk() {
    const end = Math.min(index + chunkSize, array.length)

    for (let i = index; i < end; i++) {
      results.push(processItem(array[i], i))
    }

    index = end

    if (index < array.length) {
      requestAnimationFrame(processChunk)
    } else {
      onComplete(results)
    }
  }

  processChunk()
}

const largeArray = Array.from({ length: 10000 }, (_, i) => i)

processLargeArray(
  largeArray,
  item => item * 2,
  results => {
    console.log('处理完成,结果数量:', results.length)
  }
)

2.3 总阻塞时间 (TBT - Total Blocking Time)

指标说明

TBT是FCP和TTI之间所有长任务(超过50ms)的阻塞时间总和。长任务超过50ms的部分算作阻塞时间。

理想阈值: ≤200ms

优化案例

案例1:动态导入与代码分割

javascript
// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 25,
      maxAsyncRequests: 25,
      minSize: 20000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true
        },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20
        },
        lodash: {
          test: /[\\/]node_modules[\\/]lodash[\\/]/,
          name: 'lodash',
          chunks: 'all',
          priority: 10
        }
      }
    }
  }
}

// 组件中动态导入
function ChartComponent() {
  const [Chart, setChart] = React.useState(null)

  React.useEffect(() => {
    import('d3').then(d3 => {
      setChart(() => d3)
    })
  }, [])

  if (!Chart) {
    return <div>Loading chart...</div>
  }

  return <div>Chart rendered with D3</div>
}

案例2:React性能优化

jsx
import React, { memo, useMemo, useCallback, useState } from 'react'

const ExpensiveListItem = memo(function ExpensiveListItem({ item, onClick }) {
  console.log('ExpensiveListItem rendered')
  return <li onClick={() => onClick(item.id)}>{item.name}</li>
})

function App() {
  const [items, setItems] = useState([])
  const [count, setCount] = useState(0)

  const sortedItems = useMemo(() => {
    console.log('Sorting items...')
    return [...items].sort((a, b) => a.name.localeCompare(b.name))
  }, [items])

  const handleItemClick = useCallback(id => {
    console.log('Item clicked:', id)
  }, [])

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>

      <ul>
        {sortedItems.map(item => (
          <ExpensiveListItem
            key={item.id}
            item={item}
            onClick={handleItemClick}
          />
        ))}
      </ul>
    </div>
  )
}

2.4 帧率 (FPS - Frames Per Second)

指标说明

帧率是指每秒渲染的帧数,理想值是60fps(每帧16.67ms),最低应保持30fps以上以保证流畅。

理想阈值: ≥60fps

优化案例

案例1:使用transform和opacity进行动画

css
/* 好的做法:使用transform和opacity */
.slide-in {
  transform: translateX(0);
  opacity: 1;
  transition:
    transform 0.3s ease,
    opacity 0.3s ease;
}

.slide-out {
  transform: translateX(-100%);
  opacity: 0;
  transition:
    transform 0.3s ease,
    opacity 0.3s ease;
}

/* GPU加速提示 */
.gpu-accelerated {
  transform: translateZ(0);
  backface-visibility: hidden;
  will-change: transform, opacity;
}

/* 不好的做法:使用top/left/width等触发布局重排 */
.bad-animation {
  position: relative;
  left: 0;
  transition: left 0.3s ease;
}

案例2:requestAnimationFrame动画

javascript
function smoothAnimation() {
  let start = null
  const element = document.querySelector('.animate-me')
  const duration = 1000

  function step(timestamp) {
    if (!start) start = timestamp
    const progress = Math.min((timestamp - start) / duration, 1)

    const easeOutCubic = 1 - Math.pow(1 - progress, 3)
    const translateX = easeOutCubic * 300

    element.style.transform = `translateX(${translateX}px)`

    if (progress < 1) {
      requestAnimationFrame(step)
    }
  }

  requestAnimationFrame(step)
}

// 滚动性能优化
let ticking = false

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateScrollPosition()
      ticking = false
    })
    ticking = true
  }
})

function updateScrollPosition() {
  const scrollY = window.scrollY
  const header = document.querySelector('.header')

  if (scrollY > 100) {
    header.classList.add('header--scrolled')
  } else {
    header.classList.remove('header--scrolled')
  }
}

案例3:CSS will-change优化

css
/* 提前告知浏览器哪些属性会变化 */
.animated-element {
  will-change: transform, opacity;
}

/* 为频繁滚动的元素启用 */
.scroll-container {
  will-change: scroll-position;
  overflow-y: auto;
}

3. 交互性能指标

3.1 首次输入延迟 (FID - First Input Delay)

指标说明

FID是指用户首次与页面交互(点击、触摸等)到浏览器实际响应的时间。已被INP替代。

理想阈值: ≤100ms

优化案例

案例1:减少事件监听开销

javascript
// 使用事件委托
document.addEventListener(
  'click',
  e => {
    const button = e.target.closest('.button')
    if (button) {
      handleButtonClick(button)
    }

    const link = e.target.closest('.nav-link')
    if (link) {
      handleNavigation(link)
    }
  },
  { passive: true }
)

// 使用passive监听器
document.addEventListener('touchstart', handleTouchStart, { passive: true })
document.addEventListener('touchmove', handleTouchMove, { passive: true })

// 移除不需要的事件监听器
function Component() {
  const [data, setData] = useState(null)

  useEffect(() => {
    const handleScroll = () => {
      console.log('Scrolling')
    }

    window.addEventListener('scroll', handleScroll, { passive: true })

    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])

  return <div>{data}</div>
}

3.2 交互到下一步绘制 (INP - Interaction to Next Paint)

指标说明

INP是指从用户交互到下一帧绘制的时间,是替代FID的新指标,考虑所有交互而非仅首次。

理想阈值: ≤200ms 需要改进: 200ms - 500ms 较差: >500ms

优化案例

案例1:分阶段响应交互

javascript
async function handleCheckoutClick(e) {
  e.preventDefault()

  const startTime = performance.now()

  const button = e.currentTarget
  button.disabled = true
  button.textContent = 'Processing...'

  await new Promise(resolve => requestAnimationFrame(resolve))

  try {
    const cartData = getCartData()

    const [orderRes, inventoryRes] = await Promise.all([
      fetch('/api/orders', {
        method: 'POST',
        body: JSON.stringify(cartData)
      }),
      fetch('/api/inventory/reserve', {
        method: 'POST',
        body: JSON.stringify(cartData)
      })
    ])

    const order = await orderRes.json()

    button.textContent = 'Success!'
    button.classList.add('success')

    setTimeout(() => {
      window.location.href = `/order-confirmation/${order.id}`
    }, 500)
  } catch (error) {
    button.disabled = false
    button.textContent = 'Try Again'
    showError(error.message)
  }

  const endTime = performance.now()
  console.log(`交互处理时间: ${endTime - startTime}ms`)
}

案例2:防抖和节流

javascript
function debounce(func, wait = 300) {
  let timeout
  return function (...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func.apply(this, args), wait)
  }
}

function throttle(func, limit = 100) {
  let inThrottle
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => (inThrottle = false), limit)
    }
  }
}

const searchInput = document.querySelector('#search')
searchInput.addEventListener(
  'input',
  debounce(e => {
    performSearch(e.target.value)
  }, 300)
)

const resizeHandler = throttle(() => {
  updateLayout()
}, 100)

window.addEventListener('resize', resizeHandler)

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

指标说明

CLS测量整个页面生命周期中发生的所有意外布局偏移的分数总和。

理想阈值: ≤0.1 需要改进: 0.1 - 0.25 较差: >0.25

优化案例

案例1:为媒体元素预留空间

html
<!-- 为图片设置width和height -->
<img
  src="product.jpg"
  alt="Product"
  width="600"
  height="400"
  style="width: 100%; height: auto;"
/>

<!-- 使用aspect-ratio -->
<div class="image-container">
  <img src="product.jpg" alt="Product" />
</div>

<style>
  .image-container {
    aspect-ratio: 16 / 9;
    width: 100%;
    background: #f0f0f0;
  }

  .image-container img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
</style>

<!-- 广告位预留 -->
<div class="ad-slot" style="min-height: 250px; min-width: 300px;">
  <div id="ad-container"></div>
</div>

案例2:避免动态内容插入

html
<!-- 横幅提示使用固定位置 -->
<div
  class="banner"
  style="position: fixed; top: 0; left: 0; right: 0; z-index: 1000;"
>
  Important announcement!
</div>

<!-- 为字体设置fallback -->
<style>
  body {
    font-family:
      'MyWebFont',
      -apple-system,
      BlinkMacSystemFont,
      sans-serif;
  }

  @font-face {
    font-family: 'MyWebFont';
    font-display: swap;
    src: url('myfont.woff2') format('woff2');
  }
</style>

4. 资源性能指标

4.1 资源加载时间

指标说明

单个资源从请求到加载完成的时间。

优化案例

案例1:HTTP/2与Server Push

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

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

    http2_push_preload on;

    location / {
        root /var/www/html;
        index index.html;

        add_header Link "</styles.css>; rel=preload; as=style";
        add_header Link "</script.js>; rel=preload; as=script";
    }
}

4.2 资源体积

指标说明

所有资源的总大小。

优化案例

案例1:图片优化

javascript
// 使用sharp进行图片处理
const sharp = require('sharp')
const fs = require('fs')

async function optimizeImage(inputPath, outputPath) {
  const image = sharp(inputPath)
  const metadata = await image.metadata()

  let pipeline = image

  if (metadata.width > 1920) {
    pipeline = pipeline.resize(1920)
  }

  await Promise.all([
    pipeline.clone().webp({ quality: 80 }).toFile(`${outputPath}.webp`),
    pipeline
      .clone()
      .jpeg({ quality: 85, mozjpeg: true })
      .toFile(`${outputPath}.jpg`),
    pipeline.clone().avif({ quality: 70 }).toFile(`${outputPath}.avif`)
  ])
}

案例2:Tree Shaking

javascript
// 导入单个模块而非整个库
import { debounce } from 'lodash-es'
import { format, parseISO } from 'date-fns'

// webpack配置
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false
  }
}

4.3 缓存命中率

指标说明

从缓存中加载的资源占总资源的比例。

理想阈值: ≥90%

优化案例

案例1:Nginx缓存配置

nginx
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;

server {
    location / {
        proxy_pass http://backend;
        proxy_cache my_cache;
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

案例2:Service Worker缓存

javascript
const CACHE_NAME = 'my-cache-v1'
const ASSETS = ['/', '/styles.css', '/script.js', '/favicon.ico']

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

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

5. Web Vitals指标

Web Vitals是Google定义的用户体验核心指标:

指标良好阈值需要改进较差
LCP≤2.5s2.5s-4s>4s
FID≤100ms100ms-300ms>300ms
CLS≤0.10.1-0.25>0.25
INP≤200ms200ms-500ms>500ms
TTFB≤800ms800ms-1800ms>1800ms

6. 优化案例

完整优化流程案例

优化前分析:

LCP: 4.2s
FID: 280ms
CLS: 0.4
TTFB: 1.2s
总资源大小: 3.8MB

优化步骤:

  1. 图片优化

    • 转换为WebP格式,体积减少60%
    • 实现响应式图片
    • 预加载LCP图片
  2. JavaScript优化

    • 代码分割,首屏JS减少70%
    • Web Worker处理计算任务
    • 延迟加载非关键脚本
  3. CSS优化

    • 内联关键CSS (约15KB)
    • 延迟加载非关键CSS
    • 移除未使用的CSS
  4. 服务器优化

    • 启用CDN,TTFB降至300ms
    • 配置缓存策略
    • 启用Brotli压缩
  5. 布局优化

    • 为图片预留空间
    • 使用font-display: swap
    • 固定定位横幅

优化后结果:

LCP: 1.8s (提升57%)
FID: 50ms (提升82%)
CLS: 0.05 (提升87%)
TTFB: 300ms (提升75%)
总资源大小: 1.2MB (减少68%)

基于 VitePress 的本地知识库