前置知识: SVG

SVG 性能优化

5 min高级

性能瓶颈定位、节点与滤镜优化、懒加载、SVGO 压缩、缓存与渲染优化,以及何时切换到 Canvas。

前置知识

建议先阅读以下内容再进入本文:


前置知识:viewBox 与坐标缩放原理(《坐标系与 viewBox》,030-SVGCoordinateSystemViewBox)、CSS 嵌入方式对样式的影响(《SVG CSS 样式化》,130-SVGCSSStyling)。

学习目标:能判断 SVG 的性能瓶颈在节点数、路径复杂度还是滤镜;掌握减节点、SVGO、批量 DOM、合成层四类优化手段;建立缓存与压缩的发布策略;知道什么规模该切换到 Canvas。

1. 性能瓶颈分析

1.1 SVG 渲染性能特征

因素影响
DOM 节点数量节点多 → 重排重绘开销大
复杂路径长路径 → 解析与渲染慢
滤镜feGaussianBlur 等 → CPU/GPU 开销大
蒙版与裁剪软蒙版 → 像素级计算
文本渲染大量 <text> → 排版开销
透明度与混合opacity、mix-blend-mode → 合成层开销

1.2 节点数量阈值

节点数性能
< 100流畅
100 - 1000静态可用,动画需优化
1000 - 5000明显卡顿
> 5000考虑改用 Canvas/WebGL

2. 优化策略

2.1 减少节点

html
<!-- 冗余:多个单独的 line -->
<g stroke="#333">
  <line x1="10" y1="10" x2="100" y2="10" />
  <line x1="10" y1="20" x2="100" y2="20" />
  <line x1="10" y1="30" x2="100" y2="30" />
</g>

<!-- 优化:合并为一个 path -->
<path d="M 10 10 L 100 10 M 10 20 L 100 20 M 10 30 L 100 30" stroke="#333" />

2.2 复用 symbol

html
<defs>
  <symbol id="dot" viewBox="0 0 10 10">
    <circle cx="5" cy="5" r="4" />
  </symbol>
</defs>
<use href="#dot" x="0" y="0" />
<use href="#dot" x="20" y="0" />
<!-- 1000 个 use 比直接画 1000 个 circle 内存占用小 -->

2.3 简化路径

html
<!-- 原始路径 -->
<path d="M 10.123456 10.234567 L 50.345678 10.456789 ..." />

<!-- SVGO 优化后 -->
<path d="M10 10L50 10..." />

使用 SVGO 工具自动优化:

bash
npm install -g svgo

# 单文件(--multipass 允许多轮压缩直到不再变化)
svgo --multipass input.svg -o output.svg

# 批量
svgo --multipass -f input-dir -o output-dir
js
// .svgo.config.js(SVGO 3+ 的配置风格,插件用字符串或对象均可)
module.exports = {
  multipass: true,
  plugins: [
    // 默认插件集:合并路径、删元数据、压缩坐标等 20+ 项
    {
      name: 'preset-default',
      params: {
        overrides: {
          // 需要保留 id 时关闭个别插件(按需增删)
          cleanupIds: false,
        },
      },
    },
    // 移除 width/height 属性,让 SVG 完全响应外部 CSS(响应式场景常用)
    'removeDimensions',
    'sortAttrs',
  ],
};

坐标精度(旧版 CLI 的 --precision 参数已移除)通过 preset-default 里的 convertPathData 插件参数 floatPrecision 控制。

2.4 避免复杂滤镜

html
<!-- 慢:模糊大区域 -->
<filter id="blur">
  <feGaussianBlur stdDeviation="10" />
</filter>
<rect width="1920" height="1080" filter="url(#blur)" />

<!-- 快:模糊小区域再缩放 -->
<filter id="blur-small" x="0" y="0" width="200" height="200">
  <feGaussianBlur stdDeviation="10" />
</filter>

2.5 transform 替代几何属性

javascript
// 慢:修改 x 触发布局计算
rect.setAttribute('x', 100);

// 快:transform 不参与布局,可被合成器加速
rect.style.transform = 'translateX(100px)';

与之配套的是描边问题:几何被 transform 缩放时,stroke-width 也跟着变。需要”缩放图形但描边恒定”(地图、网格线)时,加 vector-effect="non-scaling-stroke";SVG 2 还定义了 non-scaling-size、non-rotation、fixed-position 等值,但浏览器支持仅限于实验性,跨浏览器代码只应依赖 non-scaling-stroke。

2.6 will-change 提示

css
.animated-element {
  will-change: transform, opacity;
}

让浏览器提前为元素创建独立图层。

3. 懒加载

3.1 IntersectionObserver

javascript
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach((img) => {
  observer.observe(img);
});
html
<img data-src="large-diagram.svg" alt="图表" width="800" height="600" />

3.2 内联关键 SVG

首屏关键 SVG 内联,避免额外请求:

html
<!-- 内联首屏 Logo -->
<svg viewBox="0 0 100 40" class="logo">
  <path d="..." fill="currentColor" />
</svg>

<!-- 懒加载非关键 SVG -->
<img data-src="diagram.svg" alt="图表" loading="lazy" />

<img> 引用的 SVG 同样要写好宽高与流式 CSS,避免加载时布局抖动(CLS):

