SVG Sprite Optimization: Reduce HTTP Requests
An SVG sprite combines many individual icons into one file. Instead of 50 separate HTTP requests for 50 icons, the browser makes one request and caches the entire set. Here’s how to build, optimize, and use SVG sprites effectively.
How an SVG sprite works
An SVG sprite is a single SVG file containing multiple icons defined as <symbol> elements. Each symbol has an id. To display an icon, you use <use href="sprite.svg#icon-id">:
<!-- sprite.svg -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="icon-arrow" viewBox="0 0 24 24">
<path d="M5 12h14M12 5l7 7-7 7"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24">
<path d="M5 13l4 4L19 7"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<!-- more icons... -->
</svg>Using an icon from the sprite:
<svg width="24" height="24" aria-hidden="true">
<use href="/sprite.svg#icon-arrow" />
</svg>Optimize individual icons before building the sprite
The most important step: optimize each icon before merging into a sprite. Optimizing the final sprite file is less effective because shared path patterns across icons help with HTTP compression, but the per-icon waste is still there.
# Optimize the icons folder first
npx svgo -r -f ./icons --multipass
# Then build the sprite
node scripts/build-sprite.jsBuilding a sprite with Node.js
// scripts/build-sprite.js
import fs from 'node:fs';
import path from 'node:path';
const iconsDir = './icons';
const outputPath = './public/sprite.svg';
const symbols = fs.readdirSync(iconsDir)
.filter(f => f.endsWith('.svg'))
.map(file => {
const name = path.basename(file, '.svg');
const content = fs.readFileSync(path.join(iconsDir, file), 'utf-8');
// Extract viewBox and inner content
const viewBox = content.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
const inner = content
.replace(/<svg[^>]*>/, '')
.replace(/</svg>/, '')
.trim();
return ` <symbol id="icon-${name}" viewBox="${viewBox}">
${inner}
</symbol>`;
});
const sprite = [
'<svg xmlns="http://www.w3.org/2000/svg" style="display:none">',
...symbols,
'</svg>',
].join('
');
fs.writeFileSync(outputPath, sprite);
console.log(`Sprite built: ${symbols.length} icons → ${outputPath}`);Using svg-sprite or svgstore
For larger projects, dedicated tools handle more edge cases:
npm install --save-dev svgstore
# Or with svg-sprite (more configurable)
npm install --save-dev svg-sprite// scripts/build-sprite.js (using svgstore)
import svgstore from 'svgstore';
import fs from 'node:fs';
import path from 'node:path';
const sprites = svgstore({ cleanDefs: true, cleanSymbols: true });
for (const file of fs.readdirSync('./icons').filter(f => f.endsWith('.svg'))) {
sprites.add(`icon-${path.basename(file, '.svg')}`,
fs.readFileSync(path.join('./icons', file), 'utf-8'));
}
fs.writeFileSync('./public/sprite.svg', sprites.toString());
Inlining the sprite in HTML
For maximum performance, inject the sprite inline at the top of your HTML body. This eliminates the external HTTP request entirely:
<!-- In your HTML template / layout component -->
<body>
<!-- Inline sprite (hidden) -->
<div style="display:none">
{/* sprite.svg content */}
</div>
<!-- ... rest of page -->
</body>In Next.js, read the sprite file in a Server Component and render it inline:
// app/layout.tsx
import fs from 'node:fs';
export default function RootLayout({ children }) {
const sprite = fs.readFileSync('./public/sprite.svg', 'utf-8');
return (
<html>
<body>
<div
style={{ display: 'none' }}
dangerouslySetInnerHTML={{ __html: sprite }}
/>
{children}
</body>
</html>
);
}Icon component wrapper
A small wrapper component makes sprites ergonomic to use:
// components/Icon.tsx
interface IconProps {
name: string;
size?: number;
className?: string;
}
export function Icon({ name, size = 24, className }: IconProps) {
return (
<svg width={size} height={size} aria-hidden="true" className={className}>
<use href={`/sprite.svg#icon-${name}`} />
</svg>
);
}
// Usage
<Icon name="arrow" size={20} className="text-gray-500" />When sprites are worth it
| Icons in project | Recommendation |
|---|---|
| Under 10 | Inline per-component is fine |
| 10–50 | Sprite worth considering |
| 50+ | Sprite strongly recommended |
Sprites are most valuable when the same icons appear across many pages — the cached sprite file is reused on every page load. For icons used on only one page, inlining may be simpler. See inline vs. external SVG for the full trade-off analysis.
Optimize SVGs instantly — no setup needed
500 free optimizations per month. No credit card required.
Get a free API key →