How to Build a High‑Performance Infinite‑Scroll Feeds Component with Virtual Scrolling
This article explains the design and implementation of a reusable feeds component that supports multiple layout modes, renders different data types, and uses virtual scrolling to keep DOM size low and maintain smooth performance during infinite scrolling.
Requirement Analysis
Infinite‑scroll feeds continuously load new data as the user scrolls, which creates a large number of DOM nodes and leads to jank. Virtual scrolling is introduced to limit the number of rendered nodes by rendering only the items that are visible in the viewport.
Supported Layout Modes
The component supports four layout configurations based on column count and whether the item height is fixed:
Single column, fixed height ("one‑row‑fixed")
Single column, variable height ("one‑row‑variable")
Two‑column, fixed height ("two‑row‑fixed")
Two‑column, variable height ("two‑row‑variable") – items are always placed on the shorter side.
Data‑type Rendering
Feeds often contain heterogeneous items (image‑text cards, product cards, video cards). Each item carries a type field, allowing the component to render the appropriate UI module based on a registration map.
Virtual Scrolling Implementation
When the page scrolls, new feed data is fetched and rendered. To avoid performance degradation, the component calculates the visible range ( startIndex ‑ endIndex) from the current scroll position and renders only items within that range.
useEffect(() => {
const onScroll = throttle(() => {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
const { startIndex, endIndex } = getRenderDataIndex(feedsItemStyles.current, { scrollTop, clientHeight, scrollHeight });
// render items between startIndex and endIndex
}, 200);
window.addEventListener('scroll', onScroll);
}, []);
function findTargetIndex(styles, target) {
let left = 0, right = styles.length - 1, ans = styles.length;
while (left <= right) {
const mid = Math.floor((right - left) / 2) + left;
if (target <= styles[mid].top) { ans = mid; right = mid - 1; }
else { left = mid + 1; }
}
return ans;
}
function getRenderDataIndex(feedsStyles, { scrollTop, clientHeight, scrollHeight }) {
const start = Math.max(scrollTop, 0);
const end = Math.min(scrollTop + clientHeight, scrollHeight);
let startIndex = 0;
if (start !== 0) {
const target = findTargetIndex(feedsStyles, start) - 5;
startIndex = Math.max(target, 0);
}
// endIndex calculation omitted for brevity
return { startIndex, endIndex: 0 };
}Layout Calculation Logic
The component distinguishes between fixed (single column) and grid (two‑column) modes. The itemSize parameter can be a constant number or a function that returns a height based on the item type.
const calculateItemStyle = (componentStyles, data, { itemSize = undefined, model = 'fixed' } = {}) => {
if (typeof itemSize === 'undefined') throw new Error('itemSize must be a function or number');
if (model === 'fixed') {
data.forEach(item => {
const size = typeof itemSize === 'function' ? itemSize(item) : itemSize;
const last = componentStyles[componentStyles.length - 1] || { top: 0, height: 0 };
componentStyles.push({ left: 0, top: last.top + last.height, height: size, width: '100%' });
});
} else {
let leftFeeds = 0, rightFeeds = 0;
const calculateGridPosition = (() => {
return (styles, size) => {
let itemStyle;
if (leftFeeds <= rightFeeds) {
itemStyle = { left: '0', top: leftFeeds, direction: 'left' };
leftFeeds += size;
} else {
itemStyle = { left: '50%', top: rightFeeds, direction: 'right' };
rightFeeds += size;
}
styles.push({ left: itemStyle.left, top: itemStyle.top, height: size, width: '100%', direction: itemStyle.direction });
};
})();
data.forEach(item => {
const size = typeof itemSize === 'function' ? itemSize(item) : itemSize;
calculateGridPosition(componentStyles, size);
});
}
return componentStyles;
};Component Usage
Developers specify the layout mode and provide an itemSize function that returns the height for each item type. The component registers custom item components once, and the rendering core maps each data item's type to the registered component.
<FeedsComponent
mode="grid" // "fixed" for single column, "grid" for two‑column
itemSize={item => {
if (item.type === 'video') return /* video height logic */;
if (item.type === 'banner') return 300;
return /* default height */;
}}
/> feedsComponent.registerComponent({
video: Video,
item: Item
}); let feedsItemMap = {};
function Feeds(props) {}
Feeds.registerItemComponent = function(componentMap) {
feedsItemMap = componentMap;
};
<div class="feeds-container">
{data.map(item => createElement(feedsItemMap[item.type], { ...item, index }))}
</div>Conclusion
By abstracting layout variations, data‑type rendering, and virtual scrolling into a single reusable component, developers can assemble infinite‑scroll feeds quickly while keeping the DOM size low and maintaining high performance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
