前言
在React
应用开发中,列表渲染是一个非常常见的场景。当列表项变得复杂,例如每个列表项都是一个功能完备的编辑器时,性能问题便会凸显出来。用户的每一次输入或操作都可能引发整个列表的重新渲染,导致界面卡顿,用户体验下降。本文将探讨如何使用 React.memo
以及相关的 Hooks
(如 useCallback
)来优化这类可编辑列表的性能,避免不必要的渲染,从而提升应用的流畅度。
优化前,可以看到字是一段一段蹦出来的

优化后,字是输出是粒粒分明

接下来直接进入正文
正文
场景构建:一个简单的可编辑列表
首先,我们来构建一个未优化的可编辑列表。
我们先来创建一个markdown编辑器
,让用户可以编辑内容。
tsx
import { MDXEditor, headingsPlugin } from "@mdxeditor/editor";
import "@mdxeditor/editor/style.css";
import { useCallback, useEffect, useRef, useState, memo } from "react";
interface EditorProps {
markdown: string;
onChange: (value: string) => void;
}
function Editor(props: EditorProps) {
const { markdown, onChange } = props;
const [resuleValue, setResuleValue] = useState("");
const valueRef = useRef("");
const handleChange = useCallback((value: string) => {
setResuleValue(value);
valueRef.current = value;
onChange(value);
}, []);
useEffect(() => {
if (markdown !== valueRef.current) {
valueRef.current = markdown;
setResuleValue(markdown);
}
}, [markdown]);
if (!resuleValue) {
return null;
}
console.log("渲染", markdown);
return (
<MDXEditor
className="text-black bg-gray-100"
markdown={resuleValue}
plugins={[headingsPlugin()]}
onChange={handleChange}
/>
);
}
export default Editor;
//export default memo(Editor);
再创建一个 Item
组件,用来渲染列表的每一选,并且每一选都有标题
、详情
、选项
供用户修改。
tsx
// src/app/home/Item.tsx
import React, { useCallback, memo, useEffect } from "react";
import Editor from "@/components/Editor";
export interface ItemProps {
id: number;
title: string;
description: string;
options: string[];
changeTitle: (id: number, title: string) => void;
changeDescription: (id: number, description: string) => void;
changeOption: (id: number, index: number, option: string) => void;
}
function Item(props: ItemProps) {
const {
id,
title,
description,
options,
changeTitle,
changeDescription,
changeOption,
} = props;
const handleChangeTitle = useCallback(
(value: string) => {
changeTitle(id, value);
},
[changeTitle]
);
const handleChangeDescription = useCallback(
(value: string) => {
changeDescription(id, value);
},
[changeDescription]
);
const handleChangeOption = useCallback(
(index: number, value: string) => {
changeOption(id, index, value);
},
[changeOption]
);
useEffect(() => {
console.log("changeOption");
}, [changeOption]);
return (
<div className="flex flex-col gap-4 bg-gray-100 p-4">
<Editor markdown={title} onChange={handleChangeTitle} />
<Editor markdown={description} onChange={handleChangeDescription} />
<div>
<h1>Options</h1>
{options.map((option, index) => {
const handleChangeOptionIndex = useCallback(
(value: string) => {
handleChangeOption(index, value);
},
[handleChangeOption]
);
return (
<Editor
markdown={option}
onChange={handleChangeOptionIndex}
key={index}
/>
);
})}
</div>
</div>
);
}
export default Item;
//export default memo(Item);
然后,我们在父组件 HomePage
中渲染列表:
tsx
// src/app/home/page.tsx
"use client";
import React, { useCallback, useState } from "react";
import Item from "./Item";
import data from "../data";
interface ItemProps {
id: number;
title: string;
description: string;
options: string[];
}
export default function Home() {
const [items, setItems] = useState<ItemProps[]>(data);
const handleChangeTitle = useCallback((id: number, title: string) => {
setItems((prevItems) => prevItems.map((item) => item.id === id ? { ...item, title } : item));
}, []);
const handleChangeDescription = useCallback((id: number, description: string) => {
setItems((prevItems) => prevItems.map((item) => item.id === id ? { ...item, description } : item));
}, []);
const handleChangeOption = useCallback((id: number, index: number, option: string) => {
setItems((prevItems) => prevItems.map((item) => item.id === id ? { ...item, options: item.options.map((o, i) => i === index ? option : o) } : item));
}, []);
return (
<div className="flex flex-col gap-4">
{items.map((item) => (
<Item
key={item.id}
id={item.id}
title={item.title}
description={item.description}
options={item.options}
changeTitle={handleChangeTitle}
changeDescription={handleChangeDescription}
changeOption={handleChangeOption}
/>
))}
</div>
);
}
同时,我们还需要一些初始数据:
ts
// src/app/data.ts
const data = [
{
id: 1,
title: "Title 1",
description: "Description 1",
options: ["Option 1", "Option 2", "Option 3"],
},
{
id: 2,
title: "Title 2",
description: "Description 2",
options: ["Option 1", "Option 2", "Option 3"],
},
{
id: 3,
title: "Title 3",
description: "Description 3",
options: ["Option 1", "Option 2", "Option 3"],
},
//...例子的长度是25
];
export default data;
不必要的重新渲染
现在,打开浏览器的开发者工具,当你在任何一个输入框中输入内容时,你会发现控制台打印出了所有 Item
组件的渲染信息。这意味着,仅仅修改了一个列表项,却导致了所有列表项的重新渲染,所有列表项中的markdown编辑器
也重新渲染了一遍。那页面能不卡吗

