MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 列表渲染优化与 useMemo 的结合

2021-01-144.0k 阅读

React 列表渲染基础

在 React 开发中,列表渲染是非常常见的场景。例如,展示一个用户列表、商品列表等。React 提供了简洁的方式来实现列表渲染,主要通过数组的 map 方法。

假设我们有一个简单的数组,包含一些数字,我们想要在页面上展示这些数字的列表:

import React from 'react';

const numbers = [1, 2, 3, 4, 5];

const NumberList = () => {
    return (
        <ul>
            {numbers.map((number) => (
                <li key={number}>{number}</li>
            ))}
        </ul>
    );
};

export default NumberList;

在上述代码中,我们使用 map 方法遍历 numbers 数组,并为每个数字创建一个 <li> 元素。这里的 key 属性非常重要,它是 React 用于高效识别列表项的标识。当列表项顺序变化、添加或删除时,key 帮助 React 准确地更新 DOM,而不是重新渲染整个列表。

列表渲染中的性能问题

当列表数据量较小的时候,上述简单的列表渲染方式通常不会有明显的性能问题。但是,随着列表数据量的增大,性能问题就可能逐渐显现出来。

例如,假设我们有一个包含 10000 个元素的列表,并且这个列表所在的组件频繁重新渲染。每次重新渲染,React 都会重新调用 map 方法,重新创建所有的列表项元素。这会导致大量的不必要计算和 DOM 操作,从而影响应用的性能。

React 列表渲染优化策略

为了优化列表渲染的性能,我们可以采用多种策略。

使用虚拟列表

虚拟列表是一种常见的优化方式。它的核心思想是只渲染当前视口可见的列表项,而不是渲染整个列表。当用户滚动列表时,动态地加载新的可见项,并卸载离开视口的项。

在 React 生态中,有一些成熟的库可以帮助我们实现虚拟列表,比如 react - virtualizedreact - window。以 react - virtualized 为例,使用它的 List 组件来优化上述的数字列表渲染:

import React from 'react';
import { List } from'react - virtualized';

const numbers = Array.from({ length: 10000 }, (_, i) => i + 1);

const rowRenderer = ({ index, key, style }) => {
    return (
        <div key={key} style={style}>
            {numbers[index]}
        </div>
    );
};

const NumberList = () => {
    return (
        <List
            height={400}
            rowCount={numbers.length}
            rowHeight={35}
            rowRenderer={rowRenderer}
            width={200}
        />
    );
};

export default NumberList;

在上述代码中,react - virtualizedList 组件通过 rowRenderer 函数只渲染当前视口内的列表项。heightwidth 定义了列表容器的尺寸,rowHeight 定义了每个列表项的高度,rowCount 表示列表项的总数。这样,无论列表有多大,实际渲染的项数始终是有限的,大大提高了性能。

优化列表项的渲染

除了虚拟列表,我们还可以对列表项本身的渲染进行优化。当列表项组件比较复杂,包含较多的计算或子组件时,如果列表项频繁重新渲染,也会影响性能。

假设我们有一个复杂的列表项组件 ListItem,它接收一些数据并进行一些计算:

import React from'react';

const ListItem = ({ data }) => {
    // 一些复杂计算
    const result = data * 2 + Math.sqrt(data);
    return (
        <div>
            {result}
        </div>
    );
};

export default ListItem;

然后在列表渲染中使用这个组件:

import React from'react';
import ListItem from './ListItem';

const dataList = [1, 2, 3, 4, 5];

const NumberList = () => {
    return (
        <ul>
            {dataList.map((data) => (
                <ListItem key={data} data={data} />
            ))}
        </ul>
    );
};

export default NumberList;

在上述代码中,如果 NumberList 组件因为某些原因重新渲染,所有的 ListItem 组件也会重新渲染,即使它们的 data 属性没有变化。这就导致了不必要的计算。

useMemo 原理与基本使用

