
Lazy load SVG icons with <use/> in React.js
Thursday 6 February 2025 • Reading time: 7 minutesUsing icons in your website or app almost always brings up the question of optimization.
Icons used in this article are extracted from the Remix Icon library.
The inline way
Maybe you are using a popular icon library like react-icon
, or @remixicon/react
. In that case, each icon you import will likely be inline injected in your final HTML. Depending on the complexity of these icons or their number, this can lead to a significant increase in your bundle size.
The sprite way
Another approach that I find interesting is to use SVG icons with the <use>
tag. This way, you can reference the same SVG file multiple times in your HTML without having to duplicate the SVG code.
Downloading, optimizing (with SVGOMG), and importing icons inside your project can be a lot of work if you need numerous icons for your application but starting from scratch you'll generally add icons one by one when needed so it doesn't feel like a big deal.
The basic setup for this usage it to create a single .svg
sprite file that will reference several icons inside
html<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none" > <symbol id="arrow-right" viewBox="0 0 24 24"> <path d="m16.172 11-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2h12.172Z" /> </symbol> <symbol id="arrow-left" viewBox="0 0 24 24"> <path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11Z" /> </symbol> </svg>
If your sprite.svg
is located in /images/icons/sprite.svg
, you can call your icons like this:
html<svg> <use href="/images/icons/sprite.svg#arrow-right"></use> </svg>
Doing this will allow you to reference the same SVG file multiple times without increasing your bundle size.
However, if you use only one or two icons in a specific page, you'll load the entire sprite file just for these icons. Depending on its size, it can be acceptable or not.
Splitting your sprite into individual files
To avoid loading the entire sprite file, you can split your sprite into individual files.
html<!-- arrow-left.svg --> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none" > <symbol id="icon" viewBox="0 0 24 24"> <path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414L7.828 11Z" /> </symbol> </svg> <!-- arrow-right.svg --> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none" > <symbol id="icon" viewBox="0 0 24 24"> <path d="m16.172 11-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2h12.172Z" /> </symbol> </svg>
These icons can be used like this:
html<svg> <use href="/images/icons/arrow-right.svg#icon"></use> </svg>
Browser cache is doing the rest: icons used in several places will be loaded instantly after their first use.
Usage in React.js
In order to reduce boilerplate, you can create a component that will handle the loading of your icons.
tsx// @/components/icon.tsx type Props = React.SVGProps<SVGSVGElement> & { code: string; }; export default function Icon({ code, ...props }: Props) { return ( <svg width={24} height={24} {...props}> <use href={`/images/icons/${code}.svg#icon`} /> </svg> ); }
Usage:
tsx<Icon code="arrow-right" />
Now with lazy loading
I find it interesting to lazy load these icons because it allows for a reduced number of request during the initial page load: only icons that are in view are requested over the network.
You'll need to adjust your icon.tsx
component like this:
tsx// @/components/icon.tsx // This code implements lazy loading for SVG icons using the Intersection Observer API. type Props = React.SVGProps<SVGSVGElement> & { code: string; }; export default function Icon({ code, ...props }: Props) { // Creates a ref to track the SVG element const ref = React.useRef<SVGSVGElement>(null); // Uses useState to track if the icon is in viewport const [inView, setInView] = React.useState(false); React.useEffect(() => { // Checks if IntersectionObserver is supported by the browser const isCompatible = "IntersectionObserver" in window; if (isCompatible) { const svg = ref.current; if (svg) { // Creates an observer that triggers when icon enters viewport const observer = new IntersectionObserver( ([entry]) => setInView(entry.isIntersecting), // Adds a root margin to trigger the observer a bit earlier: 24px before svg enters the viewport { rootMargin: "24px" } ); // Sets up observation of the SVG element on mount observer.observe(svg); return () => { // Cleans up by unobserving on unmount observer.unobserve(svg); }; } } else { // Falls back to always showing the icon setInView(true); } }, []); // Only sets the SVG reference when icon is in view // Prevents unnecessary loading of SVG icons outside viewport const href = inView ? `/images/icons/${code}.svg#icon` : undefined; return ( <svg ref={ref} width={24} height={24} {...props}> <use href={href} /> </svg> ); }
Here the result in video:
Below the code I use on my website without comments and with the addition of a IconCode
type more specific allowing for auto completion for the code
property when using the <Icon />
components.
tsx"use client"; import * as React from "react"; export type IconCode = "arrow-left" | "arrow-right"; type Props = React.SVGProps<SVGSVGElement> & { code: IconCode; }; export default function Icon({ code, ...props }: Props) { const ref = React.useRef<SVGSVGElement>(null); const [inView, setInView] = React.useState(false); React.useEffect(() => { const isCompatible = "IntersectionObserver" in window; if (isCompatible) { const svg = ref.current; if (svg) { const observer = new IntersectionObserver( ([entry]) => setInView(entry.isIntersecting), { rootMargin: "24px" } ); observer.observe(svg); return () => { observer.unobserve(svg); }; } } else { setInView(true); } }, []); const href = inView ? `/images/icons/${code}.svg#icon` : undefined; return ( <svg ref={ref} width={24} height={24} {...props}> <use href={href} /> </svg> ); }
Of course, this is only an approach to this specific problematic, this code can be adjusted or completely rewritten if necessary.