Skip to content

[two_dimensional_scrollables] optimizes tableview janks with >250k rows#10737

Closed
wangfeihang wants to merge 3 commits intoflutter:mainfrom
wangfeihang:fix_tableview_janks
Closed

[two_dimensional_scrollables] optimizes tableview janks with >250k rows#10737
wangfeihang wants to merge 3 commits intoflutter:mainfrom
wangfeihang:fix_tableview_janks

Conversation

@wangfeihang
Copy link
Contributor

[two_dimensional_scrollables] optimizes tableview janks with >250k rows

Description:

This PR optimizes the scrolling jank issue of the TableView component when the number of rows exceeds 250,000.

Root Cause:

The _updateFirstAndLastVisibleCell method in RenderTableViewport uses linear for-loop traversal on _columnMetrics and _rowMetrics to locate the visible boundary cells: _firstNonPinnedRow, _lastNonPinnedRow, _firstNonPinnedColumn, and _lastNonPinnedColumn. When the number of rows/columns is extremely large (e.g., >250k rows), this linear traversal causes significant main-thread blocking and scrolling jank.

Solution:

Replace the linear for-loop with binary search algorithm to find the visible boundary cells (_firstNonPinnedRow, _lastNonPinnedRow, _firstNonPinnedColumn, _lastNonPinnedColumn). Binary search reduces the time complexity from O(n) to O(log n), effectively optimizing the scrolling jank issue under large data volumes.

Fixes: #138271

Video performance comparison

before:
https://github.com/user-attachments/assets/ca5b8821-4bdb-411f-bb2c-63998ac7c0d9

after:
https://github.com/user-attachments/assets/ebbf96d9-9e04-4ede-ae79-cd433885d3ab

Pre-Review Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] page, which explains my responsibilities.
  • I read and followed the [relevant style guides] and ran [the auto-formatter].
  • I signed the [CLA].
  • The title of the PR starts with the name of the package surrounded by square brackets, e.g. [shared_preferences]
  • I [linked to at least one issue that this PR fixes] in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under[^1].
  • I updated CHANGELOG.md to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under[^1].
  • I updated/added any relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1].
  • All existing and new tests are passing.

@wangfeihang wangfeihang requested a review from Piinks as a code owner January 7, 2026 07:16
@google-cla
Copy link

google-cla bot commented Jan 7, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions github-actions bot added p: two_dimensional_scrollables Issues pertaining to the two_dimensional_scrollables package triage-framework Should be looked at in framework triage labels Jan 7, 2026
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant performance optimization for TableView with a large number of rows by replacing a linear search with a binary search to find visible cells. The implementation is correct and includes relevant tests. I've provided a few suggestions to simplify the new binary search helper function and its call sites for improved code clarity and to better reflect the underlying data invariants.

Comment on lines +734 to +756
int? _binarySearchFirstFromMap(Map<int, _Span> map, int length, bool Function(_Span) condition) {
if (map.isEmpty || length <= 0) {
return null;
}
var low = 0;
int high = length - 1;
int? result;
while (low <= high) {
final int mid = low + ((high - low) >> 1);
final _Span? span = map[mid];
if (span == null) {
low = mid + 1;
continue;
}
if (condition(span)) {
result = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
return result;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of _binarySearchFirstFromMap can be simplified. The length parameter is redundant as it's always map.length. Additionally, the _rowMetrics and _columnMetrics maps are populated with contiguous integer keys starting from 0, making them dense. Therefore, the span == null check is unnecessary and can be removed, which also allows for using a non-nullable _Span type for span. This improves code clarity by reflecting the data structure's invariants. I'll suggest updates to the call sites in subsequent comments.

  int? _binarySearchFirstFromMap(Map<int, _Span> map, bool Function(_Span) condition) {
    if (map.isEmpty) {
      return null;
    }
    var low = 0;
    int high = map.length - 1;
    int? result;
    while (low <= high) {
      final int mid = low + ((high - low) >> 1);
      final _Span span = map[mid]!;
      if (condition(span)) {
        result = mid;
        high = mid - 1;
      } else {
        low = mid + 1;
      }
    }
    return result;
  }

Comment on lines +781 to +785
_firstNonPinnedColumn = _binarySearchFirstFromMap(
_columnMetrics,
_columnMetrics.length,
(span) => !span.isPinned && span.trailingOffset >= _targetLeadingColumnPixel,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating this call to _binarySearchFirstFromMap to use the simplified signature, removing the redundant length parameter.

    _firstNonPinnedColumn = _binarySearchFirstFromMap(
      _columnMetrics,
      (span) => !span.isPinned && span.trailingOffset >= _targetLeadingColumnPixel,
    );

Comment on lines +786 to +790
_lastNonPinnedColumn = _binarySearchFirstFromMap(
_columnMetrics,
_columnMetrics.length,
(span) => !span.isPinned && span.trailingOffset >= _targetTrailingColumnPixel,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating this call to _binarySearchFirstFromMap to use the simplified signature.

    _lastNonPinnedColumn = _binarySearchFirstFromMap(
      _columnMetrics,
      (span) => !span.isPinned && span.trailingOffset >= _targetTrailingColumnPixel,
    );

Comment on lines +813 to +817
_firstNonPinnedRow = _binarySearchFirstFromMap(
_rowMetrics,
_rowMetrics.length,
(span) => !span.isPinned && span.trailingOffset >= _targetLeadingRowPixel,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating this call to _binarySearchFirstFromMap to use the simplified signature.

    _firstNonPinnedRow = _binarySearchFirstFromMap(
      _rowMetrics,
      (span) => !span.isPinned && span.trailingOffset >= _targetLeadingRowPixel,
    );

Comment on lines +819 to +823
_lastNonPinnedRow = _binarySearchFirstFromMap(
_rowMetrics,
_rowMetrics.length,
(span) => !span.isPinned && span.trailingOffset >= _targetTrailingRowPixel,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating this call to _binarySearchFirstFromMap to use the simplified signature.

    _lastNonPinnedRow = _binarySearchFirstFromMap(
      _rowMetrics,
      (span) => !span.isPinned && span.trailingOffset >= _targetTrailingRowPixel,
    );

@wangfeihang wangfeihang closed this Jan 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p: two_dimensional_scrollables Issues pertaining to the two_dimensional_scrollables package triage-framework Should be looked at in framework triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tableview widget janks execessively with >250k rows

1 participant