React 性能反模式

深入理解常见的 React 性能反模式:内联函数作为 props、创建新的对象和数组、无效的 memo 使用、以及其他导致性能问题的代码模式。

React 性能反模式

为什么需要了解反模式

理解什么是「反模式」比知道什么是「正确做法」更重要。性能优化的第一条原则是:不要优化还没有测量过的代码

但这不意味着你可以随意写低效代码。了解常见的性能陷阱,可以帮助你:

  1. 在代码审查中发现潜在问题
  2. 在调试性能问题时快速定位原因
  3. 在编写代码时避免常见的坑

本文不是要你变成强迫症式的优化狂,而是帮助你建立对 React 性能的正确直觉。


内联函数作为 Props

问题所在

在 JSX 中直接定义函数是最常见的性能问题:

function Parent() {
  const [count, setCount] = useState(0);

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

      {/* 每次渲染都创建新的 onClick 函数 */}
      <Child onClick={() => console.log('clicked')} />

      {/* 每次渲染都创建新的 handleChange 函数 */}
      <Input onChange={e => setValue(e.target.value)} />

      {/* 每次渲染都创建新的 renderItem 函数 */}
      <List items={items} renderItem={item => <ListItem key={item.id} item={item} />} />
    </div>
  );
}

为什么这是问题?

即使组件被 React.memo 包装,每次父组件渲染,子组件的 onClick prop 都是一个新的函数引用。React.memo 的浅比较会发现这个变化,导致子组件重新渲染。

解决方案

方案一:useCallback

function Parent() {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState('');

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

  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);

  const renderItem = useCallback((item) => (
    <ListItem key={item.id} item={item} />
  ), []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <Child onClick={handleClick} />
      <Input onChange={handleChange} />
      <List items={items} renderItem={renderItem} />
    </div>
  );
}

方案二:重构组件接收 children

function Parent() {
  const [count, setCount] = useState(0);

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

      {/* children 作为函数时可以接受稳定的引用 */}
      <Child>
        {() => <ExpensiveComponent />}
      </Child>
    </div>
  );
}

function Child({ children }) {
  // children 是稳定的函数引用
  return <div>{children()}</div>;
}

在渲染中创建新对象和数组

问题所在

function UserCard({ user }) {
  const [liked, setLiked] = useState(false);

  return (
    <div>
      <h1>{user.name}</h1>

      {/* 每次渲染都创建新对象 */}
      <ProfileCard
        user={user}
        style={{ color: 'red' }}
        className="user-card"
      />

      {/* 每次渲染都创建新数组 */}
      <Tags tags={['React', 'JavaScript', 'CSS']} />

      {/* 每次渲染都创建新函数 */}
      <Button onClick={() => setLiked(!liked)}>Like</Button>
    </div>
  );
}

解决方案

方案一:useMemo

function UserCard({ user }) {
  const [liked, setLiked] = useState(false);

  const cardStyle = useMemo(() => ({
    color: 'red',
  }), []);

  const cardClassName = useMemo(() => 'user-card', []);

  const tags = useMemo(() => ['React', 'JavaScript', 'CSS'], []);

  return (
    <div>
      <h1>{user.name}</h1>
      <ProfileCard user={user} style={cardStyle} className={cardClassName} />
      <Tags tags={tags} />
      <Button onClick={() => setLiked(!liked)}>Like</Button>
    </div>
  );
}

方案二:将常量移到组件外

// 常量不需要在每次渲染时创建
const DEFAULT_STYLE = { color: 'red' };
const DEFAULT_CLASS = 'user-card';
const DEFAULT_TAGS = ['React', 'JavaScript', 'CSS'];

function UserCard({ user }) {
  return (
    <div>
      <ProfileCard user={user} style={DEFAULT_STYLE} className={DEFAULT_CLASS} />
      <Tags tags={DEFAULT_TAGS} />
    </div>
  );
}

方案三:优化数据结构

function UserCard({ user }) {
  return (
    <div>
      <ProfileCard user={user} />
    </div>
  );
}

// ProfileCard 内部自己定义样式
function ProfileCard({ user }) {
  const style = useMemo(() => ({
    color: 'red',
  }), []);

  return <div style={style}>{user.name}</div>;
}

依赖数组不完整

问题所在

function SearchResults({ query, items }) {
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, []);  // 错误!缺少 query 和 items

  return <div>{filteredItems.map(item => item.name)}</div>;
}
function UserProfile({ userId }) {
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []);  // 错误!缺少 userId,可能使用过期的 userId
}

