React 性能坑:别让 AI 踩了,快来添加 rule 吧


一、AI 写 React 的「甜蜜陷阱」

最近用 AI 写 React 项目,速度确实快得飞起。但跑起来一看,卡得像 PPT。打开 React DevTools Profiler 一分析,好家伙,满屏的红色警告。

AI 写代码有个特点:语法正确,逻辑清晰,但性能意识为零。它不会主动考虑: - 这个组件会不会频繁重渲染? - 这个函数需要 useCallback 吗? - 这个计算要不要 useMemo? - 这个 effect 依赖数组对不对?

更要命的是,AI 很擅长「复制粘贴」式编程。你让它写 10 个类似组件,它会给你 10 份几乎一样的代码,每份都带着同样的性能问题。

所以,与其事后优化,不如事前约束。今天就来聊聊如何用 rule 文件,让 AI 在写 React 代码时就避开这些坑。

二、React 性能问题的「重灾区」

在开始写 rule 之前,先盘点一下 AI 最容易踩的 React 性能坑。这些坑我都踩过,有些还踩了不止一次。

2.1 无脑的内联函数

这是 AI 最爱干的事:

jsx 复制代码
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={() => deleteTodo(todo.id)}  // 每次渲染都创建新函数
          onEdit={() => editTodo(todo.id)}      // 又是新函数
        />
      ))}
    </ul>
  );
}

看起来没问题对吧?但每次 TodoList 渲染,所有 TodoItem 都会收到新的函数引用,导致全部重渲染。

正确做法

jsx 复制代码
function TodoList({ todos }) {
  const handleDelete = useCallback((id) => {
    deleteTodo(id);
  }, []);

  const handleEdit = useCallback((id) => {
    editTodo(id);
  }, []);

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={handleDelete}
          onEdit={handleEdit}
        />
      ))}
    </ul>
  );
}

2.2 滥用 useEffect

AI 特别喜欢用 useEffect 解决一切问题,哪怕不需要:

jsx 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [displayName, setDisplayName] = useState('');

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // 完全不需要 effect
  useEffect(() => {
    if (user) {
      setDisplayName(`${user.firstName} ${user.lastName}`);
    }
  }, [user]);

  return <div>{displayName}</div>;
}

displayName 完全可以在渲染时计算,不需要额外的 state 和 effect:

jsx 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  const displayName = user 
    ? `${user.firstName} ${user.lastName}` 
    : '';

  return <div>{displayName}</div>;
}

2.3 缺失的 memo

AI 写组件时很少主动加 React.memo,导致父组件一更新,所有子组件跟着重渲染:

jsx 复制代码
// AI 的写法
function ExpensiveChild({ data }) {
  // 复杂的渲染逻辑
  return <div>{/* ... */}</div>;
}

function Parent() {
  const [count, setCount] = useState(0);
  const data = { value: 'static' };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveChild data={data} />  // count 变化时也会重渲染
    </div>
  );
}

改进

jsx 复制代码
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
  return <div>{/* ... */}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const data = useMemo(() => ({ value: 'static' }), []);

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

2.4 Context 滥用导致的「连锁爆炸」

AI 很喜欢用 Context 共享状态,但经常把所有东西塞进一个 Context:

jsx 复制代码
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  const [settings, setSettings] = useState({});

  // 任何一个值变化,所有消费者都重渲染
  const value = { user, setUser, theme, setTheme, notifications, setNotifications, settings, setSettings };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

只要 theme 变了,所有用到 AppContext 的组件都会重渲染,哪怕它们只关心 user

正确做法:拆分 Context

jsx 复制代码
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

// 各自独立,互不影响

2.5 列表渲染的 key 问题

AI 知道要加 key,但经常用错:

jsx 复制代码
// 用 index 当 key(列表顺序变化时会出问题)
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// 用随机值当 key(每次渲染都变,等于没加)
{items.map(item => (
  <Item key={Math.random()} data={item} />
))}

正确做法:用稳定的唯一标识

jsx 复制代码
{items.map(item => (
  <Item key={item.id} data={item} />
))}

2.6 状态提升过度

AI 倾向于把状态放在最顶层,导致不必要的重渲染:

jsx 复制代码
function App() {
  const [searchTerm, setSearchTerm] = useState('');  // 只有搜索框用到
  const [selectedId, setSelectedId] = useState(null);  // 只有详情页用到

  return (
    <div>
      <Header />  // 不需要这些状态,但会跟着重渲染
      <Sidebar />  // 同上
      <SearchBar value={searchTerm} onChange={setSearchTerm} />
      <DetailView id={selectedId} />
    </div>
  );
}