useMemo 是 React 提供的一个 Hook,它可以用来缓存一个值,只有当依赖项发生变化时才重新计算这个值。其基本语法如下:

const memoizedValue = useMemo(() => {
    // 计算逻辑
    return expensiveCalculation();
}, [dependency1, dependency2]);

useMemo 接收两个参数,第一个参数是一个函数,这个函数返回需要缓存的值。第二个参数是一个依赖项数组。只有当依赖项数组中的值发生变化时,useMemo 才会重新调用第一个参数的函数来重新计算并更新缓存的值。如果依赖项数组为空,useMemo 只会在组件第一次渲染时计算一次值,之后不会再重新计算。

例如,我们有一个简单的组件,包含一个复杂的计算函数 expensiveCalculation,我们使用 useMemo 来缓存计算结果:

import React, { useMemo } from'react';

const expensiveCalculation = () => {
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
        result += i;
    }
    return result;
};

const MemoizedComponent = () => {
    const memoizedResult = useMemo(() => {
        return expensiveCalculation();
    }, []);

    return (
        <div>
            <p>Memoized Result: {memoizedResult}</p>
        </div>
    );
};

export default MemoizedComponent;

在上述代码中,memoizedResult 使用 useMemo 进行了缓存。由于依赖项数组为空,expensiveCalculation 函数只会在组件第一次渲染时执行一次,之后组件重新渲染时,memoizedResult 不会重新计算,从而提高了性能。

React 列表渲染优化与 useMemo 的结合

在列表渲染优化中,useMemo 可以发挥重要作用。我们可以通过 useMemo 来优化列表项组件的渲染。

回到前面复杂的 ListItem 组件,我们可以使用 useMemo 来缓存计算结果:

import React, { useMemo } from'react';

const ListItem = ({ data }) => {
    const result = useMemo(() => {
        return data * 2 + Math.sqrt(data);
    }, [data]);

    return (
        <div>
            {result}
        </div>
    );
};

export default ListItem;

在上述代码中,result 使用 useMemo 进行了缓存,依赖项为 data。只有当 data 属性发生变化时,result 才会重新计算。这样,当 NumberList 组件重新渲染,但 ListItemdata 属性没有变化时,ListItem 不会进行不必要的重新计算,提高了列表项的渲染性能。

useMemo 在虚拟列表中的应用

在虚拟列表场景下,useMemo 同样可以发挥作用。例如,在 react - virtualizedrowRenderer 函数中,我们可能会有一些依赖于列表数据的复杂计算。

假设我们的 rowRenderer 需要根据列表项的数据计算一个复杂的样式:

import React, { useMemo } from'react';
import { List } from'react - virtualized';

const numbers = Array.from({ length: 10000 }, (_, i) => i + 1);

const rowRenderer = ({ index, key, style }) => {
    const complexStyle = useMemo(() => {
        const num = numbers[index];
        return {
            color: num % 2 === 0? 'blue' :'red',
            fontSize: num * 2 + 'px'
        };
    }, [index]);

    return (
        <div key={key} style={{...style,...complexStyle }}>
            {numbers[index]}
        </div>
    );
};

const NumberList = () => {
    return (
        <List
            height={400}
            rowCount={numbers.length}
            rowHeight={35}
            rowRenderer={rowRenderer}
            width={200}
        />
    );
};

export default NumberList;

在上述代码中,complexStyle 使用 useMemo 进行了缓存,依赖项为 index。只有当 index 发生变化时,complexStyle 才会重新计算。这样,在虚拟列表滚动过程中,当列表项进入和离开视口时,如果 index 没有变化,complexStyle 不会重新计算,进一步提高了虚拟列表的性能。

处理复杂依赖场景

在实际应用中,列表渲染可能涉及到更复杂的依赖关系。例如,列表项的渲染可能依赖于多个外部状态。