解决方案

function SearchResults({ query, items }) {
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
  }, [query, items]);  // 完整依赖

  return <div>{filteredItems.map(item => item.name)}</div>;
}

function UserProfile({ userId }) {
  useEffect(() => {
    let cancelled = false;

    fetchUser(userId).then(data => {
      if (!cancelled) {
        setUser(data);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [userId]);  // 完整依赖
}

无效的 React.memo 使用

问题一:memo 了一个本身就频繁更新的组件

const MemoizedCounter = React.memo(function Counter({ count, onIncrement }) {
  console.log('Counter rendered');  // 每次 count 变化都会渲染
  return <button onClick={onIncrement}>{count}</button>;
});

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

  // count 变化时,这个组件的 memo 毫无意义
  return <Counter count={count} onIncrement={() => setCount(c => c + 1)} />;
}

问题二:memo 了一个渲染很快的组件

const MemoizedDiv = React.memo(function Div({ text }) {
  return <div>{text}</div>;  // 渲染时间 < 0.1ms
});

function App() {
  const [text, setText] = useState('Hello');

  // memo 的比较开销可能比渲染本身还大
  return <MemoizedDiv text={text} />;
}

问题三:在 memo 的比较函数中做了昂贵的计算

const ExpensiveMemo = React.memo(
  function ExpensiveComponent({ data }) {
    return <div>{data.name}</div>;
  },
  (prevProps, nextProps) => {
    // 深度比较,计算成本很高
    return isDeepEqual(prevProps.data, nextProps.data);
  }
);

Context 过度使用

问题:单一 Context 包含太多状态

const AppContext = createContext({
  user: null,
  theme: 'light',
  language: 'en',
  notifications: [],
  cart: [],
  // ... 更多状态
});

function App() {
  const [state, setState] = useReducer(appReducer, initialState);

  // 任何状态变化都会导致所有消费者重新渲染
  return (
    <AppContext.Provider value={state}>
      <App />
    </AppContext.Provider>
  );
}

解决方案:拆分 Context

const UserContext = createContext(null);
const ThemeContext = createContext('light');
const CartContext = createContext([]);

function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [cart, dispatch] = useReducer(cartReducer, []);

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <CartContext.Provider value={{ cart, dispatch }}>
          <App />
        </CartContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

在 useEffect 中直接更新状态

问题:setState 导致无限循环

function Component({ items }) {
  const [filteredItems, setFilteredItems] = useState([]);

  useEffect(() => {
    // 每次 filteredItems 变化都会触发这个 effect
    // 而 setFilteredItems 又会导致 filteredItems 变化
    setFilteredItems(items.filter(item => item.active));
  }, [items, filteredItems]);  // 错误:filteredItems 在依赖数组中
}

解决方案

function Component({ items }) {
  const [filter, setFilter] = useState('');

  // 使用 useMemo 而不是 useEffect + setState
  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return <List items={filteredItems} />;
}

过度依赖 ref 作为状态容器

问题:ref 的变化不会触发重新渲染

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  useEffect(() => {
    // 错误:intervalRef.current 变化不会更新 UI
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  // count 更新了,但 intervalRef.current 可能和显示不一致
  return <div>Count: {count}</div>;
}

解决方案

function Timer() {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(true);
  const intervalRef = useRef(null);

  useEffect(() => {
    if (!isRunning) {
      clearInterval(intervalRef.current);
      return;
    }

    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, [isRunning]);

  return (
    <div>
      Count: {count}
      <button onClick={() => setIsRunning(r => !r)}>
        {isRunning ? 'Pause' : 'Resume'}
      </button>
    </div>
  );
}

面试中的表达

面试中聊到性能反模式,通常是在考察你对 React 渲染机制的理解深度:

最常见的性能问题是「过度创建新的函数和对象引用」。每次父组件渲染,如果 props 是新创建的对象或函数,即使内容完全相同,React.memo 的浅比较也会失败。

关于 useEffect 中的 setState,我见过有人写出无限循环的代码。关键是理解 useEffect 的依赖数组——如果你在 effect 中更新了依赖数组中的状态,就会触发无限循环。

Context 过度使用也是个问题。把太多不相关的状态放一个 Context,任何一个变化都会导致所有消费者重新渲染。拆分 Context 是常见的解决方案。

最后,我想强调的是:性能优化要有的放矢。先用 React DevTools Profiler 测量,找到真正的瓶颈,再针对性地优化。过度优化反而增加代码复杂度。


延展阅读