一、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 选择优先级
- 数据库 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 跑一下,问题一目了然:
- 搜索输入时 :每次输入一个字符,整个
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。
真相 :优化是有成本的。memo、useCallback、useMemo 本身也要消耗性能。
原则: - 先测量,再优化 - 只优化真正的瓶颈 - 简单组件不需要 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 在写代码时就避开常见的性能陷阱。
核心要点
- 函数稳定性 :传给子组件的函数用
useCallback2. 计算缓存 :复杂计算用useMemo3. 组件缓存 :纯展示组件用React.memo4. 依赖完整 :useEffect依赖数组要完整 5. 引用稳定 :对象/数组 props 要稳定 6. Context 拆分 :按功能和更新频率拆分 7. Key 规范 :用稳定唯一的 key 8. 长列表优化 :虚拟滚动 9. 图片懒加载 :非首屏图片懒加载 10. 防抖节流 :高频事件要控制 11. 先测量再优化:不要过度优化
行动清单
-
\] 在项目根目录创建 `.cursorrules` 文件 - \[ \] 复制本文的完整 Rule 示例 - \[ \] 根据项目特点调整规则 - \[ \] 在 Prompt 中引用 Rule 文件 - \[ \] 让 AI 做 Code Review - \[ \] 配置 ESLint 自动检查 - \[ \] 安装 React DevTools Profiler - \[ \] 安装 why-did-you-render - \[ \] 定期用 Profiler 检查性能 - \[ \] 记录和分享优化经验
-
React 官方性能优化指南 - React DevTools Profiler 文档 - useMemo 和 useCallback 的使用时机 - React 渲染行为完全指南
性能优化是个持续的过程,Rule 文件只是起点。随着项目演进,不断补充和完善规则,让 AI 成为你的性能优化助手,而不是性能杀手。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论,我会尽量回复。