Framework7 Virtual List: Rendering Large Datasets Without Mobile Jank
Framework7's Virtual List keeps long lists smooth by rendering only visible rows, but it demands uniform item heights and careful scroll-container management. Here is how to integrate it with Infinite Scroll and when to avoid it.
28 Mar 2026, 23:36 UTC

Rendering a thousand contact rows in a mobile browser is a fast way to ruin scrolling. Each <li> you append becomes a DOM node the browser must lay out, paint, and keep in memory. On a mid-range phone, the main thread stalls, frames drop, and the user feels the lag. Framework7's Virtual List component exists to solve exactly this problem: it renders only the rows currently visible on screen, plus a small buffer, and recycles DOM nodes as the user scrolls.
The trade-off is strict. Virtual List requires every item to share a known, uniform height. If your rows vary in size, the internal scroll math breaks and the list jumps. Choosing Virtual List is therefore an engineering decision about your data shape, not just a performance toggle.
Why Long Lists Hurt Mobile Performance
A standard Framework7 List is a plain unordered list. When you bind it to an array of 1,000 objects, you create 1,000 DOM elements. Mobile browsers have tighter memory and CPU budgets than desktops. Every scroll event triggers style recalculation and layout across that entire tree. Even if most rows are off-screen, the browser still tracks them.
Virtualization fixes this by maintaining a sliding window. The component calculates which items intersect the viewport, renders those plus a few extra above and below, and removes the rest. When you scroll, it reuses the same DOM nodes for new data instead of creating new elements. The result is a constant DOM size regardless of dataset length.
The Uniform Height Contract
Virtual List needs to know exactly how tall each row is so it can map scroll offset to data index. You supply this via an itemHeight parameter, or you ensure every row renders at the same pixel height. If one row contains a long address that wraps to three lines while others are single-line, the calculation drifts. The scroll position no longer matches the data, and the list appears to jump or leave blank gaps.
This constraint is not a bug; it is the cost of the optimization. If your dataset has variable heights, Virtual List is the wrong tool unless you can normalize the data—for example, by truncating text or using a fixed-height layout with internal scrolling.
Paging Data with Infinite Scroll
A common pattern is pairing Virtual List with Infinite Scroll. Virtual List handles the rendering window; Infinite Scroll handles fetching the next page from your API. When the user nears the bottom, Infinite Scroll fires, you fetch more records, and you update the Virtual List's dataset.
Because Virtual List recycles DOM nodes, event handlers attached directly to individual list items will not behave reliably. A handler bound to row 50 might later represent row 200 after recycling. Instead, use event delegation: attach one listener to the list container and inspect event.target to find the clicked row. In the Vue, React, and Svelte wrappers, the same recycling behavior means unstable render callbacks can show stale data unless your items are properly keyed or memoized.
One more container rule: Virtual List manages its own internal scroll element. Do not nest it inside another scrollable <div> or apply CSS transforms to ancestors. Doing so can clip the list or break momentum scrolling on iOS and Android.
Worked Example: Virtual List with Infinite Scroll
The example below uses Framework7 Core syntax. If you are working in Vue, React, or Svelte, the wrapper components expose similar behavior but the API surface differs slightly; consult the version-specific documentation. The list container needs a constrained height so Virtual List can calculate its window.
<div class="page">
<div class="page-content">
<div class="list virtual-list" id="contacts-list"></div>
<div class="preloader infinite-scroll-preloader"></div>
</div>
</div>
Initialize the list with a uniform row height and a render function:
const contactsList = app.virtualList.create({
el: '#contacts-list',
items: initialContacts,
itemHeight: 88,
renderItem: (item) => `
<li>
<a href="#" class="list-item">
<div class="item-inner">
<div class="item-title">${item.name}</div>
</div>
</a>
</li>`
});
When Infinite Scroll loads the next page, merge it into the dataset and tell Virtual List to recalculate:
let allContacts = [...initialContacts];
let page = 1;
app.on('infinite', async () => {
const nextPage = await fetchContacts(page++);
if (!nextPage.length) return;
allContacts = [...allContacts, ...nextPage];
contactsList.replaceAllItems(allContacts);
});
Delegate clicks on the container rather than individual rows:
document.getElementById('contacts-list').addEventListener('click', (event) => {
const row = event.target.closest('li');
if (!row) return;
// Handle the click using the row's data
});
When to Skip Virtual List
For small datasets—roughly under 50 items—the setup cost of Virtual List is not worth it. A standard List component is simpler to debug, more accessible to screen readers, and avoids the uniform-height constraint entirely. Reach for Virtual List when your dataset is large enough that DOM size, not fetch time, is the bottleneck.
Also beware CSS conflicts. Browser zoom on mobile can change effective pixel heights, breaking the itemHeight assumption. If your page uses overflow: hidden or transforms on ancestors, test carefully; Virtual List's internal scroll calculations can conflict with those styles.
Verifying the Result
To confirm Virtual List is working, build a page with 1,000 items and inspect the DOM. You should see only a small subset of <li> elements at any time, not 1,000. Scroll rapidly and open your browser's DevTools Performance panel; frame rates should remain steadier than with a standard list. Finally, test dynamic updates by loading a new page and confirming the scroll offset stays aligned.
Virtual List is a sharp tool. It solves a real mobile performance problem, but only if your data fits its rules. Measure your dataset, check your row heights, and delegate your events—then let the component do the windowing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.