← Back to list

Stop shipping 1,300 icons when your app uses 12

How I built a zero-config Vite plugin for Ionic icon tree-shaking

Dominic Jean · 2026-05-23 17:45 · 0 claps · 2.5 min read
#vitejs #ionic-framework #javascript-development #javascript-frameworks #tree-shaking
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Stop shipping 1,300 icons when your app uses 12

How I built a zero-config Vite plugin for Ionic icon tree-shaking

The problem

If you’ve ever built a production app with Ionic and Vite, you’ve probably noticed something odd in your dist/ folder: a svg/ directory packed with over a thousand files, weighing in at around 4 MB — even if your entire app only displays a handful of icons.

This isn’t a bug. It’s just how Ionic works. Icons are referenced by name at runtime:

<ion-icon name="add-outline" />

Because the name is resolved dynamically by the browser, your bundler has no way of knowing at build time which SVG files are actually needed. The safe default is to copy them all.

The result: ~1,300 SVG files shipped to every user, regardless of how many you use.

The solution

I built [vite-plugin-ionic-icons](https://www.npmjs.com/package/vite-plugin-ionic-icons) — a Vite plugin that statically analyzes your source files, figures out exactly which icons you use, and emits only those as build assets.

The impact is immediate:

❌ Without the plugin — ~1,300 SVG files, ~4 MB ✅ With the plugin — only the icons you use, a few KB

How to use it

Install it as a dev dependency:

npm install -D vite-plugin-ionic-icons

Then add it to your Vite config:

// vite.config.ts
import { defineConfig } from 'vite';
import ionicIcons from 'vite-plugin-ionic-icons';
export default defineConfig({
  plugins: [
    ionicIcons(),
  ],
});

That’s it. No other configuration needed.

In dev mode, icons are served on-demand directly from node_modules via a lightweight middleware — nothing is copied, and your dev server starts instantly.

In build mode, the plugin scans your source files, collects every icon name it finds, and calls Vite’s emitFile() for each one — only those, nothing more.

Framework support

The scanner handles all common frameworks through a set of regex patterns tuned to each syntax.

React / JSX / TSX

<ion-icon name="add-outline" />
<ion-icon name="trash" size="small" />

Vue

<ion-icon name="add-outline" />
<ion-icon :name="'add-outline'" />

Angular

<ion-icon name="add-outline"></ion-icon>
<ion-icon [name]="'add-outline'"></ion-icon>

Svelte

<ion-icon name="add-outline" />

Mithril / Preact h()

m('ion-icon', { name: 'add-outline' })
h('ion-icon', { name: 'add-outline' })

Plain HTML

<ion-icon name="add-outline"></ion-icon>

What about dynamic icon names?

If you resolve icon names at runtime — from an API response, a config file, or a variable — the static scanner can’t detect them. For those cases, use extraIcons:

ionicIcons({
  extraIcons: ['warning-outline', 'checkmark-circle', 'close-circle'],
})

These will always be included in the build output regardless of what the scanner finds.

How it works under the hood

The plugin hooks into two Vite lifecycle methods:

**configureServer* (dev mode) registers a middleware that intercepts any request to `/svg/.svgand pipes the file directly fromnode_modules/@ionic/core/dist/ionic/svg/`. No disk writes, no temp files.

**generateBundle** (build mode) runs the scanner once against your source directory, deduplicates the collected icon names, then calls this.emitFile() for each one. The scan result is cached so it only runs once per build session.

Build time
  └── generateBundle()
        ├── scan srcDir recursively
        ├── apply regex patterns (one per framework)
        ├── deduplicate icon names
        └── emitFile() for each used icon → dist/svg/<name>.svg
Dev time
  └── configureServer()
        └── middleware: GET /svg/<name>.svg
              └── pipe from node_modules/@ionic/core/dist/ionic/svg/<name>.svg

All available options

ionicIcons({
  // Directory (or array of dirs) to scan. Default: './src'
  srcDir: './src',
  // File extensions to include. Default: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte']
  extensions: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte'],
  // Icons to always include (for dynamic names). Default: []
  extraIcons: ['warning-outline'],
  // Where Ionic's SVGs live. Default: 'node_modules/@ionic/core/dist/ionic/svg'
  iconSrcDir: 'node_modules/@ionic/core/dist/ionic/svg',
  // Output sub-directory name. Default: 'svg'
  iconDestDir: 'svg',
  // Log detected icons and emit count. Default: false
  verbose: true,
})

Try it

📦 npm: vite-plugin-ionic-icons 🐙 GitHub: DeJy/vite-plugin-ionic-icons

If you use Ionic with Vite, give it a try. Issues, feedback, and PRs are all welcome.


메타데이터
post_id
36f01dcd32f8
slug
stop-shipping-1-300-icons-when-your-app-uses-12-36f01dcd32f8
url
https://medium.com/@touletan/stop-shipping-1-300-icons-when-your-app-uses-12-36f01dcd32f8
canonical_url
https://medium.com/@touletan/stop-shipping-1-300-icons-when-your-app-uses-12-36f01dcd32f8
author_url
https://medium.com/@touletan
status
ok
fetched_at
2026-07-15 12:51:37