为什么会这样呢?这源于 React
的默认渲染机制。当一个父组件(在这里是 HomePage
)因为状态更新(调用了 setItems
)而重新渲染时,它会默认重新渲染其所有的子组件(所有的 Item
组件),无论传递给子组件的 props
是否真的发生了变化。在我们的例子中,HomePage
的 items
状态一变,HomePage
重新渲染,进而导致了整个列表的重新渲染,这在列表项很多或者组件很复杂时,会造成严重的性能浪费。
这也就是为什么每一个markdown编辑器
都重新渲染的原因。
使用 React.memo
进行优化
为了解决这个问题,我们可以使用 React.memo
来包裹 Item
组件。React.memo
是一个高阶组件,它会对 props
进行浅比较
,只有在 props
发生变化时,才会重新渲染被包裹的组件。
修改将列表项
和markdown编辑器
使用React.memo
包裹:
tsx
//export default Item;
export default memo(Item);
tsx
//export default Editor;
export default memo(Editor);
修改后可以看出,修改哪个markdown编辑器
,哪个markdown编辑器
就重新渲染,没有额外的性能消耗,那页面不就嘎嘎快。

React.memo
使用注意事项及 useCallback
的配合
React.memo
的注意点:
-
React.memo
进行的是浅比较 :它只会检查props
的引用是否相等 (Object.is
)。如果props
是一个函数、对象或数组,那么每次父组件渲染时,即使这些props
的"内容"没变,但它们的引用地址是全新的,React.memo
会认为props
发生了变化,从而触发子组件的重新渲染。 -
函数 prop 的稳定性 :父组件每次刷新时,定义在其中的函数都会被重新创建。虽然新函数的功能和旧的完全一样,但在
JavaScript
看来,它们的内存地址是不同的。当这个函数作为prop
传给被memo
包裹的子组件时,memo
会因为检测到prop
的地址变化而判断其为"新"的prop
,从而导致优化失效,子组件依然会重新渲染。这和你直接在代码里写<Item onChange={() => {}} />
的效果是一样的。这就是为什么在这种情况下,我们需要用useCallback
来"记住"这个函数,确保它的引用地址不会轻易改变。
结语
React.memo
是一个强大而简单的性能优化工具,尤其适用于优化那些渲染开销较大且 props
相对稳定的组件。但是 React.memo
不应该被滥用,因为 React.memo
的优化效果来源于对 props
的比较,这个比较过程本身是有性能开销的。对于那些 props
频繁变化的组件,使用 memo
反而会得不偿失。
感兴趣的可以试试试。