Bundle Size & Tree-Shaking #
How vlist achieves 2-3x smaller bundles through explicit feature imports and perfect tree-shaking.
Overview #
VList uses a builder pattern with explicit features. You import only the features you need, and bundlers eliminate everything else.
import { vlist, withGrid, withSelection } from '@floor/vlist';
const gallery = vlist({ ... })
.use(withGrid({ columns: 4 }))
.use(withSelection({ mode: 'multiple' }))
.build();
// Bundle: 12.6 KB gzipped
// NOT included: withGroups, withAsync, withScale, withScrollbar, withPage
Result: 8-12 KB gzipped average (vs 20-23 KB for traditional virtual lists).
Actual Bundle Sizes #
All measurements from production builds (minified + gzipped):
Real-World Examples #
| Example | Minified | Gzipped | Features Used |
|---|---|---|---|
| Basic list | 22.5 KB | 8.2 KB | None |
| Controls | 30.6 KB | 10.5 KB | withSelection() |
| Photo gallery | 34.3 KB | 11.7 KB | withGrid() + withScrollbar() |
| Contact list | 34.3 KB | 12.3 KB | withGroups() |
| Chat UI | 34.2 KB | 11.9 KB | withGroups() (inline) |
| Infinite scroll | 38.2 KB | 13.5 KB | withAsync() + withPage() |
| Large dataset | 31.9 KB | 9.9 KB | withScale() + withScrollbar() |
| File browser | 46.2 KB | 15.3 KB | withGrid() + withGroups() + withScrollbar() |
Feature Costs #
Each feature adds incrementally to the base bundle:
| Feature | Incremental Cost (Gzipped) | Description |
|---|---|---|
| Base | 7.7 KB | Core virtualization |
withGrid() |
+4.0 KB | 2D grid layout |
withGroups() |
+4.6 KB | Grouped lists with headers |
withAsync() |
+5.3 KB | Async data loading |
withSelection() |
+2.3 KB | Item selection & keyboard nav |
withScale() |
+2.2 KB | Handle 1M+ items |
withScrollbar() |
+1.0 KB | Custom scrollbar UI |
withPage() |
+0.9 KB | Document-level scrolling |
withSnapshots() |
Included | Scroll save/restore (no cost) |
Comparison: Before vs After #
Traditional Virtual Lists #
import { VirtualList } from 'some-virtual-list';
const list = new VirtualList({
container: '#app',
items: data,
height: 48,
});
// Bundle: 62-70 KB minified / 20-23 KB gzipped
// Includes: ALL features whether you use them or not
VList (Builder Pattern) #
import { vlist } from '@floor/vlist';
const list = vlist({
container: '#app',
items: data,
item: { height: 48, template: renderItem },
}).build();
// Bundle: 22.5 KB minified / 8.2 KB gzipped
// Includes: ONLY core virtualization
// Savings: 60% smaller!
With Features #
import { vlist, withGrid, withSelection } from '@floor/vlist';
const list = vlist({ ... })
.use(withGrid({ columns: 4 }))
.use(withSelection({ mode: 'multiple' }))
.build();
// Bundle: ~34 KB minified / ~12 KB gzipped
// Includes: Core + Grid + Selection ONLY
// Savings: 40-45% smaller than traditional!
How Tree-Shaking Works #
Explicit Imports #
VList exports everything from a single entry point, allowing perfect tree-shaking:
// vlist/src/index.ts
export { vlist } from './builder';
export { withGrid } from './features/grid';
export { withGroups } from './features/groups';
export { withAsync } from './features/async';
export { withSelection } from './features/selection';
export { withScale } from './features/scale';
export { withScrollbar } from './features/scrollbar';
export { withPage } from './features/page';
export { withSnapshots } from './features/snapshots';
When you write:
import { vlist, withGrid } from '@floor/vlist';
Bundler includes:
- ✅
vlistfunction (builder core) - ✅
withGridfunction and its dependencies - ❌ Everything else (not imported, eliminated)
What Gets Eliminated #
import { vlist, withGrid } from '@floor/vlist';
// These are exported but NOT imported, so bundler eliminates them:
// - withGroups and all its code
// - withAsync and all its code
// - withSelection and all its code
// - withScale and all its code
// - withScrollbar and all its code
// - withPage and all its code
const list = vlist({ ... })
.use(withGrid({ columns: 4 }))
.build();
// Final bundle: 11.7 KB gzipped
// Eliminated: ~10 KB of unused features
Bundle Analysis #
Minimal Configuration #
import { vlist } from '@floor/vlist';
vlist({ ... }).build();
Bundle:
- vlist function: 6.3 KB gzipped
- Builder core: 1.4 KB gzipped
- Total: 7.7 KB gzipped
Medium Configuration #
import { vlist, withSelection, withScrollbar } from '@floor/vlist';
vlist({ ... })
.use(withSelection({ mode: 'single' }))
.use(withScrollbar({ autoHide: true }))
.build();
Bundle:
- Base: 7.7 KB gzipped
- withSelection: +2.3 KB gzipped
- withScrollbar: +1.0 KB gzipped
- Total: 11.0 KB gzipped
Full Configuration #
import {
vlist,
withGrid,
withGroups,
withSelection,
withAsync,
withScale,
withScrollbar
} from '@floor/vlist';
vlist({ ... })
.use(withGrid({ columns: 4 }))
.use(withGroups({ ... }))
.use(withSelection({ mode: 'multiple' }))
.use(withAsync({ adapter }))
.use(withScale())
.use(withScrollbar({ autoHide: true }))
.build();
Bundle:
- Base: 7.7 KB gzipped
- Features: +8.3 KB gzipped
- Total: ~16 KB gzipped
Still smaller than traditional virtual lists!
Optimization Strategies #
1. Import Only What You Need #
❌ Don't:
import * as VList from '@floor/vlist'; // Imports everything
✅ Do:
import { vlist, withGrid } from '@floor/vlist'; // Only what you use
2. Lazy Load Heavy Features #
For features only needed in certain views, use dynamic imports:
// Base list loads immediately
const list = vlist({ ... }).build();
// Grid feature loads on demand
button.addEventListener('click', async () => {
const { withGrid } = await import('vlist');
list.destroy();
const gridList = vlist({ ... })
.use(withGrid({ columns: 4 }))
.build();
});
Benefit: Initial page load is smaller, grid code loads when needed.
3. Conditional Feature Loading #
import { vlist, withGroups } from '@floor/vlist';
let builder = vlist({ ... });
// Only add groups if grouping enabled
if (groupBy !== 'none') {
builder = builder.use(withGroups({ ... }));
}
const list = builder.build();
Benefit: Bundle includes conditional feature only if your app logic uses it.
4. Use CDN for Examples/Prototypes #
For quick prototypes, load from CDN:
<script type="module">
import { vlist, withGrid } from 'https://cdn.jsdelivr.net/npm/@floor/vlist/+esm';
const list = vlist({ ... })
.use(withGrid({ columns: 4 }))
.build();
</script>
Benefit: Zero build step, browser caches the module.
Verification #
Check Your Bundle #
Use your bundler's analysis tool to verify tree-shaking:
Webpack:
npx webpack-bundle-analyzer stats.json
Rollup:
npx rollup-feature-visualizer
Vite:
vite build --mode production
Look for vlist modules - you should only see the ones you imported.
Expected Results #
If you imported:
import { vlist, withGrid, withSelection } from '@floor/vlist';
Bundle analyzer should show:
- ✅
vlist/builder/core.js(builder core) - ✅
vlist/features/grid/(grid feature) - ✅
vlist/features/selection/(selection feature) - ❌ NO
features/async/ - ❌ NO
features/scale/ - ❌ NO
features/sections/ - ❌ NO
features/scrollbar/
Common Misconceptions #
"I only use basic features, why is my bundle 8 KB?" #
Answer: That's the core virtualization! It includes:
- Virtual scrolling calculations
- Element pooling and recycling
- Height cache (variable heights)
- DOM structure management
- Event system
- Scroll handling
- Data management (setItems, appendItems, etc.)
- ARIA accessibility
8 KB gzipped is very small for all that functionality.
"Adding features makes the bundle bigger" #
Answer: Yes, that's expected! Each feature adds specific functionality:
- withGrid adds 2D layout calculations
- withAsync adds data fetching and sparse storage
- withSelection adds selection state and keyboard navigation
The key is you only pay for what you use. Traditional virtual lists bundle everything regardless.
"Can I make it smaller than 8 KB?" #
Answer: Yes! There's a lightweight core (not exposed in current API) that's 3.1 KB gzipped, but it has limited features. For production apps, 8-12 KB is excellent for a full-featured virtual list.
Best Practices #
✅ Do #
- Import only the features you actually use
- Use dynamic imports for conditional features
- Check bundle analysis in production builds
- Measure before and after adding features
❌ Don't #
- Don't import features you don't use
- Don't use wildcard imports (
import * from '@floor/vlist') - Don't worry about 1-2 KB differences (focus on features)
- Don't sacrifice functionality to save bytes (8-12 KB is already tiny)
FAQ #
Q: Why is vlist smaller than other virtual lists? #
A: Three reasons:
- Builder pattern - Only used features are bundled
- Self-contained core - No module overhead
- Zero dependencies - No external libraries
Q: Does tree-shaking work with all bundlers? #
A: Yes! Tested with:
- ✅ Webpack 5
- ✅ Rollup
- ✅ Vite
- ✅ esbuild
- ✅ Parcel 2
All modern bundlers support ES modules tree-shaking.
Q: Can I see the exact bundle composition? #
A: Yes! Use webpack-bundle-analyzer or rollup-feature-visualizer. You'll see each imported module and its size.
Q: What if I need all features? #
A: Even with all features, vlist is ~16 KB gzipped - still smaller than most virtual lists with basic features!
Q: How much overhead does the builder add? #
A: ~1.4 KB gzipped. The builder core is 7.7 KB vs a hypothetical direct implementation at ~6.3 KB. The flexibility is worth it.
Summary #
| Metric | VList (Builder) | Traditional | Improvement |
|---|---|---|---|
| Minimal | 7.7 KB gzipped | 20-23 KB | 2.6-3x smaller |
| With features | 8-12 KB | 20-23 KB | 2-3x smaller |
| Full-featured | ~16 KB | 20-23 KB | 1.3-1.4x smaller |
| Tree-shaking | ✅ Perfect | ❌ None | Huge benefit |
Bottom line: VList delivers 2-3x smaller bundles by letting you import only what you need.
See Also #
- Builder Pattern - How to compose features
- Features Overview - All available features
- Optimization Guide - Performance tuning
- Benchmarks - Performance metrics
Interactive Examples: vlist.dev/examples - See bundle sizes in action