改进:状态下沉到真正需要的组件

jsx 复制代码
function App() {
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchSection />  // searchTerm 放这里
      <DetailSection />  // selectedId 放这里
    </div>
  );
}

三、编写 AI Rule:从问题到规则

现在我们知道了问题在哪,接下来就是把这些经验转化成 AI 能理解的规则。

3.1 Rule 文件的基本结构

在项目根目录创建 .cursorrules.ai/rules.md(根据你用的 AI 工具):

markdown 复制代码
# React Performance Rules

## 核心原则
- 避免不必要的重渲染
- 合理使用 memo、useMemo、useCallback
- 状态管理要精细化
- 列表渲染必须用稳定的 key

## 具体规则
[详细规则见下文]

3.2 规则 1:函数 Props 必须稳定

markdown 复制代码
## 规则:传递给子组件的函数必须用 useCallback 包裹

### 错误示例
```jsx
<Button onClick={() => handleClick(id)} />

正确示例

jsx 复制代码
const handleButtonClick = useCallback(() => {
  handleClick(id);
}, [id]);

<Button onClick={handleButtonClick} />

例外情况

  • 组件不会频繁渲染(如路由页面组件) - 子组件未使用 React.memo - 函数内部没有依赖外部变量
javascript 复制代码
### 3.3 规则 2:计算密集型操作必须 useMemo

```markdown
## 规则:复杂计算、数据转换、过滤排序等操作必须用 useMemo

### 错误示例
```jsx
function DataTable({ data }) {
  const sortedData = data.sort((a, b) => a.value - b.value);  // 每次渲染都排序
  const filteredData = sortedData.filter(item => item.active);  // 每次都过滤
  
  return <Table data={filteredData} />;
}

正确示例

jsx 复制代码
function DataTable({ data }) {
  const processedData = useMemo(() => {
    const sorted = [...data].sort((a, b) => a.value - b.value);
    return sorted.filter(item => item.active);
  }, [data]);
  
  return <Table data={processedData} />;
}

判断标准

  • 操作涉及数组遍历(map、filter、reduce、sort) - 操作涉及对象/数组创建 - 计算逻辑超过 3 行
javascript 复制代码
### 3.4 规则 3:纯展示组件必须 memo

```markdown
## 规则:纯展示组件(只依赖 props)必须用 React.memo 包裹

### 错误示例
```jsx
function UserCard({ name, avatar, bio }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{bio}</p>
    </div>
  );
}

正确示例

jsx 复制代码
const UserCard = React.memo(function UserCard({ name, avatar, bio }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{bio}</p>
    </div>
  );
});

何时使用

  • 组件只依赖 props,无内部状态 - 组件会被多次渲染(如列表项) - 组件渲染成本较高(复杂 DOM 结构)
scss 复制代码
### 3.5 规则 4:useEffect 依赖数组必须完整

```markdown
## 规则:useEffect 的依赖数组必须包含所有使用的外部变量

### 错误示例
```jsx
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(query).then(setResults);
  }, []);  // 缺少 query 依赖
}

正确示例

jsx 复制代码
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(query).then(setResults);
  }, [query]);  // 包含所有依赖
}

检查清单

  • 所有在 effect 中使用的 props - 所有在 effect 中使用的 state - 所有在 effect 中使用的函数(除非用 useCallback 包裹)
csharp 复制代码
### 3.6 规则 5:避免在渲染中创建对象/数组

```markdown
## 规则:传递给子组件的对象/数组必须用 useMemo 或提取到组件外

### 错误示例
```jsx
function Parent() {
  return (
    <Child 
      config={{ theme: 'dark', size: 'large' }}  // 每次渲染都是新对象
      items={[1, 2, 3]}  // 每次渲染都是新数组
    />
  );
}

正确示例(方案 1:useMemo)

jsx 复制代码
function Parent() {
  const config = useMemo(() => ({ theme: 'dark', size: 'large' }), []);
  const items = useMemo(() => [1, 2, 3], []);
  
  return <Child config={config} items={items} />;
}

正确示例(方案 2:提取到组件外)

jsx 复制代码
const DEFAULT_CONFIG = { theme: 'dark', size: 'large' };
const DEFAULT_ITEMS = [1, 2, 3];

function Parent() {
  return <Child config={DEFAULT_CONFIG} items={DEFAULT_ITEMS} />;
}
javascript 复制代码
### 3.7 规则 6:Context 要拆分

```markdown
## 规则:不同关注点的状态要拆分到不同的 Context