html
<img
  src="diagram.svg"
  alt="响应式图表"
  width="800"
  height="600"
  loading="lazy"
/>
css
img[src$='.svg'] {
  max-width: 100%;
  height: auto;
}

4. 压缩与优化

4.1 SVGO 优化

SVGO 的命令行用法见 2.3 节;这里补充选择优化项的思路——不是所有默认项都该开:

优化说明注意
移除注释与元数据减小体积安全
合并路径多 path 合并为单 path可能破坏层级语义与独立动画单元
简化坐标降低小数精度极小图形留意锯齿
移除默认值如 fill="black" 可省略安全
removeDimensions移除 width/height,配合外部 CSS 做响应式<img> 固定尺寸场景不要开
cleanupIds移除”未使用”的 id内联 sprite 场景必须关闭,否则 <use> 断链

4.2 常用优化项

优化说明
移除注释减小体积
移除编辑器元数据如 Inkscape 命名空间
合并路径多 path 合并为单 path
简化坐标降低精度到 2 位小数
移除默认值如 fill=“black” 可省略
转换为相对路径文件更小

4.3 Gzip / Brotli 压缩

服务器配置 SVG 压缩(文本格式压缩率高):

nginx
# nginx.conf
gzip on;
gzip_types image/svg+xml;

通常可压缩 70%-90%。

5. 缓存策略

5.1 外部 SVG 文件缓存

nginx
location ~* \.svg$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

5.2 文件名哈希

html
<!-- 构建工具生成 -->
<img src="logo.a3b7c9.svg" alt="Logo" />

文件内容变化时哈希变化,浏览器自动重新下载。

6. 渲染优化

6.1 避免重排

javascript
// 慢:逐个修改属性
elements.forEach((el) => {
  el.setAttribute('x', newX);
  el.setAttribute('y', newY);
});

// 快:批量修改
svg.style.display = 'none';
elements.forEach((el) => {
  el.setAttribute('x', newX);
  el.setAttribute('y', newY);
});
svg.style.display = 'block';

6.2 使用 DocumentFragment

javascript
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const dot = createSVG('circle', { cx: i, cy: 50, r: 2 });
  fragment.appendChild(dot);
}
svg.appendChild(fragment); // 一次性插入

6.3 CSS containment

css
.chart {
  contain: layout style paint;
}

隔离元素布局、样式、绘制,避免影响外部。

7. 实战:大数据点散点图

html
<svg viewBox="0 0 800 400" class="scatter">
  <defs>
    <symbol id="point" viewBox="-1 -1 2 2">
      <circle r="1" fill="#4f5bd5" />
    </symbol>
  </defs>
</svg>

<script>
  const svg = document.querySelector('.scatter');
  const data = [];
  for (let i = 0; i < 2000; i++) {
    data.push({
      x: Math.random() * 800,
      y: Math.random() * 400,
    });
  }

  // 批量插入,减少 reflow
  const fragment = document.createDocumentFragment();
  data.forEach((d) => {
    const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
    use.setAttribute('href', '#point');
    // use 的 x/y 是实例左上角:symbol viewBox 宽 2、use 尺寸 6,半径 3,
    // 因此左上角应为 (d.x-3, d.y-3) 才能让圆心正对数据点
    use.setAttribute('x', d.x - 3);
    use.setAttribute('y', d.y - 3);
    use.setAttribute('width', 6);
    use.setAttribute('height', 6);
    fragment.appendChild(use);
  });
  svg.appendChild(fragment);
</script>

优化点:

  • symbol 复用避免重复定义 circle
  • DocumentFragment 批量插入
  • 限制节点数(> 5000 考虑 Canvas)

8. 监测与分析

8.1 Chrome DevTools

  • Performance 面板:录制动画,分析帧率与瓶颈
  • Layers 面板:查看合成层,确认 GPU 加速
  • Rendering 面板:开启 Paint flashing 高亮重绘区域

8.2 关键指标

指标目标
FPS≥ 55
首次渲染< 100ms
单帧渲染< 16ms
内存占用< 50MB

9. 何时改用 Canvas

场景推荐
数据点 < 1000SVG
数据点 1000-5000,无动画SVG(优化后)
数据点 > 5000Canvas
实时粒子系统Canvas/WebGL
复杂图像处理Canvas
需要交互与可访问性SVG

小结

初学者要点:

  • 性能第一杀手是节点数量,第二是滤镜与软蒙版;先量化(Performance 面板)再优化,不要凭感觉。
  • 动画优先改 transform / opacity,它们不触发布局;批量化 DOM 操作(DocumentFragment + 一次性挂载)。

进阶注意:

  • §1.2 的节点阈值是经验参考而非标准,实际临界点取决于节点复杂度、目标设备与是否动画;用真实数据在目标机型上测。
  • SVGO 不是开箱即用:cleanupIds 会砍掉 sprite 里被 <use> 引用的 id,removeDimensions 会破坏 <img> 固定尺寸场景——按用途裁剪插件清单。
  • 响应式的双版本内容切换(简化版 + 完整版共存)会让节点预算直接翻倍,动这种方案前先回看本文的阈值表;响应式手法本身见《响应式 SVG》。