React 性能优化技巧

性能优化是构建高质量React应用的关键。优化的React应用加载更快、响应更灵敏,提供更好的用户体验。
页面渲染: 0
组件渲染: 0

性能优化的核心指标

FCP
首次内容绘制

用户看到内容的时间

目标: < 1.8秒
LCP
最大内容绘制

主要内容加载完成时间

目标: < 2.5秒
FID
首次输入延迟

页面交互响应时间

目标: < 100毫秒
CLS
累积布局偏移

视觉稳定性指标

目标: < 0.1

React性能优化时间线

1. 代码分割与懒加载

将代码分割成更小的包,按需加载

不好的做法
// 所有组件一次性导入
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import Dashboard from './pages/Dashboard';
import Admin from './pages/Admin';
好的做法
// 使用React.lazy进行懒加载
import React, { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Admin = lazy(() => import('./pages/Admin'));

// 路由配置中使用Suspense
<Suspense fallback={<LoadingSpinner />}>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</Suspense>
进阶技巧:
  • 使用React.lazy()进行组件懒加载
  • 路由级代码分割(React Router v6支持)
  • 预加载:鼠标悬停时预加载组件
  • 使用Webpack的magic comments控制chunk名称

2. Memoization技术

缓存计算结果,避免不必要的重新计算和渲染

普通组件

渲染次数: 0

Memoized组件

渲染次数: 0

React.memo示例
import React, { memo } from 'react';

// 普通函数组件 - 每次父组件渲染都会重新渲染
const RegularComponent = ({ data }) => {
  console.log('RegularComponent 渲染了');
  return <div>{data}</div>;
};

// 使用React.memo包装 - 只有props变化时才会重新渲染
const MemoizedComponent = memo(({ data }) => {
  console.log('MemoizedComponent 渲染了');
  return <div>{data}</div>;
});

// 自定义比较函数
const CustomMemoComponent = memo(
  ({ data }) => <div>{data}</div>,
  (prevProps, nextProps) => {
    // 只有当data长度变化时才重新渲染
    return prevProps.data.length === nextProps.data.length;
  }
);
useMemo示例
import React, { useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
  // 使用useMemo缓存昂贵的计算结果
  const filteredItems = useMemo(() => {
    console.log('重新计算过滤项...');
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]); // 依赖项变化时才重新计算

  // 另一个例子:计算派生数据
  const stats = useMemo(() => {
    const total = items.length;
    const completed = items.filter(i => i.completed).length;
    const percentage = total > 0 ? (completed / total) * 100 : 0;

    return { total, completed, percentage };
  }, [items]);

  return (
    <div>
      <p>过滤后项数: {filteredItems.length}</p>
      <p>完成率: {stats.percentage.toFixed(1)}%</p>
    </div>
  );
}
useCallback示例
import React, { useState, useCallback } from 'react';

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

  // ❌ 错误:每次渲染都创建新函数
  // const handleAdd = () => {
  //   setItems([...items, `Item ${items.length}`]);
  // };

  // ✅ 正确:使用useCallback缓存函数
  const handleAdd = useCallback(() => {
    setItems(prev => [...prev, `Item ${prev.length}`]);
  }, []); // 空依赖数组,函数不会重新创建

  // 当依赖项变化时重新创建
  const handleIncrement = useCallback(() => {
    setCount(count + 1);
  }, [count]); // count变化时重新创建函数

  return (
    <div>
      <ChildComponent onAdd={handleAdd} />
      <button onClick={handleIncrement}>增加计数</button>
      <p>计数: {count}</p>
    </div>
  );
}

const ChildComponent = memo(({ onAdd }) => {
  console.log('ChildComponent 渲染了');
  return <button onClick={onAdd}>添加项</button>;
});

3. 虚拟化长列表

只渲染可见区域的列表项,提升滚动性能

当前项数: 0 渲染耗时: 0ms
import React, { useMemo } from 'react';
import { FixedSizeList } from 'react-window';

// 普通长列表 - 性能差
function BadList({ items }) {
  return (
    <div style={{ height: '400px', overflowY: 'auto' }}>
      {items.map((item, index) => (
        <div key={index} style={{
          padding: '20px',
          borderBottom: '1px solid #ccc'
        }}>
          {item}
        </div>
      ))}
    </div>
  );
}

