/ Docs

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:

  • vlist function (builder core)
  • withGrid function 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:

  1. Builder pattern - Only used features are bundled
  2. Self-contained core - No module overhead
  3. 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 #


Interactive Examples: vlist.dev/examples - See bundle sizes in action