在优化 React 应用时,有许多具体的细节可以关注,以提升性能、可维护性和用户体验。以下是一些关键的优化细节
1. 使用React.memo
对于纯组件(Pure Components),使用 React.memo 包装 防止不必要的重复渲染
示例:
const myComponent = React.memo((props) => {
});
2. 使用 useCallback 和 useMemo
useCallback 用于缓存函数引用,避免子组件因为传入函数变化而重渲染。
useMemo用于缓存计算结果,避免每次渲染时都重新计算。
const memoizedCallback = useCallback(()=>{
doSomething(a,b)
}, [a, b]);
const memovizedValue = useMemo(()=> computeExpensiveValue(a,b)), [a, b];
3. 拆分组件
将大组件拆分更小的可复用组件,提高代码可读性和性能(通过减少必要的重渲染)
场景说明:
一个简单的任务列表组件,允许用户添加任务和展示任务。如果任务列表和输入框逻辑都写在一个组件中,代码会显得臃肿,且每次状态更新时,整个组件都会重新渲染。通过拆分组件,我们可以优化其性能和结构。
3.1 未优化的代码(未拆分的大组件)
import React, { useState } from "react";
const TaskApp = () => {
const [tasks, setTasks] = useState<string[]>([]);
const [newTask, setNewTask] = useState("");
const addTask = () => {
if (newTask.trim()) {
setTasks([...tasks, newTask]);
setNewTask("");
}
};
return (
<div>
<h1>任务列表</h1>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
/>
<button onClick={addTask}>添加任务</button>
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
</div>
);
};
export default TaskApp;
问题:
- 所有逻辑都集中在一个组件中,可读性和维护性差。
- 添加任务时,整个组件重新渲染,包括任务列表。
3.2优化后的代码(拆分组件)
优化结构,拆分为更小的组件
import React, { useState, useCallback, memo } from "react";
// 子组件:任务输入框
const TaskInput = memo(({ onAddTask }: { onAddTask: (task: string) => void }) => {
const [inputValue, setInputValue] = useState("");
const handleAddTask = () => {
if (inputValue.trim()) {
onAddTask(inputValue);
setInputValue("");
}
};
console.log("TaskInput 渲染");
return (
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button onClick={handleAddTask}>添加任务</button>
</div>
);
});
// 子组件:任务列表
const TaskList = memo(({ tasks }: { tasks: string[] }) => {
console.log("TaskList 渲染");
return (
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
);
});
// 主组件:任务应用
const TaskApp = () => {
const [tasks, setTasks] = useState<string[]>([]);
// 使用 useCallback 优化回调函数,避免每次创建新函数
const addTask = useCallback(
(task: string) => {
setTasks((prevTasks) => [...prevTasks, task]);
},
[] // 依赖为空数组,表示回调函数不会因外部状态变化而重新创建
);
return (
<div>
<h1>任务列表</h1>
<TaskInput onAddTask={addTask} />
<TaskList tasks={tasks} />
</div>
);
};
export default TaskApp;
优化后的特点
组件拆分:
TaskInput
:负责处理用户输入和任务添加。
TaskList
:负责展示任务列表。
这样每个组件职责单一,易于维护和测试。
性能优化:
使用 React.memo 防止子组件在不必要的情况下重新渲染。
使用 useCallback 保证传递给子组件的 onAddTask
回调函数不会每次重新创建,减少子组件渲染。
减少重渲染:
添加任务时,仅 TaskList
和 TaskInput
中必要的部分会更新,而不会重新渲染整个组件树。
4. 避免匿名函数和对象
在 JSX 中避免使用匿名函数和对象,因为每次渲染都会创建新的实例。
// 不推荐
<MyComponent onClick={()=> handleClick()} style={{color: 'blue'}}></MyComponent>
// 推荐
const handleClick = useCallback(()=> {
// 逻辑处理
}, []);
const style={color: 'blue'}
<MyComponent onClick={handleClick()} style={style}></MyComponent>
场景说明:
在 React 中,JSX 中使用匿名函数或对象会导致性能问题,因为每次组件渲染时,匿名函数或对象会被重新创建,导致传递给子组件的属性(props)总是新实例,从而触发子组件的重渲染,即使它的 props 内容没有变化。
4.1 未优化代码:在 JSX 中使用匿名函数和对象
import React, { useState } from "react";
const ChildComponent = ({ onClick, style }: { onClick: () => void; style: React.CSSProperties }) => {
console.log("ChildComponent 渲染");
return (
<button onClick={onClick} style={style}>
点击我
</button>
);
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
return (
<div>
<h1>计数器:{count}</h1>
<ChildComponent
onClick={() => setCount(count + 1)} // 每次渲染都创建新的函数实例
style={{ color: "blue", fontSize: "16px" }} // 每次渲染都创建新的对象实例
/>
</div>
);
};
export default ParentComponent;
问题分析:
- 匿名函数:
onClick={() => setCount(count + 1)}
是一个新的函数实例,每次ParentComponent
渲染时都会重新创建。- 即使子组件内部逻辑和行为没有变化,React 仍然会认为
onClick
是新的,导致ChildComponent
重渲染。
- 对象实例:
style={{ color: "blue", fontSize: "16px" }}
是一个新的对象,每次渲染都会重新创建一个新对象。- 即使样式未变化,
ChildComponent
也会重新渲染。
4.2 优化代码:避免在 JSX 中使用匿名函数和对象
使用 useCallback
和 useMemo
import React, { useState, useCallback, useMemo } from "react";
const ChildComponent = ({ onClick, style }: { onClick: () => void; style: React.CSSProperties }) => {
console.log("ChildComponent 渲染");
return (
<button onClick={onClick} style={style}>
点击我
</button>
);
};
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 使用 useCallback 记住函数实例
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
// 使用 useMemo 记住对象实例
const buttonStyle = useMemo(() => {
return { color: "blue", fontSize: "16px" };
}, []);
return (
<div>
<h1>计数器:{count}</h1>
<ChildComponent onClick={handleClick} style={buttonStyle} />
</div>
);
};
export default ParentComponent;
优化后的效果
- 使用
useCallback
:
handleClick
是固定的函数实例,不会因ParentComponent
的重新渲染而变化。- React 的
memo
会根据onClick
的引用判断是否需要重新渲染ChildComponent
。- 使用
useMemo
:
buttonStyle
是固定的对象实例,避免了每次重新渲染时创建新对象。- React 的
memo
会根据style
的引用判断是否需要重新渲染ChildComponent
。- 性能提升:
- 控制台中可以看到,只有在
count
更新时ChildComponent
的onClick
或style
改变时,才会触发重新渲染。
5. 代码分割和懒加载
通过 React.lazy
和 Suspense
实现动态加载组件,可以有效减少初始加载时间,提高页面的首屏性能。代码分割的核心思想是将大文件拆分为多个小文件,按需加载,从而避免一次性加载过多内容。
const OtherComponent = React.lazy(()=>import('./OtherComponent'));
function MyComponent () {
return (
<Suspense fallback={<div>Loading</div>}>
<OtherComponent/></OtherComponent>
</Suspense>
)
}
具体的场景 看这个说明:常规的React项目(优化四)
6. 避免不必要的副作用
确保 useEffect 中的依赖数组正确配置,避免不必要的副作用执行。
useEffect(()=> {
// 副作用逻辑
}, [deps])
具体的场景 看这个例子: 常规的React项目(优化三)
7. 提升图片和资源加载
使用现代图片格式(如 WebP)和图像懒加载技术(如 loading=”lazy”)
<img src="image.webp" alt="example" loading="lazy" />
8. 网络请求优化
合理使用缓存,避免重复请求
// 通过缓存机制,存储已经发出的请求结果,如果同样的请求再次发起,
// 直接从缓存中获取数据,而不是重新发请求。
import axios from "axios";
// 缓存对象
const cache = new Map<string, any>();
// 封装带缓存的请求函数
async function axiosWithCache(url: string): Promise<any> {
if (cache.has(url)) {
console.log(`从缓存中获取数据: ${url}`);
return cache.get(url);
}
console.log(`发送网络请求: ${url}`);
const response = await axios.get(url);
// 将结果存入缓存
cache.set(url, response.data);
return response.data;
}
// 使用示例
(async () => {
const url = "https://jsonplaceholder.typicode.com/todos/1";
const result1 = await axiosWithCache(url); // 第一次请求
console.log(result1);
const result2 = await axiosWithCache(url); // 从缓存获取
console.log(result2);
})();
使用 abortController 取消不再需要的请求。
// 当用户离开页面或切换视图时,取消掉未完成的请求以节省资源。
// 封装请求函数,支持 AbortController
import axios from "axios";
// 封装带取消功能的请求函数
async function axiosWithAbort(url: string, controller: AbortController): Promise<any> {
try {
const response = await axios.get(url, {
signal: controller.signal, // 绑定 AbortController 的 signal
});
return response.data;
} catch (error: any) {
if (axios.isCancel(error)) {
console.log(`请求已取消: ${url}`);
} else {
console.error("请求失败", error);
}
throw error;
}
}
// 使用示例
(async () => {
const controller = new AbortController();
const url = "https://jsonplaceholder.typicode.com/todos/1";
// 模拟请求
const fetchPromise = axiosWithAbort(url, controller);
// 模拟用户取消请求
setTimeout(() => {
controller.abort(); // 取消请求
}, 100);
try {
const result = await fetchPromise;
console.log(result);
} catch (error) {
console.log("请求未完成,已被取消");
}
})();
9. 优化 CSS 和样式
使用 CSS-in-JS 库(如 styled-components 或 Emotion)提高样式管理的灵活性和性能。
避免全局样式污染,使用模块化样式。