性能优化大作战:React.memo 在可编辑列表中的奇效

前言

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 是否真的发生了变化。在我们的例子中,HomePageitems 状态一变,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的注意点:

  1. React.memo 进行的是浅比较 :它只会检查 props 的引用是否相等 (Object.is)。如果 props 是一个函数、对象或数组,那么每次父组件渲染时,即使这些 props 的"内容"没变,但它们的引用地址是全新的,React.memo 会认为 props 发生了变化,从而触发子组件的重新渲染。

  2. 函数 prop 的稳定性 :父组件每次刷新时,定义在其中的函数都会被重新创建。虽然新函数的功能和旧的完全一样,但在 JavaScript 看来,它们的内存地址是不同的。当这个函数作为 prop 传给被 memo 包裹的子组件时,memo 会因为检测到 prop 的地址变化而判断其为"新"的 prop,从而导致优化失效,子组件依然会重新渲染。这和你直接在代码里写 <Item onChange={() => {}} /> 的效果是一样的。这就是为什么在这种情况下,我们需要用 useCallback 来"记住"这个函数,确保它的引用地址不会轻易改变。

结语

React.memo 是一个强大而简单的性能优化工具,尤其适用于优化那些渲染开销较大且 props 相对稳定的组件。但是 React.memo 不应该被滥用,因为 React.memo 的优化效果来源于对 props 的比较,这个比较过程本身是有性能开销的。对于那些 props 频繁变化的组件,使用 memo 反而会得不偿失。

感兴趣的可以试试试。

相关推荐
YongGit12 分钟前
探索 AI + MCP 渲染前端 UI
前端·后端·node.js
慧一居士1 小时前
<script setup>中的setup作用以及和不带的区别对比
前端
RainbowSea1 小时前
NVM 切换 Node 版本工具的超详细安装说明
java·前端
读书点滴1 小时前
笨方法学python -练习14
java·前端·python
Mintopia1 小时前
四叉树:二维空间的 “智能分区管理员”
前端·javascript·计算机图形学
慌糖2 小时前
RabbitMQ:消息队列的轻量级王者
开发语言·javascript·ecmascript
Mintopia2 小时前
Three.js 深度冲突:当像素在 Z 轴上玩起 "挤地铁" 游戏
前端·javascript·three.js
Penk是个码农2 小时前
web前端面试-- MVC、MVP、MVVM 架构模式对比
前端·面试·mvc
MrSkye2 小时前
🔥JavaScript 入门必知:代码如何运行、变量提升与 let/const🔥
前端·javascript·面试
白瓷梅子汤2 小时前
跟着官方示例学习 @tanStack-form --- Linked Fields
前端·react.js