Framework7 Virtual List vs. Standard List View: A Decision Guide for Large Collections
Decide between Framework7's standard List View and Virtual List with a compact trade-off table, a working virtualList.create example, and a devtools-based validation plan.
03 Mar 2026, 03:32 UTC

If your Framework7 app renders a list that can grow past a few hundred rows — a message history, a product catalog, a log viewer — the default List View will eventually hurt you. Every row becomes real DOM, and mobile WebViews start dropping frames long before desktop browsers complain. Framework7 ships a Virtual List specifically for this case, but it comes with real constraints. This guide gives you the decision criteria, the trade-offs, and a working implementation sketch you can validate yourself.
The decision in one paragraph
Use the standard List View when your list is short (roughly under 200–300 rendered rows), when rows have unpredictable heights, or when you rely on automatic re-rendering from Framework7's reactivity or Vue/React/Svelte bindings. Switch to the Virtual List when the item count can reach the thousands, when you can compute each row's height, and when you're willing to manage data updates through the Virtual List API instead of mutating the DOM.
How the two options compare
| Criterion | Standard List View | Virtual List |
|---|---|---|
| DOM size | One node tree per item; grows with data | Only visible rows plus a small buffer; roughly constant |
| Scroll performance at 1,000+ items | Degrades; long layout and paint times | Stable; rows are recycled as you scroll |
| Row heights | Free-form; content can be any height | Fixed height or a reliable dynamic-height calculation required |
| Data updates | Automatic with reactivity / framework bindings | Manual: update() or replaceAllItems() |
| Per-row state | Safe to keep in DOM (event handlers, expanded state) | Unsafe — recycled rows leak DOM state; keep state in the data array |
| Search/filter UI | Trivial with reactive bindings | Must rebuild the items array and call the update API |
| Setup effort | Minimal | Moderate: renderItem template, height config, update discipline |
Why the height constraint matters
The Virtual List positions rows absolutely based on their cumulative heights. You either pass a fixed height (in pixels) for all rows or a dynamicHeight callback that computes each item's height from its data. If your computed heights don't match the real rendered heights, you'll see scroll-position jumps, blank gaps, or rows overlapping. If your rows contain variable-length text or images of unknown size, either cap them to a fixed layout (truncated text, fixed thumbnail) or stay with the standard list.
The recycled-row trap
Virtual List reuses DOM nodes as they scroll out of view. Anything you attach to a row node — a class toggled by a tap, an inline expanded panel, a checkbox state — will reappear on a completely different item later. The rule: all per-item state lives in your items array, and renderItem (or your template) derives the row's appearance purely from that item's data. If a row needs to expand, set item.expanded = true in the data and call virtualList.update().
Implementation sketch (vanilla JS)
The following creates a Virtual List for 5,000 contact-style rows. Run it in a page whose HTML contains <div class="list virtual-list"><ul></ul></div> inside a Framework7 view. No special permissions are needed; this is ordinary client-side code.
// items: plain array of data objects — the single source of truth
const items = Array.from({ length: 5000 }, (_, i) => ({
id: i,
title: `Contact ${i}`,
subtitle: `+1 555 000 ${String(i).padStart(4, '0')}`,
}));
const vl = app.virtualList.create({
el: '.virtual-list',
items: items,
height: 68, // px; must match your row's real rendered height
renderItem(item) {
return `
<li>
<div class="item-content">
<div class="item-inner">
<div class="item-title">${item.title}</div>
<div class="item-after">${item.subtitle}</div>
</div>
</div>
</li>`;
},
});
// After fetching new data — do NOT touch the DOM directly:
function applyFilter(query) {
const filtered = items.filter(i =>
i.title.toLowerCase().includes(query.toLowerCase())
);
vl.replaceAllItems(filtered);
}Two things to check after wiring this up: the height value must equal the actual rendered row height (inspect one row in devtools and confirm), and every data change must go through vl.update(), vl.replaceAllItems(), or the item-level methods — direct DOM edits will be overwritten on the next scroll.
Vue, React, and Svelte variants
The Framework7 component packages wrap the same engine. In the Vue package, for example, you add the virtual-list prop to <f7-list> and provide virtual-list-params (items, height, renderItem). The decision criteria and constraints are identical — recycled rows, height requirements, and manual update semantics still apply underneath the component. Prop names have changed across major Framework7 versions, so confirm against the typings or bundled examples of the version installed in your project before copying prop names from older tutorials.
Validating the choice
Don't take the performance claim on faith — measure it in your own shell page:
- Render the same 5,000 items twice: once as a standard
<div class="list">with rows in a loop, once as a Virtual List. - In browser devtools, compare DOM node counts (Elements panel or
document.querySelectorAll('li').lengthin the console). The standard list should show ~5,000 rows; the Virtual List should hold roughly constant at visible-rows-plus-buffer. - Record a scroll in the Performance profiler for both. Look for dropped frames and long layout/paint blocks in the standard list.
- Trigger an append or filter and confirm the Virtual List only reflects it after your update API call — this verifies your update path, not just rendering.
If the Virtual List shows blank areas or jumping during fast scroll, your height configuration is wrong or your rows render taller than declared — fix that before blaming the engine.
Limitations to plan around
- Search-as-you-type costs more code: every keystroke rebuilds the items array and calls
replaceAllItems(), versus a one-line computed filter in a reactive standard list. - Truly variable-height content (long-form text, user-generated media) is a poor fit; inaccurate
dynamicHeightresults are worse than the performance problem you started with. - API names and component props differ across Framework7 major versions. Verify
virtualList.createparameters against your installed version's documentation or TypeScript definitions.
The short version: reach for the Virtual List when row count is the problem and heights are knowable; keep the standard List View when simplicity, variable content, or reactive filtering matters more than raw scroll throughput.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.