Back to Blog
· 6 min read

前端性能优化笔记

性能优化

研究表明,页面加载时间每增加一秒,用户流失率可能上升 7%。

1. 核心指标:Web Vitals

Google 提出的 Web Vitals 是衡量用户体验的核心指标:

1.1 LCP(Largest Contentful Paint)

最大内容绘制,衡量页面主要内容加载完成的时间。

// 使用 Web Vitals 库监控 LCP
import { onLCP } from 'web-vitals';

onLCP(({ value, entries }) => {
  console.log(`LCP: ${value}ms`);
  // 优化目标: LCP < 2.5s
});

1.2 FID(First Input Delay)

首次输入延迟,衡量用户首次交互的响应速度。

import { onFID } from 'web-vitals';

onFID(({ value }) => {
  console.log(`FID: ${value}ms`);
  // 优化目标: FID < 100ms
});

1.3 CLS(Cumulative Layout Shift)

累积布局偏移,衡量页面视觉稳定性。

import { onCLS } from 'web-vitals';

onCLS(({ value }) => {
  console.log(`CLS: ${value}`);
  // 优化目标: CLS < 0.1
});

p s: 使用 Chrome DevTools 的 Lighthouse 面板可以一键测试这些指标。

2. 资源优化策略

2.1 资源压缩与合并

// webpack 配置示例
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
    ],
  },
};

2.2 图片优化

// 使用 Next.js Image 组件
import Image from 'next/image';

function ProductImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      placeholder="blur"
      blurDataURL="data:image/..." // 模糊占位图
      loading="lazy"
      quality={75}
      width={400}
      height={300}
    />
  );
}

2.3 WebP 图片格式

<!-- 响应式图片使用 picture 标签 -->
<picture>
  <source srcset="image.webp" type="image/webp" />
  <source srcset="image.jpg" type="image/jpeg" />
  <img src="image.jpg" alt="描述" />
</picture>

3. 缓存策略

3.1 浏览器缓存配置

// Next.js next.config.js 配置
module.exports = {
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/:path*.json',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=3600, must-revalidate',
          },
        ],
      },
    ];
  },
};

3.2 Service Worker 缓存

// sw.js
const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/main.js',
];

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);
    })
  );
});

4. 代码拆分与懒加载

4.1 路由级代码拆分

// React Router v6 + React.lazy
import { Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';

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

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

4.2 组件级懒加载

import { useState, lazy, Suspense } from 'react';

function ModalContainer() {
  const [showModal, setShowModal] = useState(false);

  // 动态导入大型组件
  const HeavyModal = lazy(() => import('./HeavyModal'));

  return (
    <>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>
      {showModal && (
        <Suspense fallback={<ModalLoading />}>
          <HeavyModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </>
  );
}

4.3 图片懒加载

// 使用 Intersection Observer 实现图片懒加载
function LazyImage({ src, alt }) {
  const [isVisible, setIsVisible] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      });
    });

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={imgRef}>
      {isVisible && <img src={src} alt={alt} />}
    </div>
  );
}

5. 渲染优化

5.1 减少重排与重绘

// ❌ 避免:多次修改 DOM 触发多次重排
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// ✅ 推荐:使用 CSS 类一次性修改
element.classList.add('expanded');

// ✅ 推荐:使用 document fragment
const fragment = document.createDocumentFragment();
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item;
  fragment.appendChild(li);
});
ul.appendChild(fragment);

5.2 使用 CSS Transform 和 Opacity

/* ❌ 影响布局的属性 */
.element {
  width: 100px;
  height: 100px;
  top: 50px;
}

/* ✅ 不影响布局的属性 */
.element {
  transform: translateX(50px);
  opacity: 0.5;
}

5.3 React 性能优化

import { memo, useMemo, useCallback } from 'react';

// 使用 memo 避免不必要的重渲染
const ListItem = memo(({ item, onClick }) => {
  return <li onClick={() => onClick(item.id)}>{item.name}</li>;
});

// 使用 useMemo 缓存计算结果
function ExpensiveComponent({ data, filter }) {
  const filteredData = useMemo(() => {
    return data.filter(item => item.name.includes(filter));
  }, [data, filter]);

  return filteredData.map(item => <ListItem key={item.id} item={item} />);
}

// 使用 useCallback 缓存回调函数
function Parent() {
  const handleClick = useCallback((id) => {
    console.log('Clicked:', id);
  }, []);

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

6. 网络优化

6.1 CDN 配置

// next.config.js 配置 CDN
module.exports = {
  images: {
    domains: ['cdn.example.com', 'images.unsplash.com'],
    path: '/_next/image',
    loader: 'default',
  },
};

6.2 预连接和预加载

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

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

<!-- 预取下一个路由 -->
<link rel="prefetch" href="/dashboard" />

6.3 异步加载第三方脚本

<!-- 方式 1: async -->
<script src="https://analytics.example.com/script.js" async></script>

<!-- 方式 2: defer -->
<script src="https://analytics.example.com/script.js" defer></script>

<!-- 方式 3: 动态加载 -->
<script>
  const script = document.createElement('script');
  script.src = 'https://analytics.example.com/script.js';
  script.async = true;
  document.head.appendChild(script);
</script>

7. 性能测试工具

7.1 Lighthouse

# 使用 Chrome CLI 运行 Lighthouse
lighthouse https://example.com \
  --preset=desktop \
  --view \
  --output=json \
  --output-path=./lighthouse-report.json

7.2 Web Vitals 实际指标

// 完整的性能监控实现
import { getCLS, getFID, getLCP } from 'web-vitals';

function sendToAnalytics({ name, value, id }) {
  // 发送到分析服务
  gtag('event', name, {
    event_category: 'Web Vitals',
    event_label: id,
    value: Math.round(name === 'CLS' ? value * 1000 : value),
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

8. 常见优化模式

graph TD
    A[用户请求] --> B{缓存检查}
    B -->|命中| C[返回缓存资源]
    B -->|未命中| D[请求服务器]
    D --> E{资源类型}
    E -->|静态资源| F[CDN 响应]
    E -->|动态内容| G[服务器处理]
    F --> H[存入浏览器缓存]
    G --> I[生成响应]
    H --> C
    I --> C

9. 优化清单

优化项目标优先级
LCP< 2.5s
FID< 100ms
CLS< 0.1
图片压缩WebP + 响应式
代码拆分按路由拆分
缓存策略静态资源一年
懒加载非首屏资源
CDN全球节点

10. 总结

前端性能优化是一个系统工程,需要从多个维度入手:

  • 指标驱动: 以 Web Vitals 为核心指标
  • 用户体验: 关注实际加载和交互体验
  • 渐进增强: 优先优化关键渲染路径
  • 持续监控: 建立性能监控体系

注意: 优化要有的放矢,过度优化会增加维护成本。使用 Lighthouse 定期检测,关注对用户影响最大的问题。


相关阅读