We have a large grid rendering lots of expensive components. To reduce the load on the page we use a vitualized list and only render the visible portion at any given moment. This means that whenever someone scrolls, there is flash of white as the components previously outside of the visible area render for the first time.
To get around this we can define an "overscan" area - an amount of items that gets rendered around the visible area that is immediately visible on scroll. Unfortunately that means that these components are now updating alongside page state, which is in our case an expensive operation for any reasonable amount of overscan.
I was thinking one way to resolve this would be to defer the update/render lifecycle of components outside of the visible area to when the call stack clears, utilizing a setTimeout
, or perhaps using a requestIdleCallback
. I have not been able to find a way of doing this in React 16/17 - any ideas?
CodePudding user response:
You can give the components a property telling them whether they're in overscan or not, and have shouldComponentUpdate
return false
if inOverscan
is true
for both the previous and current props (or if using function components, React.memo
's comparison function would return true
if inOverscan
is true
for both the previous and current props).
Here's an example:
const { useState, Component } = React;
class Example1 extends Component {
shouldComponentUpdate(nextProps) {
return !nextProps.inOverscan || !this.props.inOverscan
}
render() {
const {inOverscan, value} = this.props;
return <div>
Example1 - inOverscan = {String(inOverscan)} - value = {String(value)}
</div>;
}
}
const Example2 = React.memo(
({inOverscan, value}) => {
return <div>
Example2 - inOverscan = {String(inOverscan)} - value = {String(value)}
</div>;
},
// Beware that the return value for `memo`'s comparison
// function is the opposite of the one for
// `shouldComponentUpdate`
(prevProps, nextProps) => nextProps.inOverscan && prevProps.inOverscan
);
const App = () => {
const [value, setValue] = useState(0);
const [inOverscan1, setInOverscan1] = useState(true);
const [inOverscan2, setInOverscan2] = useState(true);
const incrementValue = () => setValue(value => value 1);
return <div>
<div>
App - value = {String(value)}
</div>
<div>
<input type="button" value="Increment value" onClick={() => setValue(value => value 1)} />
</div>
<div>
<label>
<input type="checkbox" checked={inOverscan1} onChange={(e) => setInOverscan1(e.target.checked)} />
Example1 in overscan
</label>
</div>
<div>
<label>
<input type="checkbox" checked={inOverscan2} onChange={(e) => setInOverscan2(e.target.checked)} />
Example2 in overscan
</label>
</div>
<hr/>
<Example1 inOverscan={inOverscan1} value={value} />
<Example2 inOverscan={inOverscan2} value={value} />
</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
If you run that, you'll see that initially, incrementing value
doesn't re-render Example1
or Example2
. But if you untick the box for one of them so it's not considered "in overscan" anymore, it updates to match the current value
and follows updates to value
, while the one that's still "in overscan" doesn't.