// 使用react-window虚拟化列表
function GoodList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      项 #{index}: {items[index]}
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width={300}
      itemCount={items.length}
      itemSize={50} // 每项高度
    >
      {Row}
    </FixedSizeList>
  );
}

// 自定义虚拟列表实现
function VirtualList({ items, itemHeight, visibleCount }) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerHeight = itemHeight * visibleCount;

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + visibleCount + 1,
    items.length
  );

  const visibleItems = useMemo(() => {
    return items.slice(startIndex, endIndex);
  }, [items, startIndex, endIndex]);

  return (
    <div
      style={{ height: containerHeight, overflowY: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{
        height: items.length * itemHeight,
        position: 'relative'
      }}>
        {visibleItems.map((item, index) => (
          <div
            key={startIndex + index}
            style={{
              position: 'absolute',
              top: (startIndex + index) * itemHeight,
              width: '100%',
              height: itemHeight
            }}
          >
            {item}
          </div>
        ))}
      </div>
    </div>
  );
}

4. 优化重新渲染

识别和避免不必要的组件重新渲染

重新渲染检查清单
避免在JSX中创建新对象
// ❌ 错误:每次渲染都创建新对象
function BadComponent({ items }) {
  return (
    <ChildComponent
      style={{ color: 'red', fontSize: '16px' }} // 新对象
      config={{ showHeader: true, autoSave: false }} // 新对象
      onClick={() => console.log('clicked')} // 新函数
    />
  );
}

// ✅ 正确:使用useMemo和useCallback
function GoodComponent({ items }) {
  const style = useMemo(() => ({
    color: 'red', fontSize: '16px'
  }), []);

  const config = useMemo(() => ({
    showHeader: true,
    autoSave: false
  }), []);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <ChildComponent
      style={style}
      config={config}
      onClick={handleClick}
    />
  );
}

// ✅ 或者将静态值提取到组件外部
const STATIC_STYLE = { color: 'red', fontSize: '16px' };
const STATIC_CONFIG = { showHeader: true, autoSave: false };

function BetterComponent({ items }) {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <ChildComponent
      style={STATIC_STYLE}
      config={STATIC_CONFIG}
      onClick={handleClick}
    />
  );
}

5. 图片与资源优化

优化图片、字体和其他静态资源加载

网络加载模拟器
网络速度: 3G (慢速)
图片优化策略
// 1. 使用适当的图片格式
// WebP (现代浏览器) > JPEG (照片) > PNG (透明) > SVG (矢量)

// 2. 使用懒加载
<img
  src="thumbnail.jpg"
  data-src="large.jpg"
  loading="lazy"
  alt="描述"
  className="lazy-image"
/>

// 3. 使用srcset和sizes
<img
  src="image-800w.jpg"
  srcSet="image-400w.jpg 400w,
          image-800w.jpg 800w,
          image-1200w.jpg 1200w"
  sizes="(max-width: 600px) 400px,
         (max-width: 1200px) 800px,
         1200px"
  alt="响应式图片"
/>

// 4. 使用图片CDN
const imageUrl = `https://cdn.example.com/${width}x${height}/${quality}/image.jpg`;

// 5. 使用React-lazyload或类似库
import LazyLoad from 'react-lazyload';

<LazyLoad height={200} offset={100}>
  <img src="large-image.jpg" alt="懒加载图片" />
</LazyLoad>
字体优化
// 使用font-display: swap避免FOIT
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 显示回退字体直到自定义字体加载完成 */
}

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

// 使用fontfaceobserver控制字体加载
import FontFaceObserver from 'fontfaceobserver';

const font = new FontFaceObserver('CustomFont');
font.load().then(() => {
  document.documentElement.classList.add('fonts-loaded');
});

6. 构建与打包优化

优化Webpack配置和构建输出

