Handle SVGs in React properly

June 23, 2025

We should talk about SVGs in your JavaScript / React apps. Long story short: putting them there bloats your bundle and degrades the performance of your app. Let’s take a look at a better approach for handling SVG, while keeping our JS bundle small and the page performant.

TLDR

SVG sprites give you the best performance-to-DX tradeoff.

Why you shouldn't Image + SVG

1<img src="icon.svg" />

Using an SVG as an image tag seems fine at first. The browser grabs the image from your public folder (or wherever), and it just works. The upside is that these external assets get cached by the browser or your CDN, so they load faster the next time you visit this page. But here’s the problem: there’s usually a little flicker when the image loads, because the browser first downloads your HTML document, then starts downloading images, scripts, and styles. Also, when you use an <img /> tag for your icons, you can't style or animate the SVG with CSS.

Why you shouldn't inline SVG

1const icon = (
2 <svg viewBox="0 0 24 24" width={16} height={16}>
3 <path 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" />
4 </svg>
5)

Another approach is often used to combat the issues I've just mentioned - inlining the SVG into the HTML document. When the Browser downloads the HTML, it doesn’t need to make a secondary request for the image asset; it is there immediately (no flicker). The other benefit is that the icon can be styled with CSS. Although it looks like a solution, this approach is not without pitfalls. Inlining the SVG into your HTML reduces memory performance because you add many elements to the page. There is a nice article on the topic by Chrome engineers.

Another problem is that the SVG bloats your JavaScript bundle size. The browser has to download, parse, and evaluate the JavaScript on the page before anything renders. Including SVG in your bundle makes you pay this cost twice. You have a larger bundle to download and parse (path data can be large for some icons, especially if they are more intricate), then the rendered HTML adds many of additional elements to the page, slowing down DOM traversal. A lot of React apps use this second technique, but I knew there had to be a better way.

Rendering Icons using SVG Sprites

There’s a better way to handle SVGs that eliminates the problems of both <img> and inlining: SVG Sprites.

This trick’s been around for a while, and it works surprisingly well. If you’ve done anything with game dev, you’ll get the concept. Instead of having a bunch of individual icons scattered around, you combine them into one file - a sprite sheet. Then you just reference the specific icon you need.

The browser only loads the sprite sheet once, so you avoid the flicker of separate image requests, and you also avoid stuffing a bunch of inline SVGs into your JSX. Best of both worlds.

The Symbol Element

Let me introduce you to the <symbol> element. The symbol element “is used to define graphical template objects which can be instantiated by a <use> element.” - MDN. When combined with the <defs> element, we can construct a SVG sprite with our icons.

First, we create a file sprite.svg, and add an <svg> element that wraps a defs element and a <symbol>. Next, we take the icon (that we would have inlined), swap the SVG for a symbol element, and give it an ID. The id is important! Finally, we’ll add a second icon to the sprite by adding it as a symbol.

1<!-- sprite.svg -->
2<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <defs>
4 <symbol viewBox="0 0 24 24" id="icon-1">
5 <path d="M6.84 5.76L8.4 7.68H5.28l-.72 2.88H2.64l.72-2.88H1.44L0 13.44h3.84l-.48 1.92h3.36L4.2 18.24h2.82l2.34-2.88h5.28l2.34 2.88h2.82l-2.52-2.88h3.36l-.48-1.92H24l-1.44-5.76h-1.92l.72 2.88h-1.92l-.72-2.88H15.6l1.56-1.92h-2.04l-1.68 1.92h-2.88L8.88 5.76zm.24 3.84H9v1.92H7.08zm7.925 0h1.92v1.92h-1.92Z" />
6 </symbol>
7 <symbol viewBox="0 0 24 24" id="icon-2">
8 <path d="M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z" />
9 </symbol>
10 </defs>
11</svg>

You can keep defining all your icons as symbols in this file (although be conscious of the file size, keep it under 125kb). Next, we will create a React component that will reference our sprite.

1import React from "react";
2import ReactDOM from "react-dom";
3
4// keep a list of the icon ids we put in the symbol
5const icons = ["icon-1", "icon-2"];
6
7// then define an Icon component that references the
8function Icon({ id, ...props }) {
9 return (
10 <svg {...props}>
11 <use href={`/sprite.svg#${id}`} />
12 </svg>
13 );
14}
15
16// In your App, you can now render the icons
17function App() {
18 return (
19 <div className="App">
20 {icons.map((id) => {
21 return <Icon key={id} id={id} />;
22 })}
23 </div>
24 );
25}
26
27const rootElement = document.getElementById("root");
28ReactDOM.render(<App />, rootElement);

