react-icons-sprite 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +325 -0
  3. package/package.json +4 -2
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2025 Jure Rotar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # react-icons-sprite
2
+
3
+ `react-icons-sprite` is a lightweight plugin for Vite and Webpack that turns React icon components into a single SVG
4
+ spritesheet and rewrites your code to reference those symbols via `<use>`.
5
+
6
+ It supports multiple React icon packages that export icons as individual React components. This approach both shrinks
7
+ your bundle (no more inlined React components for every icon) and reduces runtime overhead, since React no longer has to
8
+ reconcile large, nested SVG trees.
9
+
10
+ ## Supported icon libraries
11
+
12
+ Out of the box, imports from the following libraries are detected and transformed. These are the versions currently
13
+ used by the demo apps and validation suite:
14
+
15
+ | Library | Validated version |
16
+ |---|---:|
17
+ | `react-icons/*` packs (e.g. `react-icons/bi`, `react-icons/fa`, ...) | `5.6.0` |
18
+ | `lucide-react` | `1.16.0` |
19
+ | `@radix-ui/react-icons` | `1.3.2` |
20
+ | `@heroicons/react` (v1 and v2 subpaths) | `2.2.0` |
21
+ | `@tabler/icons-react` | `3.44.0` |
22
+ | `phosphor-react` | `1.4.1` |
23
+ | `@phosphor-icons/react` | `2.1.10` |
24
+ | `react-feather` | `2.0.10` |
25
+ | `react-bootstrap-icons` | `1.11.6` |
26
+ | `grommet-icons` | `4.14.0` |
27
+ | `@remixicon/react` | `4.9.0` |
28
+ | `devicons-react` | `1.5.0` |
29
+ | `@fortawesome/free-solid-svg-icons` (and other Font Awesome icon packs) | `7.2.0` |
30
+ | `@fortawesome/react-fontawesome` | `3.3.1` |
31
+ | `@mui/icons-material` | `9.0.1` |
32
+ | `@carbon/icons-react` | `11.81.0` |
33
+ | `@ant-design/icons` | `6.2.5` |
34
+ | `@fluentui/react-icons` | `2.0.330` |
35
+ | `@primer/octicons-react` | `19.28.1` |
36
+ | `@hugeicons/react` with `@hugeicons/core-free-icons` | `1.1.6` / `4.2.0` |
37
+
38
+ > [!NOTE]
39
+ > `react-icons-sprite` does not bundle these libraries. You must install whichever icon packages you intend to use in
40
+ > your project.
41
+
42
+ ## Motivation
43
+
44
+ By default, when you use an icon library like `react-icons`, each icon is a React component. For example:
45
+
46
+ ```tsx
47
+ import { LuWheat } from "react-icons/lu";
48
+
49
+ export function Example() {
50
+ return <LuWheat />;
51
+ }
52
+ ```
53
+
54
+ looks harmless, but at build time this compiles to something like:
55
+
56
+ ```javascript
57
+ import { b as e } from './iconBase-BU3rGdXB.js'
58
+
59
+ function t(t) {
60
+ return e({
61
+ tag: `svg`, attr: { viewBox: `0 0 640 512` }, child: [{
62
+ tag: `path`, attr: {
63
+ d: `M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z`
64
+ }, child: []
65
+ }]
66
+ })(t)
67
+ }
68
+
69
+ function n(t) {
70
+ return e({
71
+ tag: `svg`, attr: { viewBox: `0 0 496 512` }, child: [{
72
+ tag: `path`, attr: {
73
+ d: `M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z`
74
+ }, child: []
75
+ }]
76
+ })(t)
77
+ }
78
+
79
+ // ... potentially dozens more child components.
80
+
81
+ export { t, n };
82
+ ```
83
+
84
+ That means:
85
+
86
+ - Every icon adds hundreds of characters of JSX to your bundle.
87
+ - At runtime, React must create and diff dozens of DOM nodes for every icon render.
88
+ - If you render 100 icons, React is reconciling potentially thousands of <path> elements.
89
+
90
+ Now compare that to output when using a spritesheet with `<use>`:
91
+
92
+ ```tsx
93
+ import { LuWheat } from "react-icons/lu";
94
+
95
+ export function Example() {
96
+ return <LuWheat />;
97
+ }
98
+ ```
99
+
100
+ ```javascript
101
+ import { b as n } from './icon-FONPSuqX.js';
102
+
103
+ var r = e(t());
104
+ const i = () => (0, r.jsx)(n, { iconId: `ri-react-icons-lu-LuWheat` });
105
+ export { i as IconWheat };
106
+ ```
107
+
108
+ `icon-FONPSuqX.js` in this case is our simple icon wrapper, which looks like this:
109
+
110
+ ```javascript
111
+ var n = e(t());
112
+ const r = ({ iconId: e, ...t }) => {
113
+ let r = `assets/react-icons-sprite-C-JClopV.svg#${e}`;
114
+ return (0, n.jsxs)(`svg`, {
115
+ height: `1em`,
116
+ width: `1em`,
117
+ preserveAspectRatio: `xMidYMid meet`,
118
+ viewBox: `0 0 24 24`, ...t,
119
+ children: [(0, n.jsx)(`title`, { children: e }), (0, n.jsx)(`use`, { href: r })]
120
+ })
121
+ };
122
+ export { r as b };
123
+ ```
124
+
125
+ Runtime difference:
126
+
127
+ - React only reconciles two DOM nodes (`<svg>` + `<use>`).
128
+ - All heavy path data lives in the static spritesheet once, not duplicated across components.
129
+ - Updating 100 icons is as cheap as updating 100 `<use>` tags.
130
+
131
+ This is a big win when you’re rendering icons in lists, tables, or maps where dozens or hundreds of them appear at once.
132
+
133
+ ### Performance comparison
134
+
135
+ The build-time transform is also intentionally small. The current Vitest benchmark transforms a component with 240 icon
136
+ usages at **32,805.97 ops/sec** (mean **0.0305 ms** per transform) on the machine listed below.
137
+
138
+ | icon (pack) | react-icons icon render mean time | react-icons-sprite icon render mean time | Relative difference |
139
+ |-------------------------|----------------------------------:|-----------------------------------------:|--------------------:|
140
+ | **FiCpu** (fi) | 0.188 ms | 0.048 ms | 74.6% reduction |
141
+ | **MdBuild** (md) | 0.198 ms | 0.048 ms | 76.0% reduction |
142
+ | **FaCamera** (fa) | 0.162 ms | 0.015 ms | 90.7% reduction |
143
+ | **IoAperture** (io5) | 0.029 ms | 0.015 ms | 49.7% reduction |
144
+ | **BiBell** (bi) | 0.023 ms | 0.014 ms | 38.5% reduction |
145
+ | **AiOutlineAlert** (ai) | 0.023 ms | 0.014 ms | 38.3% reduction |
146
+ | **BsAlarm** (bs) | 0.027 ms | 0.014 ms | 47.4% reduction |
147
+ | **RiAnchor** (ri) | 0.023 ms | 0.014 ms | 38.6% reduction |
148
+ | **CgArrows** (cg) | 0.029 ms | 0.014 ms | 52.0% reduction |
149
+ | **HiAcademicCap** (hi) | 0.023 ms | 0.014 ms | 38.6% reduction |
150
+ | **SiTypescript** (si) | 0.023 ms | 0.014 ms | 39.7% reduction |
151
+ | **TiThLarge** (ti) | 0.023 ms | 0.014 ms | 40.0% reduction |
152
+
153
+ * **Test details / machine:** **Lenovo Legion 5 Pro 16ACH6H** (Ryzen 7 5800H — 8 cores / 16 threads, base ≈ 3.2 GHz,
154
+ turbo ≈ 4.4 GHz, DDR4-3200 memory); Node.js v24.10.0.
155
+ * Differences will vary based on icons used in your application, but they will generally be the range of 50-75%
156
+ reduction in render time. Larger icons will generate a larger difference.
157
+
158
+ ## Installation
159
+
160
+ Install the plugin via npm or yarn:
161
+
162
+ ```bash
163
+ npm install --save-dev react-icons-sprite
164
+ ```
165
+
166
+ ### Vite
167
+
168
+ Add the plugin to the `plugins` array in your Vite config.
169
+
170
+ ```typescript
171
+ // vite.config.ts
172
+ import { defineConfig } from 'vite';
173
+ import { reactIconsSprite } from 'react-icons-sprite/vite';
174
+
175
+ export default defineConfig({
176
+ plugins: [reactIconsSprite()],
177
+ });
178
+ ```
179
+
180
+ ### Webpack
181
+
182
+ Add the loader to transform modules that import icons and install the plugin to emit the sprite and rewrite the
183
+ placeholder URL.
184
+
185
+ ```js
186
+ // webpack.config.js (v5)
187
+ const path = require('path');
188
+ const { reactIconsSprite } = require('react-icons-sprite/webpack');
189
+
190
+ module.exports = {
191
+ mode: 'production',
192
+ // ... your existing config
193
+ module: {
194
+ rules: [
195
+ {
196
+ test: /\.(mjs|cjs|js|jsx|ts|tsx)$/,
197
+ exclude: /node_modules/,
198
+ use: [
199
+ {
200
+ loader: require.resolve('react-icons-sprite/webpack/loader'),
201
+ },
202
+ // put your ts/tsx loader after ours (e.g. swc-loader or ts-loader)
203
+ ],
204
+ },
205
+ ],
206
+ },
207
+ plugins: [
208
+ reactIconsSprite({
209
+ // optional: fileName: 'icons.svg'
210
+ }),
211
+ ],
212
+ };
213
+ ```
214
+
215
+ ### Rsbuild
216
+
217
+ ```ts
218
+ // rsbuild.config.ts
219
+ import { defineConfig } from '@rsbuild/core';
220
+ import { pluginReact } from '@rsbuild/plugin-react';
221
+ import { ReactIconsSpriteWebpackPlugin } from 'react-icons-sprite/webpack';
222
+
223
+ export default defineConfig({
224
+ plugins: [pluginReact()],
225
+ tools: {
226
+ rspack: (config, { env }) => {
227
+ if (env === 'production') {
228
+ config.plugins?.push(new ReactIconsSpriteWebpackPlugin());
229
+ config.module?.rules?.push({
230
+ test: /\.(ts|tsx|js|jsx)$/,
231
+ use: [
232
+ {
233
+ loader: 'react-icons-sprite/webpack/loader',
234
+ },
235
+ ],
236
+ });
237
+ }
238
+ },
239
+ },
240
+ });
241
+ ```
242
+
243
+ ## How it works
244
+
245
+ In **development mode**, the plugin does nothing special. Icons are rendered as they normally would from your icon
246
+ library. This keeps hot module replacement (HMR) snappy — there’s no extra parsing of the codebase or regenerating of
247
+ the sprite on every save. If the plugin were to build the sprite during dev, it would need to constantly scan for icon
248
+ imports and rebuild the sheet, which is expensive and slows down iteration. So, in dev, you get the normal component
249
+ behavior.
250
+
251
+ In **build mode**, the plugin transforms your code. It parses each module, looks for imports from supported React icon
252
+ packages, and rewrites the JSX. Instead of rendering full inline `<svg>` trees, it replaces them with
253
+ `<ReactIconsSpriteIcon iconId="..." />`. While doing this, it collects every unique icon used across the project. After
254
+ the bundling step, the plugin renders all those icons once to static markup and generates a single SVG file containing
255
+ `<symbol>` definitions for each one. Finally, it rewrites your bundle to point every `<ReactIconsSpriteIcon>` at that
256
+ spritesheet using a `<use>` tag.
257
+
258
+ The result: during development you keep fast feedback loops, and in production you ship a single optimized sprite file
259
+ with lightweight `<use>` references.
260
+
261
+ ### Usage with react-router framework mode
262
+
263
+ `react-router`'s framework mode allows you to pre-render pages. In order for `react-icons-sprite` to work with
264
+ pre-rendered pages, include the bellow `buildEnd` hook.
265
+
266
+ ```ts
267
+ // react-router.config.ts
268
+ import type { Config } from '@react-router/dev/config';
269
+ import { REACT_ICONS_SPRITE_URL_PLACEHOLDER } from 'react-icons-sprite';
270
+
271
+ export const replaceReactIconsSpritePlaceholdersOnPreRenderedPages: NonNullable<
272
+ Config['buildEnd']
273
+ > = async ({ reactRouterConfig }) => {
274
+ const clientDir = resolve('build/client');
275
+
276
+ const [svgSpriteFile] = await Array.fromAsync(
277
+ glob('./build/client/react-icons-sprite-*.svg'),
278
+ );
279
+
280
+ const svgSpriteName = svgSpriteFile.replace(/build[/\\]client[/\\]?/, '/');
281
+
282
+ const preRenderedFileUrls =
283
+ (reactRouterConfig.prerender!.paths as string[]).map((path) => {
284
+ return resolve(clientDir, `.${path}`, 'index.html');
285
+ });
286
+
287
+ await Promise.all(
288
+ preRenderedFileUrls.map(async (filePath) => {
289
+ const content = await readFile(filePath, 'utf8');
290
+
291
+ if (!content.includes(REACT_ICONS_SPRITE_URL_PLACEHOLDER)) {
292
+ return;
293
+ }
294
+
295
+ const updatedContent = content.replaceAll(
296
+ REACT_ICONS_SPRITE_URL_PLACEHOLDER,
297
+ svgSpriteName,
298
+ );
299
+
300
+ await writeFile(filePath, updatedContent, 'utf8');
301
+ }),
302
+ );
303
+ };
304
+
305
+ const reactRouterConfig: Config = {
306
+ prerender: {
307
+ paths: [
308
+ // Your list of pages to pre-render
309
+ ],
310
+ },
311
+ buildEnd: async (args) => {
312
+ await replaceReactIconsSpritePlaceholdersOnPreRenderedPages(args);
313
+ },
314
+ };
315
+
316
+ export default reactRouterConfig;
317
+ ```
318
+
319
+ ## Contributing
320
+
321
+ Contributions are welcome! Feel free to open an issue or submit a pull request.
322
+
323
+ ## License
324
+
325
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for details.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "react-icons-sprite",
4
- "version": "1.1.0",
4
+ "version": "1.1.1",
5
5
  "type": "module",
6
6
  "description": "A lightweight Vite, Rsbuild and Webpack plugin for react-icons that builds a single SVG sprite and rewrites icons to <use>, reducing bundle size and runtime overhead.",
7
7
  "author": "Jure Rotar <hello@jurerotar.com>",
@@ -46,7 +46,9 @@
46
46
  }
47
47
  },
48
48
  "files": [
49
- "dist"
49
+ "dist",
50
+ "README.md",
51
+ "LICENSE.md"
50
52
  ],
51
53
  "scripts": {
52
54
  "build": "tsdown",