Skip to content

Conversation

@976520
Copy link
Owner

@976520 976520 commented Dec 29, 2025

No description provided.

@976520 976520 self-assigned this Dec 29, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @976520, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a series of enhancements across the scrolloop library, focusing on improving the robustness and performance of its core virtual scrolling mechanisms. Changes range from adopting modern JavaScript features for better encapsulation and optimizing numerical computations, to refining the build process for smaller and faster bundles. Additionally, the documentation and example components have been updated to reflect broader support and provide a smoother user experience, particularly concerning SSR and animation handling.

Highlights

  • Core Logic Refinements: Internal properties in FixedLayoutStrategy, VirtualScrollSource, and Virtualizer have been converted to private class fields (#property) for improved encapsulation. Numerical calculations for startIndex and renderStart in FixedLayoutStrategy and calculateVirtualRange were optimized using the bitwise OR | 0 operator. The Virtualizer now memoizes virtual item calculations to prevent unnecessary re-renders when the visible range remains unchanged.
  • Build Configuration Optimizations: The tsup.config.ts files across @scrolloop/core, @scrolloop/react, @scrolloop/react-native, and @scrolloop/shared have been updated. This includes increasing terser compression passes to 3, adding more aggressive terser options for minification, and adjusting the target ES versions and external dependencies for better build efficiency and compatibility.
  • Documentation and Website Enhancements: The README.md has been significantly updated with new installation instructions for both React and React Native, revised descriptions, and a new 'Packages' section. The documentation site's favicon was updated to SVG, and the 'SlotMachine' animation component received a major refactor for smoother transitions and better animation management.
  • React Component Streamlining: The InfiniteList.tsx component underwent a refactor to streamline Server-Side Rendering (SSR) data handling using useMemo, simplify initial page loading, and consolidate error/loading/empty state rendering logic. The useTransition.ts hook was optimized with minified variable names and simplified event listener setups, while domPruner.ts now uses helper functions for requestIdleCallback and cancelIdleCallback.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

📊 Test Coverage Report (vitest)

Package Statements Branches Functions Lines
@scrolloop/core 218/218 (100%) 54/55 (98.18%) 25/25 (100%) 218/218 (100%)
@scrolloop/react 510/817 (62.42%) 101/122 (82.78%) 8/21 (38.09%) 510/817 (62.42%)
@scrolloop/react-native 0/237 (0%) 1/4 (25%) 1/4 (25%) 0/237 (0%)
@scrolloop/shared 0/119 (0%) 1/7 (14.28%) 1/7 (14.28%) 0/119 (0%)

Copy link
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

The pull request primarily focuses on refactoring and optimizing the scrolloop library, particularly its core virtualizer logic and documentation. Key changes include updating the README.md with separate installation instructions for React and React Native, removing InfiniteList examples, and introducing a 'Packages' section. The core virtualizer (Virtualizer.ts) was refactored to use private class fields (#) for better encapsulation and introduced memoization for virtualItems to prevent unnecessary re-renders. Integer conversions in layout and range calculations now use the bitwise OR operator (| 0) for conciseness. The SlotMachine.vue component in the documentation was significantly refactored to animate digit strips instead of individual digits, improving animation smoothness, and its watch logic was updated for better cleanup and animation handling. Build configurations (tsup.config.ts files) were updated with more aggressive Terser compression settings and adjusted target ES versions. The InfiniteList.tsx component saw a major refactor to simplify SSR data handling, remove several useMemo and useCallback definitions by inlining styles and render functions, and optimize prefetching logic. Review comments highlighted the need to re-memoize the renderItem prop in InfiniteList to avoid performance issues, to use more descriptive variable names in domPruner.ts for readability, to use a more specific type for cleanupTimer in SlotMachine.vue, and to revert a change in Virtualizer.ts that replaced Math.max/Math.min with less readable conditional clamping for renderRange.

Comment on lines +236 to +238
renderItem={(index, itemStyle) =>
renderItem(mergedAllItems[index], index, itemStyle)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Creating a new function for the renderItem prop on every render of InfiniteList will break the memoization of the VirtualList component, causing it to re-render unnecessarily. This can lead to performance degradation, especially in a scrolling component.

You should memoize this callback using useCallback as it was before this change. You can re-introduce the memoized callback before the return statement:

const virtualListRenderItem = useCallback(
  (index: number, itemStyle: CSSProperties) => {
    return renderItem(mergedAllItems[index], index, itemStyle);
  },
  [mergedAllItems, renderItem]
);
Suggested change
renderItem={(index, itemStyle) =>
renderItem(mergedAllItems[index], index, itemStyle)
}
renderItem={virtualListRenderItem}

Comment on lines 19 to 71
export function pruneOffscreenDOMIdle(
container: HTMLElement,
visibleRange: { start: number; end: number },
onPrune: (index: number) => void
): () => void {
let cancelled = false;
let requestId: ReturnType<typeof requestIdleCallback> | null = null;

const pruneChunk = () => {
range: { start: number; end: number },
onPrune: (idx: number) => void
) {
let id: any,
cancelled = false;
const prune = () => {
if (cancelled) return;

const items = container.querySelectorAll("[data-item-index]");
let pruned = 0;
const maxPrunePerFrame = 5;

for (const item of items) {
if (pruned >= maxPrunePerFrame) break;

const index = parseInt(item.getAttribute("data-item-index") || "-1", 10);
if (index < 0) continue;

if (index < visibleRange.start || index > visibleRange.end) {
onPrune(index);
pruned++;
}
}

if (pruned > 0 && !cancelled) {
requestId = requestIdleCallback(pruneChunk);
}
let count = 0;
container.querySelectorAll("[data-item-index]").forEach((el) => {
if (count++ > 5) return;
const i = parseInt(el.getAttribute("data-item-index") || "-1", 10);
if (i >= 0 && (i < range.start || i > range.end)) onPrune(i);
});
if (count > 0 && !cancelled) id = RIC(prune);
};

requestId = requestIdleCallback(pruneChunk);

id = RIC(prune);
return () => {
cancelled = true;
if (requestId !== null) {
cancelIdleCallback(requestId);
}
CIC(id);
};
}

export function pruneOffscreenDOMChunk(
container: HTMLElement,
visibleRange: { start: number; end: number },
chunkSize: number,
onPrune: (index: number) => void
): () => void {
let cancelled = false;
let timeoutId: number | null = null;

const pruneChunk = () => {
range: { start: number; end: number },
chunk: number,
onPrune: (idx: number) => void
) {
let id: any,
cancelled = false;
const prune = () => {
if (cancelled) return;

const items = Array.from(container.querySelectorAll("[data-item-index]"));
const offscreenItems = items.filter((item) => {
const index = parseInt(item.getAttribute("data-item-index") || "-1", 10);
return (
index >= 0 && (index < visibleRange.start || index > visibleRange.end)
);
});

const chunk = offscreenItems.slice(0, chunkSize);
chunk.forEach((item) => {
const index = parseInt(item.getAttribute("data-item-index") || "-1", 10);
if (index >= 0) {
onPrune(index);
const items = [...container.querySelectorAll("[data-item-index]")].filter(
(el) => {
const i = parseInt(el.getAttribute("data-item-index") || "-1", 10);
return i >= 0 && (i < range.start || i > range.end);
}
});

if (offscreenItems.length > chunkSize && !cancelled) {
timeoutId = window.setTimeout(pruneChunk, 16) as unknown as number;
}
);
items
.slice(0, chunk)
.forEach((el) =>
onPrune(parseInt(el.getAttribute("data-item-index")!, 10))
);
if (items.length > chunk && !cancelled) id = setTimeout(prune, 16);
};

timeoutId = window.setTimeout(pruneChunk, 16) as unknown as number;

id = setTimeout(prune, 16);
return () => {
cancelled = true;
if (timeoutId !== null) {
window.clearTimeout(timeoutId as any);
}
clearTimeout(id);
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Variable and parameter names throughout this file have been shortened to the point of being cryptic (e.g., range, idx, prune, id, i, chunk). This significantly reduces code readability and makes it difficult to maintain. Please use more descriptive names like visibleRange, index, pruneTask, etc., to improve code clarity.

const prevValue = ref(props.value);
const lastChangeTime = ref(Date.now());
const animationId = ref(0);
const cleanupTimer = ref<any>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using any for the cleanupTimer ref is not type-safe and bypasses TypeScript's type checking. It's better to provide a more specific type for better safety and code clarity.

const cleanupTimer = ref<ReturnType<typeof setTimeout> | null>(null);

Comment on lines 93 to +100
let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.overscan),
endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan),
startIndex: (visibleRange.startIndex - this.#overscan) | 0,
endIndex: (visibleRange.endIndex + this.#overscan) | 0,
};

for (const plugin of this.plugins) {

if (renderRange.startIndex < 0) renderRange.startIndex = 0;
if (renderRange.endIndex > this.#count - 1)
renderRange.endIndex = this.#count - 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This block of code is a more verbose and less clear way of achieving what was previously done with Math.max and Math.min. The | 0 bitwise OR is redundant here as the operands are integers. Using Math.max and Math.min is more idiomatic and readable for clamping values within a range.

Suggested change
let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.overscan),
endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan),
startIndex: (visibleRange.startIndex - this.#overscan) | 0,
endIndex: (visibleRange.endIndex + this.#overscan) | 0,
};
for (const plugin of this.plugins) {
if (renderRange.startIndex < 0) renderRange.startIndex = 0;
if (renderRange.endIndex > this.#count - 1)
renderRange.endIndex = this.#count - 1;
let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.#overscan),
endIndex: Math.min(this.#count - 1, visibleRange.endIndex + this.#overscan),
};

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.

3 participants