为什么在 React 中渲染列表项时要花这么多时间编写脚本? (性能优化)
Why is so much time spent in scripting when rendering list items in React? (performance optimisation)
我一直在尝试提高使用 react-redux
和 reselect
的 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 Tree
或 Event 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
中,这样当 statusType
或 categoryId
变化。
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));
}
我一直在尝试提高使用 react-redux
和 reselect
的 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 Tree
或 Event 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
中,这样当 statusType
或 categoryId
变化。
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));
}