Framework7 Virtual List: Lazy Init and Fixed Rows for Long Mobile Feeds
Long Framework7 feeds stutter from DOM weight. Virtual List renders only visible rows with a buffer, and lazy page-enter initialization keeps memory low while preserving smooth scroll.
03 Mar 2026, 22:53 UTC

A mobile feed with several thousand rows can drop frames on a mid-range phone even when the data is already in memory. The bottleneck is usually DOM weight: every list item becomes elements the browser must style, composite, and keep in memory. Framework7’s Virtual List addresses this by rendering only the rows inside the viewport (the visible area of the screen) plus a small buffer, while a spacer element preserves the full scroll height.
The useful takeaway is to initialize the Virtual List lazily when the feed page becomes active, use a fixed row height so the component can calculate positions without measuring every row, and refresh the layout explicitly after data changes instead of recreating the list.
Why regular lists choke on long mobile feeds
A standard Framework7 list renders every item in the data array. With a few thousand rows, the page can contain thousands of list item, link, and text nodes. Scrolling forces the browser to recompute layout and composite layers for many of those nodes at once. On iOS and Android WebViews this appears as stutter during scroll and delayed response to pull-to-refresh.
Virtual List changes that contract. You pass it a data array and a render function for a single row. The component calculates which rows intersect the visible area and renders only those, plus a buffer above and below. Rows outside the window exist as data, not DOM nodes.
Lazy initialization with the Framework7 router
Creating a Virtual List when the app starts wastes work for pages the user may never open. Framework7’s router emits page lifecycle events, so a practical pattern is to create the list inside page:init for the feed page and destroy it on page:beforeout. Keep the instance in a variable scoped to that page so updates reuse the same list.
This keeps memory low when the feed is inactive and avoids upfront cost at app launch. It assumes you are using Framework7’s DOM router with access to the page instance and app object; module or Web Component builds may use different initialization APIs, so check the docs for your major version.
Worked example: fixed-height feed page
The following pattern uses a fixed row height, lazy initialization, and explicit refresh after data arrives. Replace placeholders such as the app instance and data source with your own implementation.
<div class="page" data-name="feed">
<div class="page-content">
<div class="list virtual-list" data-virtual-list-params='{"height": 56}'>
<ul></ul>
</div>
</div>
</div>
Run this script in the page context where the Framework7 app instance is available. No special permissions are required beyond normal app execution.
let virtualList = null;
let items = initialItems(); // your data array
$$(document).on('page:init', '.page[data-name="feed"]', function (e, page) {
const app = page.app;
const el = page.el.querySelector('.virtual-list');
virtualList = app.virtualList.create({
el,
items,
renderItem: function (item) {
return '<li class="item-content"><div class="item-inner"><div class="item-title">' + item.title + '</div></div></li>';
},
height: 56
});
});
$$(document).on('page:beforeout', '.page[data-name="feed"]', function () {
if (virtualList) {
virtualList.destroy();
virtualList = null;
}
});
When new data arrives, append it to the array and tell the list to recalculate. Do not recreate the Virtual List.
function appendItems(newChunk) {
items = items.concat(newChunk);
if (virtualList) {
virtualList.updateItems(items);
virtualList.refresh();
}
}
Verification and expected checks
Open browser dev tools on the feed page and inspect the DOM while scrolling. A regular list with the same data should show thousands of nodes; the Virtual List page should show only the visible rows plus a small buffer, typically tens of nodes.
Add console logs inside page:init and around virtualList.create to confirm initialization runs only when the feed page enters. Then call appendItems programmatically and verify that newly visible rows are added to the DOM without rebuilding the whole list.
Risk: fixed height means row content must fit within 56 pixels. Longer titles or wrapped text can clip. If rows vary in height, dynamic height mode is available but requires stable measurement and increases layout cost.
Trade-offs and limitations
Virtual List improves scroll performance at the cost of flexibility. It assumes a single scroll container; nested scroll areas or custom scroll libraries need explicit configuration. Complex interactive widgets inside rows can be expensive to re-render during fast scroll. Browser find-in-page and some accessibility behaviors differ from a fully rendered list because most rows are not present in the DOM at any given time.
Behavior and initialization APIs differ between major Framework7 versions, especially around Web Components and module builds. Treat this pattern as a starting point and verify against your installed version.
Closing
Use Virtual List when the feed is long, rows are mostly uniform, and smooth scrolling matters more than rendering every row up front. Keep initialization lazy, prefer fixed row height, and refresh layout explicitly after mutations. Verify with DOM node counts and lifecycle logs before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.