### 错误示例
```jsx
const AppContext = createContext();

function AppProvider({ children }) {
  const value = {
    user, setUser,
    theme, setTheme,
    settings, setSettings,
    // ... 更多状态
  };
  
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

正确示例

jsx 复制代码
// 按功能拆分
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <SettingsProvider>
          {children}
        </SettingsProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

拆分原则

  • 按更新频率拆分(频繁变化的单独一个 Context) - 按功能模块拆分(用户、主题、设置等) - 按消费者拆分(只有少数组件用的单独一个 Context)
javascript 复制代码
### 3.8 规则 7:列表渲染的 key 规范

```markdown
## 规则:列表渲染必须使用稳定且唯一的 key

### 错误示例
```jsx
// ❌ 使用 index
{items.map((item, index) => <Item key={index} {...item} />)}

// ❌ 使用随机值
{items.map(item => <Item key={Math.random()} {...item} />)}

// ❌ 使用不稳定的值
{items.map(item => <Item key={Date.now()} {...item} />)}

正确示例

jsx 复制代码
// ✅ 使用数据的唯一标识
{items.map(item => <Item key={item.id} {...item} />)}

// ✅ 如果没有 id,组合多个字段
{items.map(item => <Item key={`${item.type}-${item.name}`} {...item} />)}

// ✅ 实在没有唯一标识,用 index(但要确保列表顺序不会变)
{staticItems.map((item, index) => <Item key={index} {...item} />)}

Key 选择优先级

  1. 数据库 ID 或 UUID 2. 业务唯一标识(如用户名、订单号) 3. 组合字段(type + name + timestamp) 4. Index(仅当列表完全静态时)
ini 复制代码
## 四、进阶规则:针对特定场景

### 4.1 表单优化规则

```markdown
## 规则:表单字段要独立管理状态

### 错误示例(整个表单一个 state)
```jsx
function Form() {
  const [formData, setFormData] = useState({ name: '', email: '', bio: '' });

  const handleChange = (field, value) => {
    setFormData({ ...formData, [field]: value });  // 任何字段变化,整个表单重渲染
  };

  return (
    <div>
      <Input value={formData.name} onChange={v => handleChange('name', v)} />
      <Input value={formData.email} onChange={v => handleChange('email', v)} />
      <Textarea value={formData.bio} onChange={v => handleChange('bio', v)} />
    </div>
  );
}

正确示例(字段独立)

jsx 复制代码
function FormField({ name, label }) {
  const [value, setValue] = useState('');
  
  return (
    <div>
      <label>{label}</label>
      <input value={value} onChange={e => setValue(e.target.value)} />
    </div>
  );
}

function Form() {
  return (
    <div>
      <FormField name="name" label="Name" />
      <FormField name="email" label="Email" />
      <FormField name="bio" label="Bio" />
    </div>
  );
}

或者使用 useReducer

jsx 复制代码
function formReducer(state, action) {
  return { ...state, [action.field]: action.value };
}

function Form() {
  const [formData, dispatch] = useReducer(formReducer, {
    name: '', email: '', bio: ''
  });

  return (
    <div>
      <MemoizedInput 
        value={formData.name} 
        onChange={v => dispatch({ field: 'name', value: v })} 
      />
      {/* ... */}
    </div>
  );
}
javascript 复制代码
### 4.2 虚拟滚动规则

```markdown
## 规则:超过 100 项的列表必须使用虚拟滚动

### 错误示例
```jsx
function LongList({ items }) {
  return (
    <div>
      {items.map(item => (
        <ListItem key={item.id} data={item} />  // 渲染 10000 个 DOM 节点
      ))}
    </div>
  );
}

正确示例(使用 react-window)

jsx 复制代码
import { FixedSizeList } from 'react-window';

function LongList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem data={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

何时使用

  • 列表项超过 100 个 - 列表项包含复杂 DOM 结构 - 用户需要快速滚动浏览
javascript 复制代码
### 4.3 图片懒加载规则

```markdown
## 规则:非首屏图片必须懒加载

### 错误示例
```jsx
function Gallery({ images }) {
  return (
    <div>
      {images.map(img => (
        <img key={img.id} src={img.url} alt={img.title} />  // 一次性加载所有图片
      ))}
    </div>
  );
}

正确示例(原生 loading 属性)

jsx 复制代码
function Gallery({ images }) {
  return (
    <div>
      {images.map(img => (
        <img 
          key={img.id} 
          src={img.url} 
          alt={img.title}
          loading="lazy"  // 浏览器原生懒加载
        />
      ))}
    </div>
  );
}

正确示例(Intersection Observer)

jsx 复制代码
function LazyImage({ src, alt }) {
  const [imageSrc, setImageSrc] = useState(null);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
        setImageSrc(src);
        observer.disconnect();
      }
    });

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

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

  return <img ref={imgRef} src={imageSrc || placeholder} alt={alt} />;
}
ini 复制代码
### 4.4 防抖节流规则

```markdown
## 规则:高频事件处理必须防抖或节流

