SVG Sprite Optimization: Reduce HTTP Requests

·6 min read

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.js

Building 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 projectRecommendation
Under 10Inline per-component is fine
10–50Sprite 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 →