DEV Community

Dominic Jean
Dominic Jean

Posted on

Stop shipping 1,300 icons when you only use 12 — icon tree-shaking for Ionic + Vite

If you use @ionic/core with Vite, you've probably hit this at some point: your build output contains a svg/ folder with every single Ionic icon — all ~1,300 of them — even though your app only uses a dozen.

This happens because Ionic registers icons at runtime by name (<ion-icon name="add-outline" />), so bundlers have no idea which SVG files to keep and which to drop.

I built vite-plugin-ionic-icons to fix exactly that.


What it does

  • Build mode — scans your source files for every <ion-icon name="..."> usage across all frameworks, then emits only those icons as build assets. If you use 12 icons, you ship 12 SVG files.
  • Dev mode — serves icons on-demand directly from node_modules via a middleware. No copying, instant startup.

The difference in bundle output can be dramatic:

Before After
SVG files emitted ~1,300 only what you use
svg/ folder size ~4 MB a few KB

Installation

npm install -D vite-plugin-ionic-icons
Enter fullscreen mode Exit fullscreen mode

Requires vite >= 4.0.0 and @ionic/core already installed.


Setup (zero config)

// vite.config.ts
import { defineConfig } from 'vite';
import ionicIcons from 'vite-plugin-ionic-icons';

export default defineConfig({
  plugins: [
    ionicIcons(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

That's it. In dev, icons are served at /svg/<name>.svg. In production, only the icons detected in your source are emitted to dist/svg/.


Framework support

The scanner recognizes icon usage across all major frameworks with a set of regex patterns tailored to each syntax.

React / JSX / TSX

<ion-icon name="add-outline" />
<ion-icon name="trash" size="small" />
Enter fullscreen mode Exit fullscreen mode

Vue (static & literal dynamic binding)

<ion-icon name="add-outline" />
<ion-icon :name="'add-outline'" />
Enter fullscreen mode Exit fullscreen mode

Angular (static & literal property binding)

<ion-icon name="add-outline"></ion-icon>
<ion-icon [name]="'add-outline'"></ion-icon>
Enter fullscreen mode Exit fullscreen mode

Svelte

<ion-icon name="add-outline" />
Enter fullscreen mode Exit fullscreen mode

Mithril / Preact h()

m('ion-icon', { name: 'add-outline' })
h('ion-icon', { name: 'add-outline' })
Enter fullscreen mode Exit fullscreen mode

Plain HTML

<ion-icon name="add-outline"></ion-icon>
Enter fullscreen mode Exit fullscreen mode

Dynamic icons

If you resolve icon names at runtime (from an API, user settings, etc.), the static scanner can't detect them. Just list them explicitly:

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

All options

ionicIcons({
  // Directory (or array of dirs) to scan. Default: './src'
  srcDir: './src',

  // File extensions to scan. Default: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte']
  extensions: ['.js', '.ts', '.jsx', '.tsx', '.html', '.vue', '.svelte'],

  // Icons to always include (for dynamic icon names).
  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. Default: 'svg'
  iconDestDir: 'svg',

  // Log detected icons and emit count. Default: false
  verbose: true,
})
Enter fullscreen mode Exit fullscreen mode

How it works under the hood

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
Enter fullscreen mode Exit fullscreen mode

The scanner runs once per build/serve session and the result is cached — no repeated file-system work.


Links

Feedback and PRs are very welcome. If you use Ionic with Vite, give it a try and let me know how it goes!

Top comments (0)