普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月28日首页

React 性能优化(下):useCallback 与 useTransition 实战

作者 Csvn
2026年3月28日 12:00

引言

在 React 应用性能优化中,useCallbackuseTransition 是两个强大但常被误解的 Hook。本文将深入探讨它们的正确使用场景、常见陷阱以及实际代码示例,帮助你写出更高效的 React 应用。

useCallback:避免不必要的函数重建

核心原理

useCallback 返回一个记忆化的回调函数,只有在依赖项变化时才会重新创建。这对于避免子组件不必要的重新渲染至关重要。

基础用法

import React, { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ 错误:每次渲染都会创建新函数
  const handleClick = () => {
    console.log('Count:', count);
  };

  // ✅ 正确:使用 useCallback 记忆化
  const handleMemoizedClick = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <div>
      <button onClick={handleMemoizedClick}>点击</button>
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

配合 React.memo 使用

import React, { useState, useCallback, memo } from 'react';

const ChildComponent = memo(({ onIncrement, value }) => {
  console.log('Child rendered');
  return (
    <button onClick={onIncrement}>
      Count: {value}
    </button>
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ✅ 只有 count 变化时,onIncrement 才会变化
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <ChildComponent onIncrement={handleIncrement} value={count} />
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

常见陷阱

// ❌ 陷阱 1:依赖项过多导致失去优化效果
const handler = useCallback(() => {
  doSomething(a, b, c, d, e);
}, [a, b, c, d, e]); // 几乎每次都会重新创建

// ✅ 解决:只依赖真正需要的变量
const handler = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// ❌ 陷阱 2:在 useCallback 内部使用非稳定引用
const handler = useCallback(() => {
  config.doSomething(); // config 每次都是新对象
}, [config]);

// ✅ 解决:依赖稳定的值
const handler = useCallback(() => {
  configRef.current.doSomething();
}, []);

useTransition:优化耗时更新

核心概念

useTransition 允许你将某些状态更新标记为"过渡"更新,让 UI 保持响应式,避免阻塞用户交互。

基础用法

import React, { useState, useTransition } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value) => {
    setQuery(value);
    
    // ✅ 将耗时的过滤操作标记为过渡更新
    startTransition(() => {
      const filtered = heavyFilter(value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input 
        value={query}
        onChange={e => handleSearch(e.target.value)}
        placeholder="搜索..."
      />
      {isPending && <span>加载中...</span>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function heavyFilter(query) {
  // 模拟耗时操作
  const data = largeDataset.filter(item => 
    item.name.includes(query)
  );
  return data;
}

实际场景:标签切换

import React, { useState, useTransition } from 'react';

function TabComponent() {
  const [activeTab, setActiveTab] = useState('posts');
  const [isPending, startTransition] = useTransition();

  const tabs = {
    posts: <PostsList />,
    comments: <CommentsList />,
    analytics: <AnalyticsPanel />
  };

  const handleTabChange = (tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        {Object.keys(tabs).map(tab => (
          <button
            key={tab}
            onClick={() => handleTabChange(tab)}
            disabled={isPending}
          >
            {tab}
          </button>
        ))}
      </nav>
      {isPending && <div className="spinner">切换中...</div>}
      <main>
        {tabs[activeTab]}
      </main>
    </div>
  );
}

与 Suspense 配合

import React, { useState, useTransition, Suspense } from 'react';

function Dashboard() {
  const [activeView, setActiveView] = useState('overview');
  const [isPending, startTransition] = useTransition();

  const handleViewChange = (view) => {
    startTransition(() => {
      setActiveView(view);
    });
  };

  return (
    <div>
      <TabNav onChange={handleViewChange} active={activeView} />
      <Suspense fallback={<LoadingSkeleton />}>
        <ViewContent view={activeView} />
      </Suspense>
    </div>
  );
}

性能对比实测

// 未优化的版本
function UnoptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  
  // 每次输入都会重新渲染整个列表
  const filtered = items.filter(item => 
    item.name.includes(filter)
  );

  return (
    <div>
      <input onChange={e => setFilter(e.target.value)} />
      <List data={filtered} />
    </div>
  );
}

// 优化后的版本
function OptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  const [displayItems, setDisplayItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setFilter(value);
    
    startTransition(() => {
      const filtered = items.filter(item => 
        item.name.includes(value)
      );
      setDisplayItems(filtered);
    });
  };

  return (
    <div>
      <input value={filter} onChange={handleChange} />
      {isPending && <LoadingIndicator />}
      <List data={displayItems} />
    </div>
  );
}

最佳实践总结

useCallback 使用指南

  1. 优先优化子组件:只有当函数作为 prop 传递给 React.memo 组件时才需要
  2. 避免过早优化:不是所有函数都需要 useCallback
  3. 注意依赖项:确保依赖项稳定且必要
  4. 配合 useMemo:复杂计算场景结合使用

useTransition 使用指南

  1. 识别耗时更新:列表过滤、大数据渲染、复杂计算
  2. 提供加载反馈:使用 isPending 显示加载状态
  3. 区分优先级:用户输入立即响应,数据更新可延迟
  4. 避免滥用:简单更新不需要过渡

总结

useCallbackuseTransition 是 React 性能优化的利器,但需要正确使用:

  • useCallback 解决的是函数引用稳定性问题
  • useTransition 解决的是更新优先级问题

记住:性能优化应该基于实际测量,而非猜测。使用 React DevTools Profiler 找出真正的瓶颈,再针对性地应用这些 Hook。

昨天 — 2026年3月27日首页

React 性能优化(上):memo 与 useMemo 的正确使用

作者 Csvn
2026年3月27日 10:30

引言

在 React 应用开发中,性能优化是每个开发者都必须面对的课题。很多团队在遇到渲染性能问题时,第一反应就是给组件加上 React.memo,给函数加上 useMemo。然而,滥用这些优化手段反而可能导致性能下降

今天我们来深入探讨 memouseMemo 的工作原理,以及如何在实际项目中正确使用它们。

React 渲染机制回顾

在深入优化之前,我们需要理解 React 的渲染机制:

  1. 状态变化触发渲染:当组件的 state 或 props 发生变化时,React 会重新渲染该组件及其子组件
  2. 虚拟 DOM 比对:React 会生成新的虚拟 DOM 树,并与旧树进行比对(diff)
  3. 最小化真实 DOM 操作:只有实际变化的部分才会更新到真实 DOM

关键点:即使父组件重新渲染,子组件的 props 如果没有实际变化,理论上子组件不需要重新渲染。但默认情况下,React 会重新渲染所有子组件。

React.memo:组件级别的记忆化

React.memo 是一个高阶组件,用于记忆化函数组件的渲染结果。

基本用法

import React from 'react';

// 未经优化的组件 - 每次父组件渲染都会重新渲染
const ChildComponent = ({ name, count }) => {
  console.log('ChildComponent rendered');
  return <div>{name}: {count}</div>;
};

// 使用 React.memo 优化
const MemoizedChild = React.memo(({ name, count }) => {
  console.log('MemoizedChild rendered');
  return <div>{name}: {count}</div>;
});

// 自定义比较函数
const CustomMemoChild = React.memo(
  ({ name, count, data }) => {
    console.log('CustomMemoChild rendered');
    return <div>{name}: {count}</div>;
  },
  (prevProps, nextProps) => {
    // 只比较 name 和 count,忽略 data
    return prevProps.name === nextProps.name && 
           prevProps.count === nextProps.count;
  }
);

常见陷阱

陷阱 1:对象/数组/函数作为 props

// ❌ 错误示范 - 每次渲染都会创建新对象,memo 失效
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <MemoizedChild 
      config={{ theme: 'dark' }}  // 每次都是新对象引用
      onClick={() => {}}          // 每次都是新函数引用
    />
  );
}

// ✅ 正确示范 - 使用 useMemo/useCallback 稳定引用
function Parent() {
  const [count, setCount] = useState(0);
  const config = useMemo(() => ({ theme: 'dark' }), []);
  const handleClick = useCallback(() => {}, []);
  
  return (
    <MemoizedChild 
      config={config}
      onClick={handleClick}
    />
  );
}

陷阱 2:过度使用 memo

// ❌ 不推荐 - 简单组件不需要 memo
const SimpleLabel = React.memo(({ text }) => <span>{text}</span>);

// ✅ 推荐 - 仅在以下情况使用 memo:
// 1. 组件渲染开销大
// 2. 组件接收复杂对象/数组 props
// 3. 父组件频繁渲染但子组件 props 稳定

useMemo:值级别的记忆化

useMemo 用于记忆化计算结果,避免每次渲染都重新计算。

基本用法

import React, { useMemo, useState } from 'react';

function ExpensiveComponent({ items, filter }) {
  // 昂贵的计算操作
  const filteredItems = useMemo(() => {
    console.log('Expensive filtering...');
    return items
      .filter(item => item.category === filter)
      .sort((a, b) => a.priority - b.priority)
      .map(item => ({
        ...item,
        computed: heavyComputation(item)
      }));
  }, [items, filter]); // 仅当 items 或 filter 变化时重新计算
  
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

实际应用场景

场景 1:复杂计算

function Dashboard({ data }) {
  // 数据统计计算
  const statistics = useMemo(() => {
    return {
      total: data.reduce((sum, item) => sum + item.value, 0),
      average: data.reduce((sum, item) => sum + item.value, 0) / data.length,
      max: Math.max(...data.map(item => item.value)),
      min: Math.min(...data.map(item => item.value)),
    };
  }, [data]);
  
  return <StatsPanel stats={statistics} />;
}

场景 2:依赖稳定的对象引用

function Form({ onSubmit }) {
  // 表单配置对象 - 需要稳定引用
  const formConfig = useMemo(() => ({
    validateOnBlur: true,
    validateOnChange: false,
    shouldUnregister: false,
  }), []);
  
  return <FormRenderer config={formConfig} />;
}

常见误区

误区 1:useMemo 总是能提升性能

// ❌ 错误 - 简单计算不需要 useMemo
const doubled = useMemo(() => count * 2, [count]);

// ✅ 正确 - 仅在计算开销大于记忆化开销时使用
// useMemo 本身也有成本:依赖比较 + 缓存管理

误区 2:依赖数组不完整

// ❌ 危险 - 可能使用过时的值
function Component({ userId }) {
  const user = useMemo(() => {
    return fetchUser(userId); // 如果 userId 变化,这里不会重新执行
  }, []); // 缺少 userId 依赖
  
  // ✅ 正确
  const user = useMemo(() => {
    return fetchUser(userId);
  }, [userId]);
}

性能优化检查清单

在使用 memouseMemo 之前,请确认:

  • 组件渲染确实存在性能问题(使用 React DevTools Profiler 验证)
  • 组件接收的 props 包含对象/数组/函数
  • 父组件渲染频率高于子组件需要的渲染频率
  • 计算操作确实是"昂贵"的(遍历大数组、复杂数学运算等)
  • 依赖数组完整且正确

总结

React.memouseMemo 是强大的性能优化工具,但它们不是银弹:

  1. 先测量,再优化:使用 React DevTools Profiler 找到真正的性能瓶颈
  2. 理解原理:明白它们何时生效、何时失效
  3. 避免滥用:简单的组件和计算不需要优化
  4. 注意依赖:确保依赖数组完整,避免闭包陷阱

下一篇我们将继续探讨 useCallbackuseTransition 的使用技巧,进一步完善 React 性能优化知识体系。


📚 延伸阅读

昨天以前首页

自定义 Hooks 实战(下)

作者 Csvn
2026年3月26日 10:43

引言

在 React 开发中,自定义 Hooks 是提取组件逻辑、实现代码复用的强大工具。上篇文章我们介绍了自定义 Hooks 的基本概念,今天我们将深入三个实用的自定义 Hooks:useFetchuseDebounceuseInterval,看看它们如何解决实际开发中的常见问题。


1. useFetch - 优雅的数据获取

使用场景

处理 API 请求时,我们需要管理加载状态、错误处理和响应数据。useFetch 将这些逻辑封装成一个可复用的 Hook。

完整实现

import { useState, useEffect } from 'react';

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

使用示例

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useFetch<User>(
    `/api/users/${userId}`
  );

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error.message}</div>;
  if (!data) return null;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

2. useDebounce - 防抖优化

使用场景

在搜索框输入、窗口大小调整等高频触发场景中,useDebounce 可以延迟执行,避免不必要的计算和请求。

完整实现

import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

使用示例

function SearchBox() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    // 只在用户停止输入 500ms 后执行搜索
    if (debouncedSearchTerm) {
      fetchSearchResults(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="搜索..."
    />
  );
}

3. useInterval - 定时器 Hook

使用场景

需要周期性执行某些操作(如轮询、动画、倒计时)时,useInterval 提供了声明式的定时器管理。

完整实现

import { useEffect, useRef } from 'react';

function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef<() => void>();

  // 记住最新的回调函数
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // 设置定时器
  useEffect(() => {
    function tick() {
      savedCallback.current?.();
    }

    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

使用示例

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

  useInterval(() => {
    setCount(count + 1);
  }, 1000);

  return <div>计数:{count}</div>;
}

总结对比

Hook 核心用途 关键特性 典型场景
useFetch 数据获取 自动管理 loading/error/state API 调用、数据加载
useDebounce 防抖优化 延迟值更新、自动清理定时器 搜索框、输入验证
useInterval 定时执行 支持动态延迟、自动清理 轮询、动画、倒计时

最佳实践建议

  1. 命名规范:自定义 Hook 必须以 use 开头
  2. 依赖管理:确保 useEffect 依赖数组完整
  3. 清理副作用:返回清理函数避免内存泄漏
  4. 类型安全:使用 TypeScript 泛型提高类型推断
  5. 测试覆盖:为自定义 Hook 编写单元测试

自定义 Hooks 实战(上):封装技巧与 useLocalStorage

作者 Csvn
2026年3月25日 17:55

引言

在 React 开发中,Hooks 已经成为状态管理和逻辑复用的核心工具。除了 React 内置的 Hooks,自定义 Hooks 让我们能够将组件逻辑提取到可重用的函数中,实现更好的代码组织和复用。

今天我们来深入探讨自定义 Hooks 的封装技巧,并通过一个实用的 useLocalStorage Hook 来演示如何构建高质量的自定义 Hooks。

自定义 Hooks 的核心原则

1. 命名规范

自定义 Hooks 必须以 use 开头,这是 React 的硬性要求,也是代码可读性的保障:

// ✅ 正确
const useLocalStorage = (key, initialValue) => { ... }
const useFetch = (url) => { ... }
const useDebounce = (value, delay) => { ... }

// ❌ 错误
const localStorageHook = (key, initialValue) => { ... }
const fetchData = (url) => { ... }

2. 单一职责

每个自定义 Hooks 应该只负责一件事,保持逻辑清晰:

// ✅ 好的设计:每个 Hook 职责单一
const { user } = useAuth();
const { data } = useFetch('/api/user');
const { theme } = useTheme();

// ❌ 避免:一个 Hook 做太多事
const useEverything = () => {
  // 认证 + 数据获取 + 主题管理...
}

3. 返回值设计

返回清晰的接口,优先使用对象解构:

// ✅ 清晰的返回值
const { value, setValue, removeValue } = useLocalStorage('key', 'default');

// ✅ 多个返回值时用数组
const [count, setCount] = useCounter(0);

实战:useLocalStorage Hook

基础实现

useLocalStorage 是最常用的自定义 Hooks 之一,它让本地存储变得简单优雅:

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // 读取初始值
  const readValue = () => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  };

  // 使用 lazy initialization
  const [storedValue, setStoredValue] = useState(readValue);

  // 监听其他标签页的变化
  useEffect(() => {
    const handleStorageChange = (event) => {
      if (event.key === key && event.newValue !== null) {
        try {
          setStoredValue(JSON.parse(event.newValue));
        } catch (error) {
          console.warn('Error parsing storage event:', error);
        }
      }
    };

    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  // 返回包装的 setter
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
        // 触发当前标签页的事件
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  const removeValue = () => {
    try {
      setStoredValue(initialValue);
      if (typeof window !== 'undefined') {
        window.localStorage.removeItem(key);
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error removing localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue, removeValue];
}

export default useLocalStorage;

使用示例

import useLocalStorage from './hooks/useLocalStorage';

function UserProfile() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [user, setUser] = useLocalStorage('user', null);

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <div className={`app ${theme}`}>
      <button onClick={toggleTheme}>
        切换到{theme === 'light' ? '深色' : '浅色'}模式
      </button>
      
      {user ? (
        <div>
          <p>欢迎,{user.name}!</p>
          <button onClick={logout}>退出登录</button>
        </div>
      ) : (
        <button onClick={() => setUser({ name: '访客' })}>
          模拟登录
        </button>
      )}
    </div>
  );
}

进阶:添加类型安全(TypeScript 版本)

import { useState, useEffect, Dispatch, SetStateAction } from 'react';

type SetValue<T> = Dispatch<SetStateAction<T>>;

function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>, () => void] {
  const readValue = (): T => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  };

  const [storedValue, setStoredValue] = useState<T>(readValue);

  useEffect(() => {
    const handleStorageChange = (event: StorageEvent) => {
      if (event.key === key && event.newValue !== null) {
        try {
          setStoredValue(JSON.parse(event.newValue) as T);
        } catch (error) {
          console.warn('Error parsing storage event:', error);
        }
      }
    };

    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  const setValue: SetValue<T> = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  const removeValue = () => {
    try {
      setStoredValue(initialValue);
      if (typeof window !== 'undefined') {
        window.localStorage.removeItem(key);
        window.dispatchEvent(new Event('local-storage'));
      }
    } catch (error) {
      console.warn(`Error removing localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue, removeValue];
}

export default useLocalStorage;

封装技巧总结

  1. SSR 兼容:始终检查 window 是否存在
  2. 错误处理:用 try-catch 包裹 localStorage 操作
  3. 事件同步:监听 storage 事件实现多标签页同步
  4. 函数式更新:支持传入函数进行状态更新
  5. 类型安全:使用泛型提供完整的 TypeScript 支持

总结

自定义 Hooks 是 React 逻辑复用的强大工具。通过遵循命名规范、保持单一职责、设计清晰的返回值,我们可以构建出易于理解和维护的 Hooks。

useLocalStorage 作为一个经典案例,展示了如何处理浏览器 API、错误边界、跨标签页同步等实际问题。在下篇中,我们将继续探索 useFetchuseDebounceuseInterval 等更多实用的自定义 Hooks。

❌
❌