### 错误示例
```jsx
function SearchBox() {
  const [query, setQuery] = useState('');

  const handleSearch = (value) => {
    setQuery(value);
    fetchResults(value);  // 每次输入都请求,太频繁
  };

  return <input onChange={e => handleSearch(e.target.value)} />;
}

正确示例(防抖)

jsx 复制代码
import { useDebouncedCallback } from 'use-debounce';

function SearchBox() {
  const [query, setQuery] = useState('');

  const debouncedSearch = useDebouncedCallback((value) => {
    fetchResults(value);
  }, 500);

  const handleChange = (value) => {
    setQuery(value);
    debouncedSearch(value);
  };

  return <input value={query} onChange={e => handleChange(e.target.value)} />;
}

正确示例(节流)

jsx 复制代码
import { useThrottledCallback } from 'use-debounce';

function ScrollTracker() {
  const handleScroll = useThrottledCallback(() => {
    console.log('Scroll position:', window.scrollY);
  }, 200);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);

  return <div>{/* ... */}</div>;
}

使用场景

  • 防抖:搜索输入、表单验证、窗口 resize - 节流:滚动事件、鼠标移动、拖拽
javascript 复制代码
## 五、Rule 文件的完整示例

把上面的规则整合成一个完整的 `.cursorrules` 文件:

```markdown
# React Performance Rules for AI

你是一个注重性能的 React 开发者。在编写 React 代码时,必须遵循以下性能优化规则。

## 1. 函数 Props 稳定性

**规则**:传递给子组件的函数必须用 `useCallback` 包裹。

**例外**:
- 组件不会频繁渲染
- 子组件未使用 `React.memo`

