为什么在 React 中渲染列表项时要花这么多时间编写脚本? (性能优化)

Why is so much time spent in scripting when rendering list items in React? (performance optimisation)

我一直在尝试提高使用 react-reduxreselect 的 React 应用程序(具体来说是 Electron)的性能。我有一个父(网格)组件,它使用 useSelector 从 redux 存储中获取一些数据,并且为数组中的每个项目呈现一个子组件(网格中的行)。我们还有一个过滤器功能,因此我们对产品数据数组进行一些转换。沿着这些线的东西:

 const data = useDataSelector(
   "all",
   categoryId || EVERYTHING_CATEGORY_ID
 );
 const location = useLocation();
 const [filteredData, setFilteredData] = useState([]);

 useEffect(() => {
    if (query) {
      setFilteredData(
        fuse.search(`${query}`).map((product) => product.item)
      );
    } else {
      setFilteredData(data);
    }
  }, [
    location.search,
  ])

 return (
   <>
     {data.map((productInfo) => (
            <Row key={productInfo.id} {...productInfo} />
          ))}
   </>
 );

useDataSelector 呼叫 useSelector:

export const useDataSelector = (statusType: StatusType = "all", categoryId) =>
  useSelector(productSelector(statusType, categoryId));

productSelector 是一个记忆化的选择器,它会做一些相当繁重的计算:

const productSelector = (
  statusType: StatusType,
  categoryId: number,
) =>
  createSelector(
    [selectProductData, selectProductStatus],
    (productData, productStatus) =>
    //some pretty heavy computations here
  )

现在我看到的问题是渲染网格组件非常慢。除了我们花了很长时间 scripting:

之外,在录制性能时我真的看不到太多

我在查看 Call TreeEvent Log 选项卡时看不到太多内容。好像被剪纸千刀万剐一般……这正常吗?令人失望的是我们的卷轴是多么糟糕。

(关于上面的屏幕截图:我们添加了 LazyLoad 以减少切换选项卡或加载应用程序时的加载时间)。这家伙设置 scroll 事件侦听器并在需要时呈现更多组件。

<li>
  <LazyLoad
        height={60}
        scrollContainer={scrollContainer}
        offset={500}
        overflow
        once
  >
    // actual component rendering
  </LazyLoad>
</li>

您也可以尝试记忆生成的行。

  const rows = useMemo(
    () => data.map((productInfo, idx) => <Row key={idx} {...productInfo} />),
    [data]
  );
  return rows;

并且 React 文档建议不要将索引用作列表元素中的键。您应该将其更改为在 productInfo 数据中使用任何标识符;也许你有 productInfo.id?

正如@Zachary Haber 在评论中所解释的那样,您没有获得 createSelector 的全部好处,因为您是通过在每次渲染时获取 re-executed 的函数创建记忆化选择器。

你实际上可以 pass arguments through your input selectors,虽然这很烦人,因为你有两个参数(我们不想在不记忆该对象的情况下将它们组合成一个对象)。

您可以将 productSelector 函数移动到 useDataSelector 钩子中并将其包装在 useMemo 中,这样当 statusTypecategoryId变化。

import { createSelector } from '@reduxjs/toolkit'; // or from 'reselect'
import { useMemo } from 'react';
import { useSelector } from 'react-redux';

export const useDataSelector = (statusType: StatusType = "all", categoryId: number): Product[] => {
  const productSelector = useMemo(() => {
    return createSelector(
      [selectProductData, selectProductStatus],
      (productData, productStatus): Product[] => {
        return heavyComputation(productData, productStatus, statusType, categoryId);
      }
    )
  },
    [statusType, categoryId]
  );

  return useSelector(productSelector);
}

如果有人访问 categoryId 然后去另一个然后又回到第一个,它 re-evaluate 使用这种方法。而像 re-reselect 这样的东西会将 previously-computed 选择器保留在其缓存中。

另请注意,如果您有多个组件使用 useDataSelector,它们将各自创建自己的选择器缓存版本。我在这里假设列表是唯一访问数据的地方,并且所有子组件都通过 props 传递它们的数据。

既然re-reselect解决了这些问题,让我们看看如何使用它。

import { useSelector } from 'react-redux';
import { createCachedSelector } from 're-reselect';

const productSelector = createCachedSelector(
  // select from redux state
  selectProductData,
  selectProductStatus,
  // pass through arguments
  (state: RootState, statusType: StatusType) => statusType,
  (state: RootState, statusType: StatusType, categoryId: number) => categoryId,
  // combine the results
  (productData, productStatus, statusType, categoryId): Product[] => {
    return heavyComputation(productData, productStatus, statusType, categoryId);
  }
)(
  // create a unique cache key from our arguments
  (state, statusType, categoryId) => `${statusType}__${categoryId}`
)

export const useDataSelector = (statusType: StatusType = "all", categoryId: number): Product[] => {
  return useSelector((state: RootState) => productSelector(state, statusType, categoryId));
}

TypeScript Playground