最近,我需要制作一个网页,用于显示分析仪表板中的一系列 SVG 图表。我在每个图表上使用了大量的 <rect>
、<line>
和 <text>
元素来可视化某些指标。
这可以正常工作并渲染,但会导致 DOM 树膨胀,其中每个形状都表示为单独的节点。在一个网页上同时显示所有 50 个图表会导致总共有 5,951 个 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,952 | 664 KB | 40.8 KB | 20 MB |
使用 <use> | 2,572 | 294 KB | 40.4 KB | 18 MB |
节省 | 节点减少 56% | 缩小 42% | 缩小 0.98% | 减少 10% |
如您所见,<use>
元素非常实用。而且,尽管性能优势是这里的重点,但仅仅因为它减少了标记中大量代码这一事实,在维护方面就带来了更好的开发体验。双赢!
我还没有深入研究 SVG 的世界,但这看起来很棒,文章很棒!
感谢这个很棒的输入!我觉得我们可以对这些 SVG 做更多的事情,而不是我们目前正在做的。(我知道我正在做)
谢谢!
您也可以考虑使用 `path` 元素并将所有相同颜色的条形渲染为单个路径。如果您希望条形能够单独交互(例如使用工具提示),则此方法可能不可行。
这是 SVG 最好的功能之一,也是 SVG 精灵图的基础。但是,需要注意的是:如果您将 SVG 托管在与它们所服务的主机不同的域上,并且该域尚未完全兼容 CORS,则它将无法呈现任何内容。太糟糕了。
我在 MDN 中注意到,`xlink:href` 将在 svg2 中被弃用,那么我们是否应该只使用 `href`?它的工作方式是否相同?
没错,它已被弃用,您应该使用 `href` 而不是 `xlink:href`。
您还可以通过使用 Web Components 来提高可读性和可维护性。您可以创建一个自定义元素,例如 `<metric-graph points="...">`,并动态生成 SVG(当然使用 `<use>`!)。这样,一组图表可能如下所示:`<article class="graph-grid"><metric-graph points="-0.5 7.2 3.1"/><metric-graph points="1.2 -2.4 3.8"/></article>`
+1 支持使用 Web Components 的方法
这个想法看起来很有趣,也许你可以进一步推进它
如果您在页面构建中使用了服务器端语言(php、python 等),您可能会将原始数据作为数组或列表获取。
您可以测试以下内容
1) 对于您需要绘制的每个矩形,计算一个 ID,用于表示矩形的颜色和高度。
2) 如果此 ID 之前已经计算过,则使用它。否则,绘制它,并为其添加 ID。
您甚至可以将其存储在“显示为无的 SVG”中,前提是它至少被使用两次,而不是唯一的情况,以最大限度地节省节点和代码大小。
是的,SVG 的“use”元素非常有用!例如,在此演示中,每个点都是一个 use 元素
https://tobireif.com/demos/snake_pattern/
您好,您的 `use` 的 `href` 属性不应以井号开头,例如 `<use xlink:href="#graph-footer" x="0" y="0" />` 吗?我相信这是我在其他教程中看到的方式。
很棒的文章!谢谢,Georgi!
嗨,
有人根据这篇文章制作了 SVG 图标的 npm 包
https://npmjs.net.cn/package/@novyk/ikong
太酷了!!感谢您告诉我,否则我 100% 会错过的