```jsx
// ❌ 错误
<Button onClick={() => handleClick(id)} />

// ✅ 正确
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<Button onClick={handleButtonClick} />

2. 计算密集型操作

规则 :复杂计算必须用 useMemo

判断标准: - 涉及数组遍历(map、filter、reduce、sort) - 涉及对象/数组创建 - 计算逻辑超过 3 行

jsx 复制代码
// ❌ 错误
const sortedData = data.sort((a, b) => a.value - b.value);

// ✅ 正确
const sortedData = useMemo(() => 
  [...data].sort((a, b) => a.value - b.value), 
  [data]
);

3. 纯展示组件

规则 :纯展示组件必须用 React.memo 包裹。

适用场景: - 只依赖 props,无内部状态 - 会被多次渲染(如列表项) - 渲染成本较高

jsx 复制代码
// ❌ 错误
function UserCard({ name, avatar }) {
  return <div>...</div>;
}

// ✅ 正确
const UserCard = React.memo(function UserCard({ name, avatar }) {
  return <div>...</div>;
});

4. useEffect 依赖

规则:依赖数组必须包含所有使用的外部变量。

jsx 复制代码
// ❌ 错误
useEffect(() => {
  fetchData(query);
}, []);  // 缺少 query

// ✅ 正确
useEffect(() => {
  fetchData(query);
}, [query]);

5. 对象/数组 Props

规则:传递给子组件的对象/数组必须稳定。

方案 : - 用 useMemo 包裹 - 提取到组件外部

jsx 复制代码
// ❌ 错误
<Child config={{ theme: 'dark' }} />

// ✅ 正确(方案 1)
const config = useMemo(() => ({ theme: 'dark' }), []);
<Child config={config} />

// ✅ 正确(方案 2)
const DEFAULT_CONFIG = { theme: 'dark' };
<Child config={DEFAULT_CONFIG} />

6. Context 拆分

规则:不同关注点的状态要拆分到不同的 Context。

拆分原则: - 按更新频率拆分 - 按功能模块拆分 - 按消费者拆分

jsx 复制代码
// ❌ 错误:所有状态在一个 Context
const AppContext = createContext();

// ✅ 正确:按功能拆分
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();

7. 列表 Key

规则:必须使用稳定且唯一的 key。

优先级: 1. 数据库 ID 2. 业务唯一标识 3. 组合字段 4. Index(仅当列表完全静态)

jsx 复制代码
// ❌ 错误
{items.map((item, index) => <Item key={index} />)}

// ✅ 正确
{items.map(item => <Item key={item.id} />)}

8. 表单优化

规则 :表单字段要独立管理状态或使用 useReducer

jsx 复制代码
// ❌ 错误:整个表单一个 state
const [formData, setFormData] = useState({ name: '', email: '' });

// ✅ 正确:字段独立或用 reducer
function FormField({ name }) {
  const [value, setValue] = useState('');
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

9. 长列表

规则:超过 100 项的列表必须使用虚拟滚动。

推荐库: - react-window - react-virtualized

jsx 复制代码
// ❌ 错误:渲染所有项
{items.map(item => <Item key={item.id} data={item} />)}

// ✅ 正确:虚拟滚动
<FixedSizeList height={600} itemCount={items.length} itemSize={50}>
  {Row}
</FixedSizeList>

10. 图片懒加载

规则:非首屏图片必须懒加载。

jsx 复制代码
// ❌ 错误
<img src={url} alt={title} />

// ✅ 正确
<img src={url} alt={title} loading="lazy" />

11. 防抖节流

规则:高频事件必须防抖或节流。

使用场景: - 防抖:搜索、表单验证、resize - 节流:滚动、鼠标移动、拖拽

jsx 复制代码
// ❌ 错误
<input onChange={e => fetchResults(e.target.value)} />

// ✅ 正确
const debouncedSearch = useDebouncedCallback(fetchResults, 500);
<input onChange={e => debouncedSearch(e.target.value)} />

检查清单

在提交代码前,确认: - [ ] 所有传递给子组件的函数都用了 useCallback - [ ] 所有复杂计算都用了 useMemo - [ ] 所有纯展示组件都用了 React.memo - [ ] 所有 useEffect 的依赖数组都完整 - [ ] 所有对象/数组 props 都是稳定的 - [ ] Context 按功能拆分了 - [ ] 列表的 key 是稳定唯一的 - [ ] 长列表使用了虚拟滚动 - [ ] 图片使用了懒加载 - [ ] 高频事件使用了防抖/节流

ini 复制代码
## 六、实战:用 Rule 重构一个真实项目

光说不练假把式,来看一个真实案例。这是我之前让 AI 写的一个任务管理应用,性能问题一大堆。

### 6.1 原始代码(AI 的杰作)

```jsx
// TaskList.jsx
function TaskList() {
  const [tasks, setTasks] = useState([]);
  const [filter, setFilter] = useState('all');
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    fetchTasks().then(setTasks);
  }, []);

  const filteredTasks = tasks
    .filter(task => {
      if (filter === 'completed') return task.completed;
      if (filter === 'active') return !task.completed;
      return true;
    })
    .filter(task => task.title.toLowerCase().includes(searchTerm.toLowerCase()));

  return (
    <div>
      <input 
        placeholder="Search..." 
        onChange={e => setSearchTerm(e.target.value)} 
      />
      <div>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('completed')}>Completed</button>
      </div>
      <ul>
        {filteredTasks.map((task, index) => (
          <TaskItem
            key={index}
            task={task}
            onToggle={() => toggleTask(task.id)}
            onDelete={() => deleteTask(task.id)}
            onEdit={(title) => editTask(task.id, title)}
          />
        ))}
      </ul>
    </div>
  );
}

