跪拜 Guibai
← All articles
Vue.js · Frontend · TypeScript

A Vite Plugin Splits Lucide Icons into 5 Chunks by First Letter for True On-Demand Loading

By 妙码生花 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Icon libraries like Lucide are large enough to bloat bundles, but dynamic admin UIs can't rely on static imports. This pattern solves both sides of the trade-off without per-icon chunk spam or manual maintenance of split points.

Summary

Dynamic icon selectors need every icon available, but regular pages should load only the few they actually render. Existing approaches fail: per-icon dynamic imports create thousands of chunks and network requests, while static tree-shaking can't handle runtime icon names.

The fix is a Vite plugin that reads Lucide's ESM icon directory at build time and generates five virtual modules—a-c, d-l, m-p, q-s, t-z—each exporting every icon in its range. A runtime helper maps an icon name to the correct batch, calls `defineAsyncComponent`, and caches the result. The icon selector triggers all five chunks; a page using a single `Smile` icon loads only the a-c chunk.

Build output stays clean: five extra chunks totaling roughly 500 KB uncompressed (around 130 KB gzipped), with no per-icon file fragmentation. The plugin uses explicit imports instead of re-exports so Rolldown inlines all icons into one chunk per batch.

Takeaways
Per-icon dynamic imports with `import()` produce one chunk per icon—2,000 icons means 2,000 extra files and 2,000 network requests on the selector page.
Static tree-shaking via `unplugin-auto-import` cannot handle icon names that come from runtime configuration strings.
The Vite plugin reads the Lucide ESM icons directory, groups files by first-letter range into five virtual modules, and generates explicit named exports so all icons in a batch land in a single chunk.
A runtime `getLucideComponent` function maps a kebab-case icon name to the correct batch, calls `defineAsyncComponent`, and caches the result so repeated renders hit the cache.
Production builds add five chunks (a-c: 144 KB, d-l: 127 KB, m-p: 75 KB, q-s: 97 KB, t-z: 64 KB) plus a tiny shared dependency chunk; the icon selector loads all five, while a normal page loads only the batch matching the icon's first letter.
Development mode skips the split and loads the full Lucide package for simplicity.
Conclusions

Virtual modules are the key enabler here: they let the plugin define chunk boundaries at build time without writing intermediate files to disk, so there is zero ongoing maintenance when the icon library updates.

Grouping by first letter is a pragmatic heuristic that balances chunk count against per-page waste. The largest chunk (a-c, 514 icons) is still only 144 KB uncompressed, which is acceptable even if a page uses a single icon from that range.

Using explicit `import` + `export const` instead of re-exports is a deliberate Rolldown optimization—it forces all icons in a batch into one chunk rather than letting the bundler split them again.

The runtime cache on `lucideCache` prevents `defineAsyncComponent` from creating a new async wrapper on every render, which would otherwise cause flickering as the same icon re-resolves.

Element Plus icons are handled separately via global registration because they are already split into individual components; the Lucide approach is necessary only when a library ships as a flat directory of icon files.

Concepts & terms
Virtual module (Vite/Rollup)
A module that doesn't exist on disk but is generated at build time by a plugin through the `resolveId` and `load` hooks. The plugin intercepts imports matching a prefix and returns source code as a string.
defineAsyncComponent (Vue)
A Vue 3 function that creates a component whose definition is loaded asynchronously. It returns a wrapper component that renders a placeholder until the real component's promise resolves.
Rolldown chunk inlining
When a module uses explicit named imports and exports (rather than `export * from`), bundlers like Rolldown can inline the imported module's code into the parent chunk, avoiding further code splitting.
Tree-shaking vs. runtime dynamic loading
Tree-shaking removes unused exports at build time by analyzing static import graphs. It cannot eliminate code when the import path is a runtime string, which is why dynamic icon systems need a different strategy.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