Skip to content

Feat: browser max element size#44

Merged
zaewc merged 8 commits into
developfrom
feat/browser-max-element-size
May 19, 2026
Merged

Feat: browser max element size#44
zaewc merged 8 commits into
developfrom
feat/browser-max-element-size

Conversation

@zaewc
Copy link
Copy Markdown
Owner

@zaewc zaewc commented May 19, 2026

fixes #33

zaewc added 3 commits May 19, 2026 13:49
Detects the browser's per-element height ceiling by binary-searching an
offscreen probe div. Result is cached after the first call, with an SSR
fallback when document is unavailable.
…l offset mapping

When the virtual total (count * itemSize) exceeds the probed per-element
height ceiling, the spacer is clamped and DOM scroll positions are linearly
mapped onto the virtual range. Item offsets are translated to render
relative to the current scroll so they remain visible inside the clamped
spacer.
@zaewc zaewc self-assigned this May 19, 2026
Copy link
Copy Markdown
Contributor

@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 support for massive lists in FixedLayoutStrategy by probing the browser's maximum element size and mapping virtual offsets to a clamped scrollable area. The implementation includes a new utility, getMaxElementSize, which uses binary search to determine the height limit. Review feedback correctly identified a potential 32-bit integer overflow when using bitwise operators for truncation on large lists, a possible null reference error if document.body is unavailable during probing, and an opportunity to optimize performance by caching redundant calculations in the item offset logic.


