SVG Best Practices for Web Performance
Master SVG optimization for the web. Learn accessibility, animation, icon systems, optimization tools, and performance techniques for scalable vector graphics.
SVG (Scalable Vector Graphics) is the web’s native vector format—infinitely scalable, styleable with CSS, and manipulable with JavaScript. This guide covers everything you need to know to use SVG effectively and performantly.
Understanding SVG
SVG is an XML-based format that describes images using mathematical shapes rather than pixels. This makes SVGs resolution-independent and typically smaller than raster alternatives for graphics with geometric shapes.
SVG Anatomy
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<title>Accessible title</title>
<desc>Longer description for screen readers</desc>
<circle cx="50" cy="50" r="40" fill="#3b82f6"/>
</svg>
Key attributes:
| Attribute | Purpose |
|---|---|
xmlns | Namespace (required for standalone SVGs) |
viewBox | Coordinate system (minX minY width height) |
width/height | Display dimensions |
preserveAspectRatio | Scaling behavior |
When SVG Excels
Icons and logos: Scale perfectly at any size, styleable with CSS.
Illustrations: Flat design graphics, diagrams, infographics.
Interactive graphics: Charts, maps, data visualizations.
Animations: CSS and JavaScript-powered motion.
When to Avoid SVG
Photographs: Use raster formats (JPEG, WebP, AVIF).
Complex artwork: Detailed illustrations with many paths can become enormous.
Simple images: A 10KB PNG might be smaller than the SVG equivalent.
Embedding Methods
How you embed SVG affects capabilities and performance:
Inline SVG
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
</svg>
Pros:
- Full CSS styling control
- JavaScript manipulation
- Part of DOM (no additional request)
- Best for icons that need theming
Cons:
- Increases HTML size
- Not cacheable separately
- Can bloat HTML with many icons
img Tag
<img src="logo.svg" alt="Company Logo" width="200" height="50">
Pros:
- Cacheable
- Simple implementation
- Lazy loading support
Cons:
- No CSS styling (external styles don’t apply)
- No JavaScript access
- Cannot use currentColor
CSS Background
.icon {
background-image: url('icon.svg');
background-size: contain;
width: 24px;
height: 24px;
}
Pros:
- Cacheable
- Good for decorative images
Cons:
- Not accessible (no alt text)
- No CSS fill/stroke styling
- Cannot use currentColor
object/embed
<object type="image/svg+xml" data="graphic.svg" width="400" height="300">
<img src="fallback.png" alt="Fallback">
</object>
Pros:
- SVG maintains its own styles
- Fallback support
- Interactive SVGs work
Cons:
- Additional HTTP request
- Cross-origin restrictions
- Styling from parent document limited
Comparison Table
| Method | CSS Styling | JS Access | Caching | HTTP Request |
|---|---|---|---|---|
| Inline | Full | Yes | No | None |
| img | No | No | Yes | 1 |
| Background | No | No | Yes | 1 |
| object | Internal only | Via contentDocument | Yes | 1 |
Optimization Techniques
SVGO: The Essential Tool
SVGO (SVG Optimizer) is the standard tool for SVG optimization:
# Install globally
npm install -g svgo
# Basic optimization
svgo input.svg -o output.svg
# Optimize in place
svgo input.svg
# Process directory
svgo -f ./icons/ -o ./icons-optimized/
# With custom config
svgo --config svgo.config.js input.svg
SVGO Configuration
Create svgo.config.js for consistent optimization:
module.exports = {
plugins: [
'preset-default',
'removeDimensions',
{
name: 'removeAttrs',
params: {
attrs: '(stroke|fill)'
}
},
{
name: 'addAttributesToSVGElement',
params: {
attributes: [{ 'aria-hidden': 'true' }]
}
}
]
};
Optimization Checklist
- Remove editor metadata: Illustrator, Sketch, Figma add bloat
- Simplify paths: Reduce decimal precision, merge paths
- Remove hidden elements: Layers outside viewport
- Convert shapes to paths: When it reduces code
- Remove unnecessary groups: Flatten structure
- Minify: Remove whitespace and comments
- Enable gzip/brotli: SVG compresses excellently
Before and After Example
Before optimization (1,847 bytes):
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="24px" height="24px" viewBox="0 0 24 24" version="1.1">
<!-- Generator: Sketch 52.6 (67491) -->
<title>icon/search</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M15.5,14 L14.71,14 L14.43,13.73..." id="path-1"/>
</defs>
<g id="icon/search" stroke="none" stroke-width="1" fill="none">
<use fill="#000000" xlink:href="#path-1"/>
</g>
</svg>
After optimization (312 bytes):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
Result: 83% reduction
Icon Systems
Inline SVG with Components
Best approach for modern frameworks:
React:
function SearchIcon({ size = 24, className }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
className={className}
fill="currentColor"
aria-hidden="true"
>
<path d="M15.5 14h-.79l-.28-.27A6.47..."/>
</svg>
);
}
// Usage
<SearchIcon size={20} className="text-gray-500" />
Vue:
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
:width="size"
:height="size"
fill="currentColor"
aria-hidden="true"
>
<path d="M15.5 14h-.79l-.28-.27A6.47..."/>
</svg>
</template>
<script setup>
defineProps({
size: { type: Number, default: 24 }
});
</script>
SVG Sprite System
Combine icons into a single file for efficient loading:
sprite.svg:
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-search" viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27..."/>
</symbol>
<symbol id="icon-menu" viewBox="0 0 24 24">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</symbol>
<symbol id="icon-close" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59..."/>
</symbol>
</svg>
Usage:
<!-- Include sprite once in body -->
<svg class="icon icon-search">
<use href="sprite.svg#icon-search"/>
</svg>
<!-- Or inline the sprite and reference by ID -->
<svg class="icon icon-menu">
<use href="#icon-menu"/>
</svg>
CSS:
.icon {
width: 24px;
height: 24px;
fill: currentColor;
}
External Icon Libraries
Popular optimized icon sets:
| Library | Icons | Approach | Size |
|---|---|---|---|
| Heroicons | 450+ | Components/SVG | ~1KB each |
| Lucide | 1400+ | Components/SVG | ~1KB each |
| Feather | 280+ | Stroke-based | ~0.5KB each |
| Phosphor | 6000+ | Multiple weights | ~1KB each |
Accessibility
Decorative vs Informative
Decorative icons (hide from assistive technology):
<svg aria-hidden="true" focusable="false">
<use href="#icon-decorative"/>
</svg>
Informative icons (provide meaning):
<svg role="img" aria-labelledby="icon-title">
<title id="icon-title">Search</title>
<use href="#icon-search"/>
</svg>
Icon Buttons
Always pair icons with accessible labels:
<!-- Method 1: Visible text -->
<button>
<svg aria-hidden="true"><use href="#icon-save"/></svg>
Save
</button>
<!-- Method 2: Screen reader only text -->
<button>
<svg aria-hidden="true"><use href="#icon-save"/></svg>
<span class="sr-only">Save document</span>
</button>
<!-- Method 3: aria-label -->
<button aria-label="Save document">
<svg aria-hidden="true"><use href="#icon-save"/></svg>
</button>
Screen reader text utility:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Complex Graphics
For charts, diagrams, and informative graphics:
<figure role="img" aria-labelledby="chart-title chart-desc">
<svg>
<title id="chart-title">Monthly Sales 2026</title>
<desc id="chart-desc">
Bar chart showing sales increasing from $10K in January
to $45K in December, with notable peaks in March and November.
</desc>
<!-- Chart content -->
</svg>
<figcaption>Figure 1: Monthly sales performance</figcaption>
</figure>
CSS Styling
Basic Styling
.icon {
/* Size */
width: 24px;
height: 24px;
/* Color using currentColor */
fill: currentColor;
color: #3b82f6;
/* Or direct fill */
fill: #3b82f6;
/* Stroke styling */
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
Using currentColor
The currentColor keyword inherits from the CSS color property:
<a href="/search" class="nav-link">
<svg fill="currentColor"><use href="#icon-search"/></svg>
Search
</a>
.nav-link {
color: #6b7280;
}
.nav-link:hover {
color: #3b82f6; /* Icon color changes too! */
}
Hover and State Effects
.icon-button {
color: #6b7280;
transition: color 0.2s ease;
}
.icon-button:hover {
color: #3b82f6;
}
.icon-button:active {
transform: scale(0.95);
}
/* Multicolor on hover */
.icon-button:hover .icon-primary {
fill: #3b82f6;
}
.icon-button:hover .icon-secondary {
fill: #93c5fd;
}
CSS Filters
Apply visual effects without modifying SVG:
.icon-shadow {
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
}
.icon-glow {
filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.5));
}
.icon-grayscale {
filter: grayscale(100%);
}
.icon-inverted {
filter: invert(1);
}
Animation
CSS Animations
Spin animation:
.icon-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Pulse animation:
.icon-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
Draw-on effect:
.icon-draw {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 1s ease forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
SMIL Animation (Built-in)
SVG’s native animation (avoid for new projects—use CSS/JS instead):
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#3b82f6">
<animate
attributeName="r"
values="40;45;40"
dur="1s"
repeatCount="indefinite"/>
</circle>
</svg>
JavaScript Animation
Using Web Animations API:
const icon = document.querySelector('.icon');
icon.animate([
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(1.2)', opacity: 0.8 },
{ transform: 'scale(1)', opacity: 1 }
], {
duration: 300,
easing: 'ease-out'
});
Using GSAP:
gsap.to('.icon path', {
strokeDashoffset: 0,
duration: 1,
ease: 'power2.out'
});
Security Considerations
SVG can contain JavaScript, making it a potential XSS vector.
Dangerous SVG Content
<!-- Script execution -->
<svg>
<script>alert('XSS')</script>
</svg>
<!-- Event handlers -->
<svg>
<rect onclick="alert('XSS')" width="100" height="100"/>
</svg>
<!-- External resources -->
<svg>
<image href="https://evil.com/track.gif"/>
</svg>
Sanitization
For user-uploaded SVGs:
- Use DOMPurify:
import DOMPurify from 'dompurify';
const cleanSvg = DOMPurify.sanitize(userSvg, {
USE_PROFILES: { svg: true, svgFilters: true }
});
- Use img tag (blocks scripts):
<!-- Safe: scripts won't execute -->
<img src="user-uploaded.svg" alt="User graphic">
- Content Security Policy:
Content-Security-Policy: script-src 'self'
Safe Embedding Methods
| Method | Scripts Execute? | Safe for User Content? |
|---|---|---|
| Inline SVG | Yes | No (without sanitization) |
| img tag | No | Yes |
| CSS background | No | Yes |
| object (same-origin) | Yes | No |
| object (cross-origin) | No | Partially |
Performance Optimization
File Size Strategies
Reduce path complexity:
// SVGO config to reduce precision
module.exports = {
plugins: [
{
name: 'convertPathData',
params: {
floatPrecision: 2
}
}
]
};
Combine similar paths:
<!-- Before: Multiple paths -->
<path d="M0 0h10v10H0z"/>
<path d="M20 0h10v10H20z"/>
<!-- After: Single path -->
<path d="M0 0h10v10H0zm20 0h10v10H20z"/>
Compression
SVG compresses extremely well. Enable on your server:
Nginx:
gzip on;
gzip_types image/svg+xml;
gzip_min_length 1000;
Apache:
AddOutputFilterByType DEFLATE image/svg+xml
Typical compression ratios: 60-80% reduction.
Loading Strategies
Critical icons inline:
<head>
<!-- Critical icons in head for immediate availability -->
<style>
.sprite { display: none; }
</style>
</head>
<body>
<svg class="sprite" aria-hidden="true">
<symbol id="logo">...</symbol>
<symbol id="menu">...</symbol>
</svg>
</body>
Non-critical icons lazy-loaded:
<link rel="preload" href="sprite.svg" as="image" type="image/svg+xml">
Render Performance
Avoid:
- Extremely complex paths (1000s of nodes)
- Heavy filters (blur, shadows)
- Large SVGs with many gradients
- Animating filter-heavy elements
Optimize:
/* Promote to GPU layer for smooth animation */
.animated-svg {
will-change: transform;
transform: translateZ(0);
}
Framework Integration
React (with SVGR)
Transform SVGs into React components:
npm install @svgr/webpack
webpack.config.js:
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
use: ['@svgr/webpack']
}
]
}
};
Usage:
import SearchIcon from './search.svg';
function Component() {
return <SearchIcon className="icon" />;
}
Vue
<template>
<component :is="iconComponent" class="icon" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const props = defineProps({
name: { type: String, required: true }
});
const iconComponent = defineAsyncComponent(() =>
import(`./icons/${props.name}.svg`)
);
</script>
Astro
---
// Icon.astro
const { name, size = 24, ...props } = Astro.props;
const icons = import.meta.glob('./icons/*.svg', { as: 'raw' });
const svg = await icons[`./icons/${name}.svg`]();
---
<Fragment set:html={svg} {...props} />
Common Mistakes
Mistake 1: Fixed Width/Height Without viewBox
<!-- Bad: Won't scale properly -->
<svg width="100" height="100">
<!-- Good: Scales based on viewBox -->
<svg viewBox="0 0 100 100" width="100" height="100">
Mistake 2: Inline Styles Blocking CSS
<!-- Bad: Can't override with CSS -->
<path fill="#000000" d="..."/>
<!-- Good: Use currentColor or classes -->
<path fill="currentColor" d="..."/>
Mistake 3: Missing Namespace
<!-- Bad: May not render in some contexts -->
<svg viewBox="0 0 24 24">
<!-- Good: Include namespace for standalone files -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
Mistake 4: Huge File Sizes
Causes:
- Editor metadata (Illustrator, Sketch)
- Excessive decimal precision
- Unnecessary groups and IDs
- Embedded raster images
Solution: Always run through SVGO.
Mistake 5: Poor Accessibility
<!-- Bad: Icon has no accessible name -->
<button>
<svg><use href="#icon-close"/></svg>
</button>
<!-- Good: Accessible -->
<button aria-label="Close dialog">
<svg aria-hidden="true"><use href="#icon-close"/></svg>
</button>
Summary
Quick Reference
| Use Case | Best Approach |
|---|---|
| UI icons | Inline SVG or sprite |
| Logos | Inline SVG (header), img (content) |
| Complex illustrations | img or object |
| User-uploaded | img tag (never inline) |
| Animated graphics | Inline SVG with CSS/JS |
| Background patterns | CSS background |
Optimization Checklist
- ✅ Run through SVGO
- ✅ Remove editor metadata
- ✅ Use viewBox for scalability
- ✅ Enable gzip compression
- ✅ Use currentColor for themeable icons
- ✅ Add proper accessibility attributes
- ✅ Sanitize user-uploaded SVGs
- ✅ Consider sprite system for many icons
- ✅ Test at multiple sizes
- ✅ Profile complex SVGs for render performance
SVG is powerful and flexible, but requires thoughtful implementation. Choose the right embedding method for your use case, optimize aggressively, and never forget accessibility.