前端性能优化:从加载到渲染的全链路提升

掌握 Core Web Vitals、资源优化、渲染性能、缓存策略和监控体系

前端性能优化:从加载到渲染的全链路提升

前端性能直接影响用户体验和业务转化。本文深入探讨前端性能优化的核心策略和最佳实践。

Core Web Vitals

核心指标

Core Web Vitals 指标:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   LCP (Largest Contentful Paint)                   │
│   ├── 最大内容绘制时间                              │
│   ├── 目标:< 2.5s                                  │
│   └── 影响:图片、字体、大型文本块                  │
│                                                     │
│   INP (Interaction to Next Paint)                  │
│   ├── 交互到下一次绘制                              │
│   ├── 目标:< 200ms                                 │
│   └── 影响:JavaScript 执行、事件处理               │
│                                                     │
│   CLS (Cumulative Layout Shift)                    │
│   ├── 累积布局偏移                                  │
│   ├── 目标:< 0.1                                   │
│   └── 影响:图片尺寸、动态内容、字体加载            │
│                                                     │
└─────────────────────────────────────────────────────┘

测量方法

// 使用 web-vitals 库
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric: Metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  // 使用 sendBeacon 确保数据发送
  navigator.sendBeacon('/api/analytics', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

// Performance Observer API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.startTime}ms`);
  }
});

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

资源加载优化

图片优化

// 现代图片格式和响应式图片
function OptimizedImage({ src, alt }: { src: string; alt: string }) {
  return (
    <picture>
      <source
        srcSet={`${src}.avif 1x, ${src}@2x.avif 2x`}
        type="image/avif"
      />
      <source
        srcSet={`${src}.webp 1x, ${src}@2x.webp 2x`}
        type="image/webp"
      />
      <img
        src={`${src}.jpg`}
        srcSet={`${src}.jpg 1x, ${src}@2x.jpg 2x`}
        alt={alt}
        loading="lazy"
        decoding="async"
        width={800}
        height={600}
      />
    </picture>
  );
}

// 图片预加载
function preloadCriticalImage(src: string) {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'image';
  link.href = src;
  link.fetchPriority = 'high';
  document.head.appendChild(link);
}

字体优化

/* 字体显示策略 */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* 使用系统字体直到自定义字体加载完成 */
  unicode-range: U+0000-00FF; /* 仅加载需要的字符 */
}

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

/* 字体子集化 - 减少字体文件大小 */
/* 使用 fonttools 或 glyphhanger 工具 */

代码分割

// React 懒加载
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// 路由级代码分割
const routes = [
  {
    path: '/dashboard',
    component: lazy(() => import('./pages/Dashboard')),
    preload: () => import('./pages/Dashboard'),
  },
];

// 预加载下一个可能访问的页面
function prefetchRoute(path: string) {
  const route = routes.find(r => r.path === path);
  route?.preload();
}

资源预加载

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

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

<!-- 预加载关键资源 -->
<link rel="preload" href="/critical.css" as="style" />
<link rel="preload" href="/main.js" as="script" />

<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/next-page.js" />

<!-- 模块预加载 -->
<link rel="modulepreload" href="/modules/app.js" />

渲染性能

虚拟列表

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            {items[virtualItem.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

避免布局抖动

// ❌ 触发多次重排
function badLayout() {
  const elements = document.querySelectorAll('.item');
  elements.forEach(el => {
    const height = el.offsetHeight; // 读取
    el.style.height = height + 10 + 'px'; // 写入
  });
}

// ✅ 批量读取后批量写入
function goodLayout() {
  const elements = document.querySelectorAll('.item');
  const heights: number[] = [];

  // 批量读取
  elements.forEach(el => {
    heights.push(el.offsetHeight);
  });

  // 批量写入
  elements.forEach((el, i) => {
    el.style.height = heights[i] + 10 + 'px';
  });
}

// ✅ 使用 requestAnimationFrame
function animateWithRAF() {
  requestAnimationFrame(() => {
    // 在下一帧执行 DOM 更新
    element.style.transform = 'translateX(100px)';
  });
}

React 渲染优化

// 使用 memo 避免不必要的重渲染
const ExpensiveComponent = memo(function ExpensiveComponent({ data }: Props) {
  return <div>{/* 复杂渲染逻辑 */}</div>;
});

// useMemo 缓存计算结果
function DataTable({ items }: { items: Item[] }) {
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
  }, [items]);

  return <Table data={sortedItems} />;
}

// useCallback 缓存回调函数
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return <Child onClick={handleClick} />;
}

// 使用 React.lazy 和 Suspense
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <HeavyComponent />
    </Suspense>
  );
}

缓存策略

Service Worker 缓存

// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
];

// 安装时缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