// TaskItem.jsx
function TaskItem({ task, onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(task.title);

  useEffect(() => {
    setEditValue(task.title);
  }, [task.title]);

  return (
    <li style={{ opacity: task.completed ? 0.5 : 1 }}>
      {isEditing ? (
        <input
          value={editValue}
          onChange={e => setEditValue(e.target.value)}
          onBlur={() => {
            onEdit(editValue);
            setIsEditing(false);
          }}
        />
      ) : (
        <span onClick={() => setIsEditing(true)}>{task.title}</span>
      )}
      <button onClick={onToggle}>
        {task.completed ? 'Undo' : 'Done'}
      </button>
      <button onClick={onDelete}>Delete</button>
    </li>
  );
}

6.2 性能问题分析

用 React DevTools Profiler 跑一下,问题一目了然:

  1. 搜索输入时 :每次输入一个字符,整个 TaskList 重渲染,所有 TaskItem 也跟着重渲染 2. 切换过滤器时 :同样的问题 3. 切换任务状态时 :所有任务都重渲染,哪怕只改了一个 4. 列表用 index 当 key:任务顺序变化时会出问题

6.3 应用 Rule 后的重构

jsx 复制代码
// TaskList.jsx
function TaskList() {
  const [tasks, setTasks] = useState([]);
  const [filter, setFilter] = useState('all');
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    fetchTasks().then(setTasks);
  }, []);

  // 规则 2:复杂计算用 useMemo
  const filteredTasks = useMemo(() => {
    return tasks
      .filter(task => {
        if (filter === 'completed') return task.completed;
        if (filter === 'active') return !task.completed;
        return true;
      })
      .filter(task => 
        task.title.toLowerCase().includes(searchTerm.toLowerCase())
      );
  }, [tasks, filter, searchTerm]);

  // 规则 11:搜索输入用防抖
  const debouncedSetSearchTerm = useDebouncedCallback(setSearchTerm, 300);

  // 规则 1:函数 props 用 useCallback
  const handleToggle = useCallback((id) => {
    setTasks(prev => prev.map(task =>
      task.id === id ? { ...task, completed: !task.completed } : task
    ));
  }, []);

  const handleDelete = useCallback((id) => {
    setTasks(prev => prev.filter(task => task.id !== id));
  }, []);

  const handleEdit = useCallback((id, title) => {
    setTasks(prev => prev.map(task =>
      task.id === id ? { ...task, title } : task
    ));
  }, []);

  return (
    <div>
      <input 
        placeholder="Search..." 
        onChange={e => debouncedSetSearchTerm(e.target.value)} 
      />
      <FilterButtons filter={filter} onFilterChange={setFilter} />
      <ul>
        {filteredTasks.map(task => (
          <TaskItem
            key={task.id}  // 规则 7:用稳定的 id
            task={task}
            onToggle={handleToggle}
            onDelete={handleDelete}
            onEdit={handleEdit}
          />
        ))}
      </ul>
    </div>
  );
}

// FilterButtons.jsx(规则 3:纯展示组件用 memo)
const FilterButtons = React.memo(function FilterButtons({ filter, onFilterChange }) {
  return (
    <div>
      <button 
        onClick={() => onFilterChange('all')}
        disabled={filter === 'all'}
      >
        All
      </button>
      <button 
        onClick={() => onFilterChange('active')}
        disabled={filter === 'active'}
      >
        Active
      </button>
      <button 
        onClick={() => onFilterChange('completed')}
        disabled={filter === 'completed'}
      >
        Completed
      </button>
    </div>
  );
});

// TaskItem.jsx(规则 3:用 memo)
const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(task.title);

  // 规则 4:不需要 effect,直接在渲染时同步
  useEffect(() => {
    if (!isEditing) {
      setEditValue(task.title);
    }
  }, [task.title, isEditing]);

  // 规则 1:事件处理函数用 useCallback
  const handleToggle = useCallback(() => {
    onToggle(task.id);
  }, [task.id, onToggle]);

  const handleDelete = useCallback(() => {
    onDelete(task.id);
  }, [task.id, onDelete]);

  const handleBlur = useCallback(() => {
    onEdit(task.id, editValue);
    setIsEditing(false);
  }, [task.id, editValue, onEdit]);

  // 规则 5:样式对象用 useMemo
  const itemStyle = useMemo(() => ({
    opacity: task.completed ? 0.5 : 1,
    textDecoration: task.completed ? 'line-through' : 'none'
  }), [task.completed]);

  return (
    <li style={itemStyle}>
      {isEditing ? (
        <input
          value={editValue}
          onChange={e => setEditValue(e.target.value)}
          onBlur={handleBlur}
          autoFocus
        />
      ) : (
        <span onClick={() => setIsEditing(true)}>{task.title}</span>
      )}
      <button onClick={handleToggle}>
        {task.completed ? 'Undo' : 'Done'}
      </button>
      <button onClick={handleDelete}>Delete</button>
    </li>
  );
});

6.4 性能对比

重构前后的性能对比(100 个任务):

操作 重构前 重构后 提升
搜索输入(每个字符) 120ms 15ms 87.5%
切换过滤器 95ms 12ms 87.4%
切换任务状态 85ms 8ms 90.6%
删除任务 78ms 6ms 92.3%

关键改进: - 搜索防抖:减少了 70% 的渲染次数 - useMemo 缓存过滤结果:避免每次都重新计算 - React.memo + useCallback:只有变化的任务重渲染 - 稳定的 key:避免不必要的 DOM 操作

七、让 AI 自动应用这些规则

有了 Rule 文件,怎么让 AI 真正遵守呢?

7.1 在 Prompt 中引用 Rule

每次让 AI 写代码时,明确提醒它:

markdown 复制代码
请按照 .cursorrules 中的 React 性能规则编写代码。
特别注意:
1. 函数 props 必须用 useCallback
2. 复杂计算必须用 useMemo
3. 纯展示组件必须用 React.memo