const startIndex = clamp(
0,
(virtualOffset / this.#itemSize) | 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Using the bitwise OR operator (| 0) for truncation is problematic for very large lists. In JavaScript, bitwise operations operate on 32-bit signed integers, meaning any value exceeding ~2.1 billion (2^31 - 1) will overflow and result in an incorrect (potentially negative) index. Since this PR specifically aims to support massive lists that exceed browser element size limits, it is highly likely that virtualOffset will exceed this range. Using Math.floor() is safe for all double-precision numbers up to Number.MAX_SAFE_INTEGER.

Suggested change
(virtualOffset / this.#itemSize) | 0,
Math.floor(virtualOffset / this.#itemSize),

Comment thread packages/core/src/utils/getMaxElementSize.ts Outdated
Comment thread packages/core/src/strategies/layout/FixedLayoutStrategy.ts
zaewc added 2 commits May 19, 2026 14:13
|0 coerces to int32 and overflows above 2^31, producing negative indices
on lists where count * itemSize exceeds ~2.1B px — exactly the regime
this strategy is meant to support. Math.floor is correct up to
Number.MAX_SAFE_INTEGER.
When the library is imported by a script in <head>, document exists but
document.body has not been parsed yet — appendChild would throw. Fall
back to the SSR constant in that case without caching, so a later call
after the DOM is ready still performs the real probe.
@zaewc
Copy link
Copy Markdown
Owner Author

zaewc commented May 19, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@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 mechanism to handle lists that exceed the browser's maximum element size by probing the environment's limits and clamping the layout strategy's total size. The FixedLayoutStrategy was updated to calculate virtual offsets and map them to the clamped scrollable area. Feedback highlights that the strategy has become stateful, which may prevent sharing instances across multiple virtualizers. Additionally, the change in getItemOffset to return relative coordinates could break features like 'scroll to index,' and a bug was identified where getTotalSize fails to update the internal item count state.

Comment thread packages/core/src/strategies/layout/FixedLayoutStrategy.ts Outdated
Comment thread packages/core/src/strategies/layout/FixedLayoutStrategy.ts
Comment on lines 37 to 39
getTotalSize(count: number): number {
return count * this.#itemSize;
return this.#clampedTotalSize(count * this.#itemSize);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The #lastCount state is not updated within getTotalSize. If getItemOffset is called after the item count changes but before the next getVisibleRange call (which can happen during data updates), it will use a stale count for its virtual total and clamping calculations. This may lead to incorrect item positioning during the re-render cycle.

Suggested change
getTotalSize(count: number): number {
return count * this.#itemSize;
return this.#clampedTotalSize(count * this.#itemSize);
}
getTotalSize(count: number): number {
this.#lastCount = count;
return this.#clampedTotalSize(count * this.#itemSize);
}

zaewc added 2 commits May 19, 2026 14:50
…lation to Virtualizer

Restores the original contract:
- getItemOffset(index) returns absolute virtual coordinates (index * itemSize),
  stable across calls and safe to share a single strategy instance across
  multiple Virtualizers.
- getTotalSize(count) returns the (possibly clamped) spacer size.
- New optional getVirtualSize(count) on LayoutStrategy reports the unclamped
  virtual total when it differs.

The Virtualizer compares the two and, when clamping is active, translates
each item's virtual offset into render coordinates relative to the current
scroll position. Clamped layouts skip the per-render-range memoization
because item positions then depend on scrollOffset.

Addresses three review notes:
- statefulness blocking instance sharing
- getItemOffset semantics changing with scroll
- stale lastCount during data updates
@zaewc
Copy link
Copy Markdown
Owner Author

zaewc commented May 19, 2026

/gemini review

@github-actions
Copy link
Copy Markdown

📊 Test Coverage Report (vitest)

Package Statements Branches Functions Lines
@scrolloop/core 295/295 (100%) 78/81 (96.29%) 30/30 (100%) 295/295 (100%)
@scrolloop/react 420/715 (58.74%) 73/89 (82.02%) 5/17 (29.41%) 420/715 (58.74%)
@scrolloop/react-native 0/238 (0%) 1/4 (25%) 1/4 (25%) 0/238 (0%)
@scrolloop/shared 176/176 (100%) 54/54 (100%) 14/14 (100%) 176/176 (100%)

Copy link
Copy Markdown
Contributor

@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 mechanism to handle extremely large lists that exceed the browser's maximum element size limit. It adds a utility to probe the browser's height limit and updates the FixedLayoutStrategy and Virtualizer to clamp the physical container size while mapping scroll offsets to a larger virtual range. The review feedback suggests addressing a potential precision loss in the virtual offset calculation, simplifying the item positioning logic in the Virtualizer, and deduplicating the offset mapping function into a shared utility to improve maintainability.

Comment on lines +114 to +115
virtualOffset =
scrollable > 0 ? (scrollOffset * virtualScrollable) / scrollable : 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The calculation (scrollOffset * virtualScrollable) / scrollable can lead to precision loss or overflow when dealing with extremely large lists, as the intermediate product may exceed Number.MAX_SAFE_INTEGER. Calculating the ratio first is safer.

Suggested change
virtualOffset =
scrollable > 0 ? (scrollOffset * virtualScrollable) / scrollable : 0;
virtualOffset =
scrollable > 0 ? (scrollOffset / scrollable) * virtualScrollable : 0;

Comment on lines +135 to +137
const start = isClamped
? scrollOffset + (virtualStart - virtualOffset)
: virtualStart;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Since virtualOffset is initialized to scrollOffset when not clamped, the formula scrollOffset + (virtualStart - virtualOffset) is mathematically equivalent to virtualStart in the non-clamped case. You can simplify the loop by using this formula unconditionally.

        const start = scrollOffset + (virtualStart - virtualOffset);

Comment on lines 60 to 71
export function mapToVirtualOffset(
scrollOffset: number,
viewportSize: number,
virtualTotal: number,
clampedTotal: number
): number {
if (virtualTotal <= clampedTotal) return scrollOffset;
const scrollable = clampedTotal - viewportSize;
if (scrollable <= 0) return 0;
const ratio = scrollOffset / scrollable;
return ratio * (virtualTotal - viewportSize);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This mapping logic is duplicated in Virtualizer.ts. Consider moving mapToVirtualOffset to a shared utility file in src/utils to improve maintainability and ensure consistency across different layout strategies.

…rflow

- Move mapToVirtualOffset out of FixedLayoutStrategy into src/utils so the
  Virtualizer and any future LayoutStrategy share one implementation.
- Compute the ratio (scrollOffset / scrollable) before multiplying by the
  virtual scrollable range. The previous Virtualizer formula multiplied
  first and could overflow Number.MAX_SAFE_INTEGER on extreme lists.
- Drop the if-isClamped branch in the item loop. When virtualOffset
  defaults to scrollOffset (non-clamped case), the formula
  scrollOffset + (virtualStart - virtualOffset) reduces to virtualStart,
  so a single expression covers both paths.
@zaewc
Copy link
Copy Markdown
Owner Author

zaewc commented May 19, 2026

/gemini review

@github-actions
Copy link
Copy Markdown

📊 Test Coverage Report (vitest)

Package Statements Branches Functions Lines
@scrolloop/core 293/293 (100%) 75/76 (98.68%) 30/30 (100%) 293/293 (100%)
@scrolloop/react 420/715 (58.74%) 73/89 (82.02%) 5/17 (29.41%) 420/715 (58.74%)
@scrolloop/react-native 0/238 (0%) 1/4 (25%) 1/4 (25%) 0/238 (0%)
@scrolloop/shared 176/176 (100%) 54/54 (100%) 14/14 (100%) 176/176 (100%)

Copy link
Copy Markdown
Contributor

@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 support for rendering extremely large lists by implementing a clamping mechanism to bypass browser-specific element size limits. It adds a getMaxElementSize utility to probe the environment's maximum height and a mapToVirtualOffset helper to translate physical scroll positions into a larger virtual range. The FixedLayoutStrategy and Virtualizer have been updated to utilize these utilities, ensuring correct item positioning and range calculation for huge datasets. Feedback was provided to enhance the robustness of the probe element's styling to prevent interference from global CSS.

Comment thread packages/core/src/utils/getMaxElementSize.ts
@zaewc zaewc merged commit e58aad4 into develop May 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

가상 스크롤에도 임계값이 있다는 글을 봤습니다

1 participant