The Use Element

In the example above, the magic happens in the <use> element which links to the “Fragment Identifier” (the ID that we defined on the symbol). Now, we have an icon component that lets us do all the things we could do with inline SVGs (like defining the height, width, and color of the icon), but all of the path data lives in an external asset (and not in the JavaScript bundle).

Preloading

You can preload the sprite file (and then cache it) to improve performance. “By preloading a certain resource, you are telling the browser that you would like to fetch it sooner than the browser would otherwise discover it because you are certain that it is important for the current page.” - web.dev.

To preload the sprite, add a link tag to the head of the document.

1<head>
2 <link rel="preload" href="sprite.svg" as="image" type="image/svg+xml" />
3</head>

Depending on your server’s configuration, you might need to make sure the proper cache-headers are set on your sprite.svg so the browser can cache it appropriately.

Introducing @neodx/svg

So far, the @neodx/svg library is a game-changer for handling SVG Sprites. It gives us a ready-to-go solution with great DX, like type-safety and a guide on how to build your Icon component. Put your icon into the assets folder, and it will create the types and generate the new sprite.

Included features:

Configuration

I will show the setup process for a Next.js project. Add @neodx/svg/webpack plugin to next.config.js and configure it for your needs.

1import svg from '@neodx/svg/webpack'
2
3const nextConfig = {
4 webpack: (config, { isServer }) => {
5 // Prevent doubling svg plugin, run it only for client build
6 if (!isServer) {
7 config.plugins.push(
8 svg({
9 root: 'assets',
10 output: 'public'
11 })
12 );
13 }
14 return config;
15 }
16};
17
18export default nextConfig

Create your Icon component:

1import clsx from 'clsx';
2import type { SVGProps } from 'react';
3import { SPRITES_META, type SpritesMap } from './sprite.gen';
4
5// Our icon will extend an SVG element and accept all its props
6export interface IconProps extends SVGProps<SVGSVGElement> {
7 name: AnyIconName;
8}
9// Merging all possible icon names as `sprite/icon` string
10export type AnyIconName = { [Key in keyof SpritesMap]: IconName<Key> }[keyof SpritesMap];
11// Icon name for a specific sprite, e.g. "common/left"
12export type IconName<Key extends keyof SpritesMap> = `${Key}/${SpritesMap[Key]}`;
13
14export function Icon({ name, className, ...props }: IconProps) {
15 const { viewBox, filePath, iconName, axis } = getIconMeta(name);
16
17 return (
18 <svg
19 // "icon" isn't inlined because of data-axis attribute
20 className={clsx('icon', className)}
21 viewBox={viewBox}
22 /**
23 * This prop is used by the "icon" class to set the icon's scaled size
24 * @see https://github.com/secundant/neodx/issues/92
25 */
26 data-axis={axis}
27 // prevent icon from being focused when using keyboard navigation
28 focusable="false"
29 // hide icon from screen readers
30 aria-hidden
31 {...props}
32 >
33 {/* For example, "/sprites/common.svg#favourite". Change a base path if you don't store sprites under the "/sprites". */}
34 <use href={`/sprites/${filePath}#${iconName}`} />
35 </svg>
36 );
37}
38
39/**
40 * A function to get and process icon metadata.
41 * It was moved out of the Icon component to prevent type inference issues.
42 */
43const getIconMeta = <Key extends keyof SpritesMap>(name: IconName<Key>) => {
44 const [spriteName, iconName] = name.split('/') as [Key, SpritesMap[Key]];
45 const {
46 filePath,
47 items: {
48 [iconName]: { viewBox, width, height }
49 }
50 } = SPRITES_META[spriteName];
51 const axis = width === height ? 'xy' : width > height ? 'x' : 'y';
52
53 return { filePath, iconName, viewBox, axis };
54};

Read this guide to understand the Icon component we've just built.

Use your Icon . It will type hint the name of your icons from the sprites.

1import { Icon } from '@/shared/ui/icon';
2
3export function SomeComponent() {
4 return <Icon name="my-icon-name" />;
5}

That’s it! Hope this was helpful.

References

Utopia Background
Travis Scott

Travis Scott

30M monthly listeners

UTOPIA

MY EYES

UTOPIA

Kyiv

20 °

Air quality alert

H: 25°

L: 15°

22

21°

23

20°

00

20°

01

18°

02

17°

03

17°