7.2 让 AI 做 Code Review

写完代码后,让 AI 自己检查:

复制代码
请检查上面的代码是否符合 .cursorrules 中的性能规则,
列出所有不符合的地方,并给出修改建议。

AI 会输出类似这样的报告:

markdown 复制代码
性能问题检查报告:

1. TaskList.jsx 第 15 行
   问题:filteredTasks 计算未使用 useMemo
   建议:用 useMemo 包裹,依赖 [tasks, filter, searchTerm]

2. TaskList.jsx 第 28 行
   问题:内联函数传递给 TaskItem
   建议:提取为 useCallback

3. TaskItem.jsx 第 5 行
   问题:组件未使用 React.memo
   建议:用 React.memo 包裹

4. TaskList.jsx 第 32 行
   问题:key 使用了 index
   建议:改用 task.id

7.3 自动化检查

配合 ESLint 插件,自动检查代码:

bash 复制代码
npm install eslint-plugin-react-hooks eslint-plugin-react --save-dev

.eslintrc.js

javascript 复制代码
module.exports = {
  plugins: ['react', 'react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'react/jsx-key': 'error',
    'react/jsx-no-bind': ['warn', {
      allowArrowFunctions: false,
      allowBind: false,
      ignoreRefs: true
    }]
  }
};

八、常见问题与误区

8.1 过度优化

问题 :有人看了规则后,给所有组件都加 memo,所有函数都加 useCallback

真相 :优化是有成本的。memouseCallbackuseMemo 本身也要消耗性能。

原则: - 先测量,再优化 - 只优化真正的瓶颈 - 简单组件不需要 memo(渲染成本低于 memo 的对比成本)

jsx 复制代码
// 不需要优化的例子
function SimpleButton({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// 这种情况加 memo 反而更慢
const SimpleButton = React.memo(function SimpleButton({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
});

8.2 useCallback 的依赖陷阱

问题:useCallback 的依赖数组写错,导致函数不更新。

jsx 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  // ❌ 错误:缺少 count 依赖
  const handleClick = useCallback(() => {
    console.log(count);  // 永远打印 0
  }, []);

  // ✅ 正确:包含所有依赖
  const handleClick = useCallback(() => {
    console.log(count);
  }, [count]);

  // ✅ 更好:用函数式更新,不依赖 count
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return <button onClick={handleClick}>Count: {count}</button>;
}

8.3 memo 的浅比较陷阱

问题React.memo 默认只做浅比较,对象/数组 props 还是会导致重渲染。

jsx 复制代码
const Child = React.memo(function Child({ user }) {
  return <div>{user.name}</div>;
});

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

  // ❌ 每次渲染都是新对象,memo 失效
  const user = { name: 'Alice', age: 25 };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child user={user} />  // count 变化时还是会重渲染
    </div>
  );
}

解决方案

jsx 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  // ✅ 用 useMemo 稳定引用
  const user = useMemo(() => ({ name: 'Alice', age: 25 }), []);

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

或者自定义比较函数:

jsx 复制代码
const Child = React.memo(
  function Child({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 自定义比较逻辑
    return prevProps.user.name === nextProps.user.name &&
           prevProps.user.age === nextProps.user.age;
  }
);

8.4 Context 的性能陷阱

问题:Context value 是对象时,每次渲染都是新引用。

jsx 复制代码
function AppProvider({ children }) {
  const [user, setUser] = useState(null);

  // ❌ 每次渲染都是新对象
  const value = { user, setUser };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

解决方案

jsx 复制代码
function AppProvider({ children }) {
  const [user, setUser] = useState(null);

  // ✅ 用 useMemo 稳定引用
  const value = useMemo(() => ({ user, setUser }), [user]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

8.5 状态更新的批处理

问题:多次 setState 导致多次渲染。

jsx 复制代码
function handleClick() {
  setCount(count + 1);      // 触发渲染
  setName('Alice');         // 触发渲染
  setEmail('a@example.com'); // 触发渲染
}

真相:React 18 会自动批处理,上面的代码只会触发一次渲染。但在异步回调中不会:

jsx 复制代码
function handleClick() {
  setTimeout(() => {
    setCount(count + 1);      // 触发渲染
    setName('Alice');         // 触发渲染
    setEmail('a@example.com'); // 触发渲染
  }, 1000);
}

解决方案 :用 unstable_batchedUpdates(React 18 不需要)或合并状态:

jsx 复制代码
// 方案 1:合并状态
const [state, setState] = useState({ count: 0, name: '', email: '' });

function handleClick() {
  setState(prev => ({
    ...prev,
    count: prev.count + 1,
    name: 'Alice',
    email: 'a@example.com'
  }));
}

// 方案 2:用 useReducer
function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_ALL':
      return { ...state, ...action.payload };
    default:
      return state;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleClick() {
    dispatch({
      type: 'UPDATE_ALL',
      payload: { count: 1, name: 'Alice', email: 'a@example.com' }
    });
  }
}

九、性能监控与调试工具

光有规则还不够,还要能看到效果。这些工具能帮你发现性能问题:

9.1 React DevTools Profiler

用法: 1. 安装 React DevTools 浏览器插件 2. 打开 Profiler 标签 3. 点击录制按钮 4. 操作应用 5. 停止录制,查看火焰图

关键指标 : - Render duration :组件渲染耗时 - Render count :渲染次数 - Why did this render:渲染原因

示例

jsx 复制代码
// 在代码中添加 Profiler
import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <YourComponent />
    </Profiler>
  );
}

9.2 why-did-you-render

安装

bash 复制代码
npm install @welldone-software/why-did-you-render --save-dev

配置

javascript 复制代码
// wdyr.js
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
    trackHooks: true,
    logOnDifferentValues: true
  });
}

