I had an idea for caching SVG paths. Not the usual kind of async request caching of remote SVGs, but local re-use of DOM elements that have already rendered.
SVG's <use>
element allows re-use of an existing DOM element, without manually duplicating the node. It works like this:
<!-- Add an id to the element -->
<svg>
<circle id="circle" cx="5" r="5" fill="black" />
</svg>
<!-- Pass the id as href to <use> -->
<svg>
<use href="#circle" />
</svg>
<!-- The same SVG renders twice -->
Setup
When using an icon set like Feather in React, I prefer to use a higher-order component (HOC) and a generic Icon
component to render each icon with consistent properties. We'll use this HOC to demonstrate SVG caching:
import { memo } from 'react'
const withIcon = (icon, opts) => {
const Icon = (props) => {
const { size = 24, color = 'currentColor' } = props
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
stroke="currentColor"
style={{
color,
}}
dangerouslySetInnerHTML={{
__html: icon,
}}
/>
)
}
return memo(Icon)
}
export default withIcon
Each icon is simply the SVG contents wrapped with the HOC:
const ArrowLeft = withIcon('<path d="M21 12H3m0 0l6.146-6M3 12l6.146 6" />')
Caching
We'll use React context to add an icon cache. First, create a new context and the appropriate hook to access it:
export const IconCache = React.createContext(null)
export const useIconCache = () => React.useContext(IconCache)
Setup the provider at the application root. The cache will be a plain, empty object where each key is the icon string and each value is the cached id.
const App = () => <IconCache.Provider value={{}}>{/* ... */}</IconCache.Provider>
Inside of Icon
, read the cache from context and check if this icon has a cached id. If not, generate the new id and add it to the cache:
const cache = useIconCache()
let cachedId = cache[icon]
if (!cachedId) {
cachedId = `icon-` + hash(icon).toString(16)
cache[icon] = cachedId
}
Generate a stable id by hashing the icon using the fnv1a 1 algorithm (commonly used in CSS-in-JS libraries) and then converting it to hexadecimal for a smaller string:
import hash from 'fnv1a'
If we have a cached id, we can render the <use>
tag instead of inserting the entire icon again. If this icon has not rendered before, wrap it in a group tag and attach the unique id.
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
stroke="currentColor"
style={{
color,
}}
dangerouslySetInnerHTML={{
__html: cachedId ? `<use href="#${cachedId}" />` : `<g id="${id}">${icon}</g>`,
}}
/>
)
Conclusion
Here's our new withIcon
HOC with caching:
import { memo } from 'react'
import hash from 'fnv1a'
export const IconCache = React.createContext({})
export const useIconCache = () => React.useContext(IconCache)
const withIcon = (icon) => {
const Icon = (props) => {
const { size = 24, color = 'currentColor' } = props
const cache = useIconCache()
const cachedId = cache[icon]
let id
if (!cachedId) {
id = 'icon-' + hash(icon).toString(16)
cache[icon] = id
}
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
stroke="currentColor"
style={{
color,
}}
dangerouslySetInnerHTML={{
__html: cachedId ? `<use href="#${cachedId}" />` : `<g id="${id}">${icon}</g>`,
}}
/>
)
}
return memo(Icon)
}
export default withIcon
Rendering the same icon multiple times will reuse existing DOM elements, decreasing the size of your HTML:
/* React */
<IconCache.Provider value={{}}>
<ArrowLeft />
<ArrowLeft />
<ArrowLeft />
</IconCache.Provider>
/* HTML Output:
<svg>
<g id="icon-dacb5a47"><path d="M21 12H3m0 0l6.146-6M3 12l6.146 6" /></g>
</svg>
<svg>
<use href="#icon-dacb5a47" />
</svg>
<svg>
<use href="#icon-dacb5a47" />
</svg>
*/
In this example, the cached version is about 40% fewer characters!
You can still customize each icon, because the props apply to the outer svg element and don't involve the inner elements at all:
<ArrowLeft />
<ArrowLeft size={30} color="blue" />
<ArrowLeft size={50} color="red" />
Here's a live demo and the demo source code.
- You don't have to use fnv1a, any stable id generation technique will work. Just make sure it's consistent between server and client to avoid hydration mismatch.