假设我们有一个列表,列表项的显示样式不仅依赖于自身的数据,还依赖于一个全局的主题状态:

import React, { useContext, useMemo } from'react';
import { List } from'react - virtualized';

const ThemeContext = React.createContext();

const numbers = Array.from({ length: 10000 }, (_, i) => i + 1);

const rowRenderer = ({ index, key, style }) => {
    const theme = useContext(ThemeContext);
    const complexStyle = useMemo(() => {
        const num = numbers[index];
        return {
            color: theme === 'dark'? 'white' : 'black',
            fontSize: num * 2 + 'px'
        };
    }, [index, theme]);

    return (
        <div key={key} style={{...style,...complexStyle }}>
            {numbers[index]}
        </div>
    );
};

const NumberList = () => {
    const theme = 'light';
    return (
        <ThemeContext.Provider value={theme}>
            <List
                height={400}
                rowCount={numbers.length}
                rowHeight={35}
                rowRenderer={rowRenderer}
                width={200}
            />
        </ThemeContext.Provider>
    );
};

export default NumberList;

在上述代码中,complexStyle 的计算依赖于 indexthemeuseMemo 的依赖项数组包含了这两个依赖。只有当 indextheme 发生变化时,complexStyle 才会重新计算。这样,即使主题状态或列表项索引变化,也能确保列表项的样式计算是高效的。

避免过度使用 useMemo

虽然 useMemo 可以有效优化性能,但过度使用也可能带来问题。例如,如果依赖项数组设置不当,可能导致 useMemo 缓存的结果不能及时更新。

假设我们有一个组件,useMemo 的依赖项数组错误地遗漏了一个重要依赖:

import React, { useMemo } from'react';

const MyComponent = () => {
    const [count, setCount] = React.useState(0);
    const [value, setValue] = React.useState(10);

    const memoizedValue = useMemo(() => {
        return count * value;
    }, [count]);

    return (
        <div>
            <p>Memoized Value: {memoizedValue}</p>
            <button onClick={() => setCount(count + 1)}>Increment Count</button>
            <button onClick={() => setValue(value + 1)}>Increment Value</button>
        </div>
    );
};

export default MyComponent;

在上述代码中,memoizedValue 的计算依赖于 countvalue,但依赖项数组只包含了 count。当 value 变化时,memoizedValue 不会重新计算,导致结果不准确。

另外,过多地使用 useMemo 也可能增加代码的复杂性和维护成本。因为需要仔细考虑每个 useMemo 的依赖项,确保缓存逻辑正确。所以,在使用 useMemo 时,需要权衡性能提升和代码复杂性,只在真正需要优化的地方使用。

综合优化案例

下面我们来看一个综合优化的案例,展示如何在一个实际的列表渲染场景中结合多种优化策略和 useMemo

假设我们有一个电商应用,需要展示一个商品列表。每个商品项包含商品图片、名称、价格,并且可以根据用户的筛选条件进行排序。

首先,我们定义商品数据结构和商品列表组件:

import React, { useState, useMemo } from'react';
import { List } from'react - virtualized';

const products = [
    { id: 1, name: 'Product 1', price: 10, image: 'image1.jpg' },
    { id: 2, name: 'Product 2', price: 20, image: 'image2.jpg' },
    // 更多商品数据
];

const ProductListItem = ({ product }) => {
    const priceStyle = useMemo(() => {
        return product.price > 50? { color:'red' } : { color: 'green' };
    }, [product.price]);

    return (
        <div className="product - item">
            <img src={product.image} alt={product.name} />
            <p>{product.name}</p>
            <p style={priceStyle}>{product.price}</p>
        </div>
    );
};