Bundle分析演示
总大小: 405KB GZIP后: 105KB
Webpack优化配置
// webpack.config.js
module.exports = {
  // ... 其他配置

  optimization: {
    // 1. 代码分割
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        commons: {
          name: 'commons',
          minChunks: 2,
          chunks: 'initial',
          minSize: 0,
        },
      },
    },

    // 2. 最小化
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: process.env.NODE_ENV === 'production',
          },
        },
      }),
    ],

    // 3. 运行时chunk
    runtimeChunk: 'single',
  },

  // 4. 生产模式优化
  mode: 'production',
  devtool: 'source-map',

  // 5. 缓存
  cache: {
    type: 'filesystem',
  },

  // 6. 排除大型库
  externals: {
    lodash: '_',
    jquery: 'jQuery',
  },
};
使用SWC或esbuild加速构建
// 使用@swc/core替换babel
{
  test: /\.(js|jsx)$/,
  exclude: /node_modules/,
  use: {
    loader: 'swc-loader',
    options: {
      jsc: {
        parser: {
          syntax: 'ecmascript',
          jsx: true,
        },
        transform: {
          react: {
            runtime: 'automatic',
          },
        },
      },
    },
  },
}

// 使用esbuild-loader
{
  test: /\.(js|jsx)$/,
  loader: 'esbuild-loader',
  options: {
    loader: 'jsx',
    target: 'es2015',
  },
}

7. 性能监控与分析

使用工具监控和分析应用性能

性能分析工具可以帮助你识别性能瓶颈并进行针对性优化。
React Developer Tools Profiler
// 1. 安装React Developer Tools浏览器扩展

// 2. 使用Profiler API进行编程式分析
import { Profiler } from 'react';

function App() {
  const handleRender = (
    id, // 发生提交的 Profiler 树的 "id"
    phase, // "mount" 或 "update"
    actualDuration, // 本次更新 committed 花费的渲染时间
    baseDuration, // 估计不使用 memoization 的情况下渲染整棵子树需要的时间
    startTime, // 本次更新中 React 开始渲染的时间
    commitTime, // 本次更新中 React committed 的时间
    interactions // 属于本次更新的 interactions 的集合
  ) => {
    console.log({
      id,
      phase,
      actualDuration,
      baseDuration,
      startTime,
      commitTime,
    });
  };

  return (
    <Profiler id="App" onRender={handleRender}>
      <YourApp />
    </Profiler>
  );
}
使用Web Vitals监控核心指标
import { getCLS, getFID, getLCP } from 'web-vitals';

// 发送数据到分析服务
function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  navigator.sendBeacon('/analytics', body);

  // 或者使用Google Analytics
  gtag('event', metric.name, {
    value: metric.value,
    event_label: metric.id,
    non_interaction: true,
  });
}

// 监控所有核心指标
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

// React专用性能钩子
import { useWebVitals } from 'react-web-vitals';

function App() {
  useWebVitals((metric) => {
    console.log(metric);
  });

  return <YourApp />;
}
性能预算和监控
// package.json中添加性能预算
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --profile --json > stats.json",
    "analyze": "webpack-bundle-analyzer stats.json"
  },
  "performance": {
    "budgets": [
      {
        "type": "bundle",
        "name": "main",
        "maximumSize": "200 KB"
      },
      {
        "type": "initial",
        "maximumSize": "400 KB"
      },
      {
        "type": "asset",
        "name": "*.jpg",
        "maximumSize": "100 KB"
      }
    ]
  }
}

// 使用Lighthouse CI自动化测试
module.exports = {
  ci: {
    collect: {
      numberOfRuns: 3,
      url: ['http://localhost:3000'],
    },
    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 }],
      },
    },
  },
};

性能优化工具推荐

工具名称 用途 类型
React Developer Tools 组件树查看、性能分析、状态调试 浏览器扩展
Webpack Bundle Analyzer 分析打包文件大小和依赖关系 构建工具
Lighthouse 全面性能、PWA、可访问性测试 测试工具
react-window 虚拟化长列表和网格 React组件库
why-did-you-render 检测不必要的重新渲染 调试工具
@welldone-software/why-did-you-render 更现代的重新渲染检测 调试工具
web-vitals 测量核心Web性能指标 测量库
Lighthouse CI 自动化性能测试 CI工具
常见性能陷阱与解决方案:
  • 过度优化:先测量再优化,避免过早和过度优化
  • 滥用memoization:只在必要时使用React.memo、useMemo、useCallback
  • 忽略代码分割:确保路由和大型组件进行代码分割
  • 内存泄漏:清理事件监听器、定时器和订阅
  • 阻塞主线程:将复杂计算移到Web Worker或使用requestIdleCallback
性能提示
正在加载提示...