I'm working on a React-based table that has a large number of cells, and I'm running into performance issues when trying to render the table as it is scrolled. You can look at the source, or you can check it out live.
I've tried to optimize performance by only rendering the cells that are visible to the user (i.e., those that are within the viewport), similar to what react-window does. While this helps a lot, I'm still experiencing significant flickering.
const Body = React.memo((props: BodyProps) => {
const { columns, data, rowStart, rowEnd, colStart, colEnd, width, height } = props;
const rows = [];
for (let i = rowStart; i < rowEnd; i++) {
rows.push(
<Row
columns={columns}
data={data[i]}
colStart={colStart}
colEnd={colEnd}
rowIndex={i}
key={i}
/>
);
}
return (
<div
className="body"
style={{ width, height }}
>
{rows}
</div>
);
});
const Row = React.memo((props: RowProps) => {
const { columns, data, colStart, colEnd, rowIndex } = props;
const cells = [];
for (let i = colStart; i < colEnd; i++) {
const { key } = columns[i];
cells.push(
<Cell
text={data[key]}
colIndex={i}
key={i}
/>
)
}
return (
<div
className="row"
style={{
width: columns.length * tableColWidth,
height: tableRowHeight,
top: rowIndex * tableRowHeight
}}
>
{cells}
</div>
);
});
I tried profiling the app and noticed that a long time was spent "Recalculating Styles". I didn't know what that meant so I searched online for an explanation. Then I found out about layout thrashing and how it can occur when the onscroll event handler either reads or sets layout attributes. So I switched to only saving the latest scroll position within the event handler and grabbing it within a requestAnimationFrame callback, but that didn't seem to have any effect.
useLayoutEffect(() => {
const callback = () => {
const x = viewRef.current!.scrollLeft;
const y = viewRef!.current!.scrollTop;
scrollPositionRef.current = { x, y };
}
viewRef.current?.addEventListener("scroll", callback);
return () => viewRef.current!.removeEventListener("scroll", callback);
}, []);
useLayoutEffect(() => {
const animate = () => {
const lastScrollPosition = scrollPositionRef.current;
const { x, y } = lastScrollPosition;
const newRowOffset = Math.floor(y / tableRowHeight);
const newColOffset = Math.floor(x / tableColWidth);
setOffsets([newRowOffset, newColOffset]);
rafIdRef.current = requestAnimationFrame(animate);
}
rafIdRef.current = requestAnimationFrame(animate);
return () => {
if (rafIdRef.current) {
cancelAnimationFrame(rafIdRef.current);
}
}
}, []);
Is it possible at all to render this many elements without lagging or flickering and, at the very minimum, 60 FPS? If it is, then what I'm doing wrong? If it's not, why? I feel like rendering hundreds of DOM elements shouldn't be so complicated.
My subjective conclusion: despite any optimizations, there will always be a reflow behind the scenes, which can only be solved by the hardware and the refinement of the browser engine.
Regarding the question, I can draw your attention to the following points.
1) Instead of using top for positioning your rows, try using the CSS transform property. This can help the browser with rendering performance, as it can better utilize GPU acceleration for transforms reference here: https://developer.mozilla.org/en-US/docs/Web/Performance/Fundamentals). For example:
style={{
width: columns.length * tableColWidth,
height: tableRowHeight,
transform: `translateY(${rowIndex * tableRowHeight}px)`
}}
2) Throttle the scroll event using requestAnimationFrame. This will ensure the scroll event callback is executed only once per frame, reducing the amount of work done during scrolling.
useLayoutEffect(() => {
let rafId;
const callback = () => {
const x = viewRef.current!.scrollLeft;
const y = viewRef!.current!.scrollTop;
scrollPositionRef.current = { x, y };
if (!rafId) {
rafId = requestAnimationFrame(() => {
setOffsets([
Math.floor(y / tableRowHeight),
Math.floor(x / tableColWidth),
]);
rafId = null;
});
}
};
viewRef.current?.addEventListener("scroll", callback);
return () => {
viewRef.current!.removeEventListener("scroll", callback);
if (rafId) {
cancelAnimationFrame(rafId);
}
};
}, []);
This won't solve the issue, but it will definitely increase the performance. You did a great job, BTW.
What about user experience without optimisation like Intersection Observer pattern: it will be worse than just flickering :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With