// 网络优先策略
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(networkFirst(event.request));
  } else {
    event.respondWith(cacheFirst(event.request));
  }
});

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  const cache = await caches.open(CACHE_NAME);
  cache.put(request, response.clone());
  return response;
}

async function networkFirst(request: Request): Promise<Response> {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch (error) {
    const cached = await caches.match(request);
    if (cached) return cached;
    throw error;
  }
}

HTTP 缓存

// Express 缓存头设置
app.use('/static', express.static('public', {
  maxAge: '1y',
  immutable: true,
}));

app.get('/api/data', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
    'ETag': generateETag(data),
  });
  res.json(data);
});

// 版本化资源 URL
const assetUrl = `/app.${hash}.js`;

浏览器存储

// IndexedDB 缓存大量数据
import { openDB } from 'idb';

const db = await openDB('app-cache', 1, {
  upgrade(db) {
    db.createObjectStore('api-cache', { keyPath: 'url' });
  },
});

async function cachedFetch(url: string): Promise<Response> {
  const cached = await db.get('api-cache', url);

  if (cached && Date.now() - cached.timestamp < 300000) {
    return new Response(JSON.stringify(cached.data));
  }

  const response = await fetch(url);
  const data = await response.json();

  await db.put('api-cache', {
    url,
    data,
    timestamp: Date.now(),
  });

  return new Response(JSON.stringify(data));
}

网络优化

HTTP/2 和 HTTP/3

# Nginx HTTP/2 配置
server {
    listen 443 ssl http2;

    # 服务器推送
    location / {
        http2_push /styles.css;
        http2_push /app.js;
    }
}

压缩

// Express 压缩中间件
import compression from 'compression';

app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  },
}));

// Brotli 压缩(更好的压缩率)
import shrinkRay from 'shrink-ray-current';

app.use(shrinkRay());

资源提示

// 动态资源提示
function addResourceHint(type: 'preload' | 'prefetch', url: string) {
  const link = document.createElement('link');
  link.rel = type;
  link.href = url;
  document.head.appendChild(link);
}

// 基于用户行为预加载
function onHover(event: MouseEvent) {
  const link = (event.target as HTMLElement).closest('a');
  if (link?.href) {
    addResourceHint('prefetch', link.href);
  }
}

性能监控

自定义性能指标

// 自定义性能标记
performance.mark('app-init-start');

await initializeApp();

performance.mark('app-init-end');
performance.measure('app-init', 'app-init-start', 'app-init-end');

// 获取测量结果
const measures = performance.getEntriesByType('measure');
console.log('App init time:', measures[0].duration);

// 长任务监控
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn('Long task detected:', entry);
      reportLongTask(entry);
    }
  }
});

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

错误和崩溃监控

// 全局错误捕获
window.addEventListener('error', (event) => {
  reportError({
    type: 'uncaught',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
  });
});

// Promise 错误捕获
window.addEventListener('unhandledrejection', (event) => {
  reportError({
    type: 'unhandledrejection',
    reason: event.reason,
  });
});

// 内存监控
if ('memory' in performance) {
  setInterval(() => {
    const memory = (performance as any).memory;
    if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.9) {
      reportMemoryWarning(memory);
    }
  }, 30000);
}

构建优化

// Vite 配置优化
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash-es', 'date-fns'],
        },
      },
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
});

最佳实践总结

前端性能优化最佳实践:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   加载优化                                          │
│   ├── 代码分割和懒加载                              │
│   ├── 资源压缩和优化                                │
│   ├── 使用 CDN 分发                                 │
│   └── 预加载关键资源                                │
│                                                     │
│   渲染优化                                          │
│   ├── 虚拟列表处理大数据                            │
│   ├── 避免布局抖动                                  │
│   ├── 使用 CSS 动画替代 JS                          │
│   └── 优化 React 渲染                               │
│                                                     │
│   缓存策略                                          │
│   ├── Service Worker 缓存                           │
│   ├── HTTP 缓存头                                   │
│   ├── 本地存储缓存                                  │
│   └── API 响应缓存                                  │
│                                                     │
│   监控体系                                          │
│   ├── Core Web Vitals                              │
│   ├── 自定义性能指标                                │
│   ├── 错误监控                                      │
│   └── 用户体验监控                                  │
│                                                     │
└─────────────────────────────────────────────────────┘
指标目标值优化方向
LCP< 2.5s优化首屏资源
INP< 200ms减少 JS 执行
CLS< 0.1预留空间
FCP< 1.8s减少阻塞资源
TTFB< 800ms优化服务器响应

性能优化是一个持续的过程。建立监控体系,持续迭代优化,才能保持良好的用户体验。


性能是用户体验的基石。每毫秒都关乎用户的去留。