性能优化大作战: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 反而会得不偿失。

感兴趣的可以试试试。

相关推荐
wyiyiyi23 分钟前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip1 小时前
vite和webpack打包结构控制
前端·javascript
excel1 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国1 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼1 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy2 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT2 小时前
promise & async await总结
前端
Jerry说前后端2 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天2 小时前
A12预装app
linux·服务器·前端
7723892 小时前
解决 Microsoft Edge 显示“由你的组织管理”问题
前端·microsoft·edge