如何优化大量子组件的重新渲染



我是react.js的初学者,我正在构建一个包含大量更改项的组件。

TLDR:我有一个Parent组件,它包含许多Child组件(想想>1000(,它们的状态变化非常快。然而,子组件的状态需要在父组件中已知——因此,我将所有子组件的状况提升到父组件。由于每次父组件中的状态更改时都会渲染所有子组件,因此性能非常糟糕。单个不断变化的像素可能需要超过200毫秒才能更新。在Child组件上实现shouldComponentUpdate仍然太慢。你对如何处理这样的案件有什么一般性的建议吗

作为我的问题的一个具体示例,我创建了一个"图形编辑器"示例,其中PixelGrid组件由32乘32的Pixel组件组成:

示例的JS Fiddle

当在Pixel组件上调用onMouseDownonMouseEnter事件时,该事件通过prop回调传递到父PixelGrid组件,并且相应的状态(PixelGrid.state.pixels[i].color(发生更改。请记住,PixelGrid组件应该能够访问所有像素值以获得进一步的功能,所以我认为将状态保持在Pixel本身实际上不是一个选项。但这意味着,当单个像素发生变化时,需要重新渲染整个PixelGrid组件。这显然非常缓慢。我在Pixel组件上实现了shouldComponentUpdate,以稍微加快速度,但这仍然不是很快,因为每个Pixel都经过了更改测试。

我的第一反应是通过React refs手动更改DOM中像素的内联CSS,而不是将像素状态保持在this.state.pixels中,而是保持在this.cixels,因此状态更改不会导致重新渲染,但"手动"维护视觉表示似乎很糟糕。

那么,您将如何使用React实现这样的功能呢?

使用React.memo来防止在父组件渲染但子组件不更改时渲染子组件。

示例(随机猜测Pixel组件的样子(:

const Pixel = React.memo(({x, y, color, ...rest}) => 
<div style={{
width: 1, 
height: 1, 
x, 
y, 
backgroundColor: color
}}
{...rest}
/>)

现在请记住,如果要将函数传递到Pixel中,也需要对它们进行记忆。例如,这样做是不正确的:

const Parent = () => {
// the callback gets redefined whenever Parent rerenders, causing the React.memo to still update
return <Pixel onClick={() => {}} />
}

相反,你需要做

const Parent = () => {
const cb = useCallback(() => {}, []);
return <Pixel onClick={cb} />
}

您将状态提升到父级是正确的,这样它就可以控制数据。你的Fiddle无法优化,因为你在第82行改变状态。

setPixelColor(row, col, color){
const {pixels} = this.state;
pixels[row * this.props.cols + col].color = color; // mutates state
this.setState({pixels: pixels});
}

当您运行this.state.pixels[n].color = color时,您正在为处于状态的数组中的项重新分配(嵌套(属性。这是一种变异。

为了避免这种情况,你可以将状态的副本传播到一个新的变量中,并对其进行变异:

setPixelColor(row, col, color){
const newPixels = [...this.state.pixels]; // makes a fresh copy
newPixels[row * this.props.cols + col].color = color; // mutate at your leisure
this.setState({pixels: newPixels});
}

如果执行得当,就不应该关心你是否";当单个像素发生变化时,需要重新渲染整个PixelGrid组件"这就是像素改变颜色所需要的。React的diffing算法旨在根据需要更新元素的最小数量。在您的情况下,React将仅更新相关元素上的样式属性。(参见React docs:Reconcilliation(

然而,如果React不确定到底发生了什么变化,例如元素没有唯一的键,或者元素类型发生了变化(例如从<div><p>(,它就无法正确地进行区分。

在Fiddle中,您在数组上进行映射,然后计算每个数组的索引,并将其设置为关键帧。即使您没有对Fiddle示例中的元素重新排序,您也使用了let而不是const,并指出这不是整个代码。如果您的实际代码使用index或当前的rowcolumn作为密钥,但顺序发生了变化,则React将卸载并重新装载每个子级。那肯定会影响你的表现。

(顺便说一句,您不需要计算索引,因为它可以作为.map中的第二个参数使用,请参阅MDN(。

我建议为每个初始化的像素对象添加一个唯一的id属性,该属性在父对象中设置,不会更改。由于你没有将数据与uuid一起使用,也许它们的初始位置是:

pixels.push({
col: col, 
row: row, 
color: 'white',
id: `row${row}-col${col}`; // only set once
});

我很快就对上面的更改进行了修改,以证明这是有效的:https://jsfiddle.net/bethylogism/18ezpoqc/4/

同样,React关于Reconcilliation的文档非常好,我强烈建议任何试图优化的人,因为记忆不起作用:https://reactjs.org/docs/reconciliation.html

最新更新