SVG 元素过多导致标记混乱?试试 `use`。

Avatar of Georgi Nikoloff
Georgi Nikoloff

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

最近,我需要制作一个网页,用于显示分析仪表板中的一系列 SVG 图表。我在每个图表上使用了大量的 <rect><line><text> 元素来可视化某些指标。

这可以正常工作并渲染,但会导致 DOM 树膨胀,其中每个形状都表示为单独的节点。在一个网页上同时显示所有 50 个图表会导致总共有 5,951 个 DOM 元素,这实在太多了。

我们可能会一次显示 50 到 60 个不同的图表,所有图表都具有复杂的 DOM 树。

这出于以下几个原因并非最佳选择

  • 大型 DOM 会增加内存使用量,延长 样式计算时间,并导致代价高昂的 布局重排
  • 它会增加客户端文件的大小。
  • Lighthouse 会降低性能和 SEO 分数。
  • 可维护性成为噩梦——即使我们使用模板系统——因为仍然存在大量冗余和重复。
  • 它无法扩展。添加更多图表只会加剧这些问题。

如果我们仔细查看图表,我们会发现很多重复的元素。

每个图表最终都与其他图表共享许多重复的元素。

这是一个与我们使用的图表类似的虚拟标记示例

<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
>
  <!--
    📊 Render our graph bars as boxes to visualise our data.
    This part is different for each graph, since each of them displays different sets of data.
  -->
  <g class="graph-data">
    <rect x="10" y="20" width="10" height="80" fill="#e74c3c" />
    <rect x="30" y="20" width="10" height="30" fill="#16a085" />
    <rect x="50" y="20" width="10" height="44" fill="#16a085" />
    <rect x="70" y="20" width="10" height="110" fill="#e74c3c" />
    <!-- Render the rest of the graph boxes ... -->
  </g>

  <!--
    Render our graph footer lines and labels.
  -->
  <g class="graph-footer">
    <!-- Left side labels -->
    <text x="10" y="40" fill="white">400k</text>
    <text x="10" y="60" fill="white">300k</text>
    <text x="10" y="80" fill="white">200k</text>
    <!-- Footer labels -->
    <text x="10" y="190" fill="white">01</text>
    <text x="30" y="190" fill="white">11</text>
    <text x="50" y="190" fill="white">21</text>
    <!-- Footer lines -->
    <line x1="2" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="4" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="6" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="8" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <!-- Rest of the footer lines... -->
  </g>
</svg>

这是一个实时演示。虽然页面可以正常渲染,但图表的页脚标记不断重新声明,并且所有 DOM 节点都重复了。

解决方案?SVG 元素。

幸运的是,SVG 有一个 <use> 标签,它允许我们像声明图表页脚一样只声明一次,然后简单地从页面上的任何位置引用它,以根据需要渲染任意多次。 来自 MDN

<use> 元素获取 SVG 文档内的节点,并在其他地方复制它们。其效果与将节点深度克隆到一个未公开的 DOM 中,然后粘贴到 use 元素所在的位置相同。

这正是我们想要的!从某种意义上说,<use> 就像一个模块化组件,允许我们在任何想要的地方放置相同元素的实例。但是,我们不是使用 props 等来填充内容,而是引用我们要显示的 SVG 文件的哪一部分。对于那些熟悉图形编程 API(如 WebGL)的人来说,一个很好的类比是 几何实例化。我们声明要绘制的内容一次,然后可以继续将其用作参考,同时能够更改每个实例的位置、缩放比例、旋转和颜色。

与其为每个图表实例分别绘制图表的页脚线和标签,然后用新的标记一遍遍地重新声明它,不如在单独的 SVG 中 **一次** 渲染图表,然后在需要时简单地开始引用它。<use> 标签允许我们很好地引用其他内联 SVG 元素中的元素。

让我们开始使用它

我们将把图表页脚的 SVG 组——<g class="graph-footer">——移动到页面上单独的 <svg> 元素中。它在前端将不可见。相反,这个 <svg> 将使用 display: none 隐藏,并且只包含一堆 <defs>

<defs> 元素到底是什么?MDN 再次来救援

<defs> 元素用于存储稍后将使用的图形对象。在 <defs> 元素内部创建的对象不会直接渲染。要显示它们,您必须引用它们(例如,使用 <use> 元素)。

有了这些信息,以下是更新后的 SVG 代码。我们将把它放在页面顶部。如果您使用模板,则这将放在某种全局模板(如页眉)中,以便在所有地方都包含它。

<!--
  ⚠️ Notice how we visually hide the SVG containing the reference graphic with display: none;
  This is to prevent it from occupying empty space on our page. The graphic will work just fine and we will be able to reference it from elsewhere on our page
-->
<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
  style="display: none;"
>
  <!--
    By wrapping our reference graphic in a <defs> tag we will make sure it does not get rendered here, only when it's referenced
-->
  <defs>
    <g id="graph-footer">
      <!-- Left side labels -->
      <text x="10" y="40" fill="white">400k</text>
      <text x="10" y="60" fill="white">300k</text>
      <text x="10" y="80" fill="white">200k</text>
      <!-- Footer labels -->
      <text x="10" y="190" fill="white">01</text>
      <text x="30" y="190" fill="white">11</text>
      <text x="50" y="190" fill="white">21</text>
      <!-- Footer lines -->
      <line x1="2" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="4" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="6" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="8" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <!-- Rest of the footer lines... -->
    </g>
  </defs>
</svg>

请注意,我们为组指定了一个名为 graph-footer 的 ID。这很重要,因为它是我们使用 <use> 时所依据的挂钩。

因此,我们所做的是在页面上放置另一个 <svg>,其中包含它所需的数据,然后在 <use> 中引用 #graph-footer 来渲染图表的页脚。这样,就不需要为每个图表重新声明页脚的代码。

<use> 被使用时,看看图表的实例代码变得多么简洁。

<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
>
  <!--
    📊 Render our graph bars as boxes to visualise our data.
    This part is different for each graph, since each of them displays different sets of data.
  -->
  <g class="graph-data">
    <rect x="10" y="20" width="10" height="80" fill="#e74c3c" />
    <rect x="30" y="20" width="10" height="30" fill="#16a085" />
    <rect x="50" y="20" width="10" height="44" fill="#16a085" />
    <rect x="70" y="20" width="10" height="110" fill="#e74c3c" />
    <!-- Render the rest of the graph boxes ... -->
  </g>

  <!--
    Render our graph footer lines and labels.
  -->
  <use xlink:href="graph-footer" x="0" y="0" />
</svg>

这是一个更新的 <use> 示例,没有视觉变化

问题已解决。

什么,您需要证明?让我们比较使用 <use> 版本的演示与原始版本。

DOM 节点文件大小文件大小(GZIP 压缩)内存使用量
不使用 <use>5,952664 KB40.8 KB20 MB
使用 <use>2,572294 KB40.4 KB18 MB
节省节点减少 56%缩小 42%缩小 0.98%减少 10%

如您所见,<use> 元素非常实用。而且,尽管性能优势是这里的重点,但仅仅因为它减少了标记中大量代码这一事实,在维护方面就带来了更好的开发体验。双赢!

更多信息