const ProductList = () => {
    const [sortBy, setSortBy] = useState('name');
    const sortedProducts = useMemo(() => {
        return [...products].sort((a, b) => {
            if (sortBy === 'name') {
                return a.name.localeCompare(b.name);
            } else if (sortBy === 'price') {
                return a.price - b.price;
            }
            return 0;
        });
    }, [sortBy, products]);

    const rowRenderer = ({ index, key, style }) => {
        const product = sortedProducts[index];
        return (
            <ProductListItem key={key} style={style} product={product} />
        );
    };

    return (
        <div>
            <select onChange={(e) => setSortBy(e.target.value)}>
                <option value="name">Sort by Name</option>
                <option value="price">Sort by Price</option>
            </select>
            <List
                height={400}
                rowCount={sortedProducts.length}
                rowHeight={100}
                rowRenderer={rowRenderer}
                width={300}
            />
        </div>
    );
};

export default ProductList;

在上述代码中:

  1. ProductListItem 组件使用 useMemo 来缓存商品价格的样式计算,只有当商品价格变化时才重新计算样式。
  2. ProductList 组件使用 useMemo 来缓存根据 sortByproducts 排序后的商品列表。只有当 sortByproducts 变化时,才重新进行排序计算。
  3. 使用 react - virtualizedList 组件实现虚拟列表,只渲染当前视口可见的商品项。

这样,通过多种优化策略和 useMemo 的结合,有效地提高了商品列表渲染的性能,即使在商品数据量较大且有频繁筛选排序操作的情况下,也能保持良好的用户体验。

深入理解 useMemo 与列表渲染优化的本质

从本质上讲,useMemo 与列表渲染优化都是为了减少不必要的计算和渲染。

在列表渲染中,由于列表项可能较多,且组件可能频繁重新渲染,如果每个列表项都进行无意义的重新计算和渲染,会消耗大量的性能。useMemo 通过缓存值,使得只有在依赖项变化时才重新计算,从而避免了很多不必要的计算。

例如,在复杂的列表项组件中,可能存在一些复杂的计算逻辑,如数据转换、样式计算等。这些计算如果每次列表项渲染都执行,会浪费大量资源。通过 useMemo,我们可以将这些计算结果缓存起来,只有当相关依赖发生变化时才重新计算。

而虚拟列表则是从另一个角度进行优化,它通过只渲染可见的列表项,减少了同时需要渲染的元素数量。这在大数据量列表场景下,极大地减轻了浏览器的渲染负担。

当我们将 useMemo 与虚拟列表等优化策略结合使用时,就可以从多个层面来提升列表渲染的性能。在虚拟列表的 rowRenderer 中使用 useMemo,可以进一步优化每个可见列表项的渲染过程,确保在滚动等操作时,只有真正需要变化的部分才会重新计算和渲染。

同时,深入理解 useMemo 的依赖项机制非常重要。依赖项数组的设置决定了 useMemo 何时重新计算缓存值。如果依赖项设置过少,可能导致缓存值不能及时更新;如果依赖项设置过多,又可能导致 useMemo 失去缓存效果,频繁重新计算。

在列表渲染场景中,准确地分析列表项的依赖关系,合理设置 useMemo 的依赖项,对于实现高效的列表渲染优化至关重要。例如,列表项的渲染可能依赖于列表数据本身、外部状态(如主题、筛选条件等)。我们需要将这些真正影响列表项渲染的因素都作为 useMemo 的依赖项,以确保缓存逻辑的正确性。

此外,还需要注意 useMemo 与 React 的渲染机制之间的关系。React 的渲染过程是基于状态和属性的变化来触发的。useMemo 是在这个渲染过程中对特定值进行缓存优化。理解这一点可以帮助我们更好地在列表渲染中运用 useMemo,避免出现与 React 渲染机制冲突或不协调的情况。

总之,通过深入理解 useMemo 的原理和列表渲染优化的本质,我们可以在 React 应用开发中,针对不同的列表渲染场景,灵活运用各种优化策略,打造出高性能的用户界面。无论是小型应用的简单列表,还是大型应用的复杂大数据量列表,都能通过合理的优化,提供流畅的用户体验。