Framework7 Virtual List: Render Thousands of Rows on Mobile
Render thousands of Framework7 list rows on mobile by recycling a fixed pool of DOM elements instead of mounting every item at once.
22 May 2026, 00:56 UTC

The Problem and the Useful Answer
Rendering two thousand Framework7 list items on a mobile page usually means two thousand DOM nodes, a slow first paint, and scroll that stutters while the browser lays out every row. The useful takeaway is to use Framework7’s Virtual List: it mounts only a small pool of row elements, rewrites their contents as the user scrolls, and lets the scrollbar represent the full collection without paying the layout cost for every item. The trade-off is strict: every row must share one known pixel height, and you must update the list through the virtual list API instead of mutating the rendered <ul> directly.
How the Mechanism Works
Virtual List does not create one <li> per data item. It creates enough rows to fill the visible viewport plus a small buffer, then recycles those same nodes. As scrollTop changes, the component calculates which data indexes should be visible, overwrites the recycled nodes with new content, and adjusts an invisible spacer so the browser scrollbar still matches the total item count.
- Fixed item height. The component converts scroll offset to a row index by dividing by the configured row height. If rows differ in height, the mapping breaks and the list shows blank space or jumps.
- Spacer element. A hidden block fills the scrollable length for items outside the visible window, so the scrollbar thumb stays proportional to the full dataset.
- Node recycling. The same DOM nodes are overwritten repeatedly. Event listeners or widget state attached directly to a row node do not reliably survive a scroll cycle.
Worked Configuration
This example assumes a Framework7 instance named app and a page that has already initialized. Adjust the template syntax (${title} vs {{title}}) to match your installed major version.
<div class="page">
<div class="page-content">
<div class="list virtual-list">
<ul></ul>
</div>
</div>
</div>
const items = [];
for (let i = 0; i < 2000; i += 1) {
items.push({ id: i, title: `Item ${i}` });
}
// Measure one rendered row first, including padding, border, and margin.
// const rowHeight = document.querySelector('.virtual-list li').offsetHeight;
const virtualList = app.virtualList.create({
el: '.virtual-list',
items: items,
height: 63, // placeholder: replace with measured offsetHeight
itemTemplate: `
<li>
<div class="item-content">
<div class="item-inner">
<div class="item-title">Title: ${title}</div>
</div>
</div>
</li>
`,
});
Run this JavaScript in the browser context after the page mounts, not in a terminal. Required permissions: none beyond normal page execution. The height value must match the actual rendered row height; if you copy 63 pixels without measuring, scroll position can drift near the end of the list.
Filtering and Search
Do not remove matching rows from the DOM yourself. Compute the indexes that match a query, then pass those indexes to the virtual list API.
function search(query) {
const matches = items
.map((item, index) => (item.title.toLowerCase().includes(query.toLowerCase()) ? index : -1))
.filter((index) => index !== -1);
virtualList.filterItems(matches);
}
function clearSearch() {
virtualList.resetFilter();
}
Expected check: after filterItems, the list shows only matching rows and the scrollbar reflects the filtered count. After resetFilter, all rows return. Verify through the API, not by inspecting <li> nodes directly.
Limits and Common Mistakes
- Variable-height rows. Images that load asynchronously or text that wraps to different line counts invalidate the fixed-height assumption. Set explicit width and height on images, or use a uniform placeholder.
- Embedded widgets. Accordions, form inputs, or custom components inside a row lose focus and state when the node is recycled. Rebuild them inside the item template or rebind them through Framework7 list events.
- Direct DOM mutation. Calling
appendChildon the virtual list’s<ul>desyncs the component’s internal index map. UsevirtualList.appendItem,insertItem,replaceItem, ordeleteItemwhere available, or rebuilditemsand recreate the list. - Forgotten cleanup. When the user leaves the page, call
virtualList.destroy()to remove scroll listeners and detached nodes. Otherwise the instance keeps holding memory. - Version drift. Framework7 major versions have renamed initialization and filter methods. Check
package.jsonand compare the API against your installed version before shipping.
Verification Checklist
- Create a page with at least 1,000 items.
- Measure one row:
document.querySelector('.virtual-list li').offsetHeight. - Pass that exact value to
heightinvirtualList.create. - Scroll quickly to the end. The list should reach the last item without a blank gap or jump.
- Temporarily set
heightten pixels too high or low, scroll, and observe the drift. Restore the measured value afterward; this is a diagnostic, not a production setting. - Run a search that calls
filterItems, thenresetFilter, and confirm the list updates through the API. - Run
grep '"framework7"' package.jsonand confirm the virtual list method names match your installed Framework7 major version.
Cleanup on Page Exit
Creating a virtual list attaches scroll listeners and retains a DOM pool. In the page’s before-remove or unload callback, call virtualList.destroy(). This is not a rollback of data; it is cleanup of the component’s runtime state so the browser can garbage-collect the instance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.