使用

jsx 复制代码
// 在组件上标记
function MyComponent() {
  return <div>...</div>;
}

MyComponent.whyDidYouRender = true;

控制台会输出:

css 复制代码
MyComponent re-rendered because:
  - props.user changed from { name: 'Alice' } to { name: 'Alice' }
    (different object reference)

9.3 React Scan

安装

bash 复制代码
npm install react-scan --save-dev

使用

jsx 复制代码
import { scan } from 'react-scan';

if (process.env.NODE_ENV === 'development') {
  scan({
    enabled: true,
    log: true
  });
}

会在页面上高亮显示重渲染的组件,颜色越红表示渲染越频繁。

9.4 自定义性能监控 Hook

jsx 复制代码
import { useEffect, useRef } from 'react';

function useRenderCount(componentName) {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    console.log(`${componentName} rendered ${renderCount.current} times`);
  });
}

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changedProps).length > 0) {
        console.log('[why-did-you-update]', name, changedProps);
      }
    }

    previousProps.current = props;
  });
}

// 使用
function MyComponent(props) {
  useRenderCount('MyComponent');
  useWhyDidYouUpdate('MyComponent', props);

  return <div>...</div>;
}

十、总结与行动清单

AI 写 React 代码快,但性能意识弱。通过编写 Rule 文件,我们可以让 AI 在写代码时就避开常见的性能陷阱。

核心要点

  1. 函数稳定性 :传给子组件的函数用 useCallback 2. 计算缓存 :复杂计算用 useMemo 3. 组件缓存 :纯展示组件用 React.memo 4. 依赖完整useEffect 依赖数组要完整 5. 引用稳定 :对象/数组 props 要稳定 6. Context 拆分 :按功能和更新频率拆分 7. Key 规范 :用稳定唯一的 key 8. 长列表优化 :虚拟滚动 9. 图片懒加载 :非首屏图片懒加载 10. 防抖节流 :高频事件要控制 11. 先测量再优化:不要过度优化

行动清单

性能优化是个持续的过程,Rule 文件只是起点。随着项目演进,不断补充和完善规则,让 AI 成为你的性能优化助手,而不是性能杀手。

如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论,我会尽量回复。

相关推荐
光影少年2 小时前
Vue的生命周期有哪些及执行机制?
前端·vue.js·掘金·金石计划
来碗疙瘩汤2 小时前
Vue 事件绑定完全指南:官方文档未详述的事件大全
前端·javascript·vue.js
天涯学馆2 小时前
从 V8 引擎看 JS 代码是如何一步步变成机器指令的
前端·javascript·面试
new code Boy2 小时前
JavaScript转Python”的速查表
开发语言·javascript·python
Elaine3362 小时前
【通过 Vue 实例劫持突破 Web 编辑器的粘贴限制】
前端·javascript·vue.js·chrome devtools·前端逆向
哔哩哔哩技术2 小时前
从“截图大法”到真实交互:B站专栏视频卡的技术革命
前端
zhensherlock2 小时前
Protocol Launcher 系列:一键唤起 Windsurf 智能 IDE
javascript·ide·vscode·ai·typescript·github·ai编程
十步杀一人_千里不留行2 小时前
TypeScript 里的 Type Guard 是什么
javascript·ubuntu·typescript
程序员讲BPM工作流2 小时前
npm非全局方式安装小龙虾OpenClaw
前端·npm·node.js