️ perf(data-grid): 优化大数据量横向滚动性能

This commit is contained in:
tianqijiuyun-latiao
2026-07-15 15:26:41 +08:00
parent 89c25e3019
commit 8a5209c084
2 changed files with 39 additions and 22 deletions

View File

@@ -2616,18 +2616,23 @@ describe('DataGrid layout', () => {
expect(source).toContain('applyVirtualHorizontalOffset(tableContainer, nextLeft, { forceInternalScroll: true });');
expect(source).toContain('}, [horizontalScrollVisible, scheduleVirtualHorizontalAlignment, tableRenderData, tableScrollX, virtualEditingCell]);');
expect(source).toContain('tableInstance.scrollTo({ left: clampedOffset, top: holderEl.scrollTop });');
expect(source).toContain('deferPostScrollVisualReassert?: boolean');
expect(source).toContain('if (!options?.deferPostScrollVisualReassert) {');
expect(source).toContain('const requestedExternalScrollLeft = pendingExternalScrollLeftRef.current ?? latestExternalScroll.scrollLeft;');
expect(source).toContain('deferPostScrollVisualReassert: true');
expect(source).toContain('if (isExternalScrollbarInteractionActive()) {');
expect(source).toContain('syncVirtualHorizontalVisualOffset(tableContainer, resolvedScrollLeft);');
expect(source).toContain('const visual = syncVirtualHorizontalVisualOffset(tableContainer, requestedExternalScrollLeft);');
expect(source).toContain('lastTableScrollLeftRef.current = visual.clampedOffset;');
expect(source).toContain('applyVirtualHorizontalOffset(tableContainer, resolvedScrollLeft, { forceInternalScroll: true });');
expect(source).not.toContain('deferPostScrollVisualReassert');
expect(source).not.toContain('const synced = syncVirtualHorizontalVisualOffset(tableContainer, externalScroll.scrollLeft);');
expect(source).toContain('const handleExternalHorizontalScrollPointerDown = useCallback');
expect(source).toContain('const handleExternalHorizontalScrollPointerRelease = useCallback');
expect(source).toContain('const finishExternalScrollbarDrag = useCallback');
expect(source).toContain('const handleExternalHorizontalScrollLostPointerCapture = useCallback');
expect(source).toContain('externalScrollbarDraggingRef.current\n || Date.now() < externalScrollInteractionUntilRef.current');
const interactionActiveSource = source.slice(
source.indexOf('const isExternalScrollbarInteractionActive = useCallback'),
source.indexOf('const clearExternalScrollbarInteraction = useCallback'),
);
expect(interactionActiveSource).toContain('externalScrollbarDraggingRef.current');
expect(interactionActiveSource).toContain('Date.now() < externalScrollInteractionUntilRef.current');
const finishExternalScrollbarDragSource = source.slice(
source.indexOf('const finishExternalScrollbarDrag = useCallback'),
source.indexOf('const handleExternalHorizontalScrollPointerRelease = useCallback'),

View File

@@ -3563,7 +3563,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const applyVirtualHorizontalOffset = useCallback((
tableContainer: HTMLElement,
nextOffset: number,
options?: { forceInternalScroll?: boolean; deferPostScrollVisualReassert?: boolean },
options?: { forceInternalScroll?: boolean },
) => {
const synced = syncVirtualHorizontalVisualOffset(tableContainer, nextOffset);
if (!synced) {
@@ -3578,12 +3578,10 @@ const DataGrid: React.FC<DataGridProps> = ({
syncVirtualHorizontalVisualOffset(tableContainer, clampedOffset);
};
const scheduleVisualReassert = () => {
if (!options?.deferPostScrollVisualReassert) {
requestAnimationFrame(() => {
reassertVisual();
window.setTimeout(reassertVisual, 0);
});
}
requestAnimationFrame(() => {
reassertVisual();
window.setTimeout(reassertVisual, 0);
});
};
const tableInstance = tableRef.current;
@@ -3893,9 +3891,16 @@ const DataGrid: React.FC<DataGridProps> = ({
? readVirtualHorizontalOffset(tableContainer)
: lastTableScrollLeftRef.current;
if (enableVirtual && tableContainer instanceof HTMLElement) {
const synced = syncVirtualHorizontalVisualOffset(tableContainer, resolvedScrollLeft);
if (synced) {
resolvedScrollLeft = synced.clampedOffset;
// 拖动/滚轮期间只做 DOM 视觉位移;空闲后才在这里一次性同步
// rc-virtual-list 内部 offsetLeft避免大结果集每帧触发行渲染。
const applied = applyVirtualHorizontalOffset(tableContainer, resolvedScrollLeft, { forceInternalScroll: true });
if (applied) {
resolvedScrollLeft = readVirtualHorizontalOffset(tableContainer);
} else {
const synced = syncVirtualHorizontalVisualOffset(tableContainer, resolvedScrollLeft);
if (synced) {
resolvedScrollLeft = synced.clampedOffset;
}
}
}
lastTableScrollLeftRef.current = resolvedScrollLeft;
@@ -3906,7 +3911,7 @@ const DataGrid: React.FC<DataGridProps> = ({
lastExternalScrollLeftRef.current = latestExternalScroll.scrollLeft;
horizontalSyncSourceRef.current = '';
});
}, [enableVirtual, isExternalScrollbarInteractionActive, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]);
}, [applyVirtualHorizontalOffset, enableVirtual, isExternalScrollbarInteractionActive, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]);
const refreshExternalScrollbarInteraction = useCallback(() => {
externalScrollInteractionUntilRef.current = Date.now() + EXTERNAL_HORIZONTAL_SCROLL_IDLE_SETTLE_MS;
@@ -3959,12 +3964,19 @@ const DataGrid: React.FC<DataGridProps> = ({
const requestedExternalScrollLeft = pendingExternalScrollLeftRef.current ?? latestExternalScroll.scrollLeft;
pendingExternalScrollLeftRef.current = null;
const tableContainer = tableContainerRef.current;
// 外部滑块每帧只更新一次;视觉补偿在本轮拖拽结束后再统一校正
// 用户连续拖动/滚动时,只写 marginLeft、header.scrollLeft 和固定列 CSS 变量
// 不在每一帧调用 Table.scrollTo否则 rc-virtual-list 会随数据量放大渲染开销。
if (enableVirtual && tableContainer instanceof HTMLElement) {
const applied = applyVirtualHorizontalOffset(tableContainer, requestedExternalScrollLeft, {
forceInternalScroll: true,
deferPostScrollVisualReassert: true,
});
if (isExternalScrollbarInteractionActive()) {
const visual = syncVirtualHorizontalVisualOffset(tableContainer, requestedExternalScrollLeft);
if (visual) {
lastTableScrollLeftRef.current = visual.clampedOffset;
scheduleExternalHorizontalScrollSettle(syncSequence);
return;
}
}
const applied = applyVirtualHorizontalOffset(tableContainer, requestedExternalScrollLeft, { forceInternalScroll: true });
if (applied) {
scheduleExternalHorizontalScrollSettle(syncSequence);
return;
@@ -3997,7 +4009,7 @@ const DataGrid: React.FC<DataGridProps> = ({
lastTableScrollLeftRef.current = requestedExternalScrollLeft;
scheduleExternalHorizontalScrollSettle(syncSequence);
});
}, [applyVirtualHorizontalOffset, enableVirtual, refreshExternalScrollbarInteraction, scheduleExternalHorizontalScrollSettle]);
}, [applyVirtualHorizontalOffset, enableVirtual, isExternalScrollbarInteractionActive, refreshExternalScrollbarInteraction, scheduleExternalHorizontalScrollSettle, syncVirtualHorizontalVisualOffset]);
const handleExternalHorizontalScrollPointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
clearExternalScrollbarInteraction();