Zu viele SVGs verstopfen Ihr Markup? Probieren Sie `use`.

Avatar of Georgi Nikoloff
Georgi Nikoloff am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Kürzlich musste ich eine Webseite erstellen, die eine Reihe von SVG-Diagrammen für ein Analyse-Dashboard anzeigte. Ich verwendete eine Reihe von <rect>-, <line>- und <text>-Elementen in jedem Diagramm, um bestimmte Metriken zu visualisieren.

Das funktioniert und wird problemlos dargestellt, führt aber zu einem aufgeblähten DOM-Baum, bei dem jede Form als separate Knoten dargestellt wird. Die gleichzeitige Anzeige aller 50 Graphen auf einer Webseite führt zu insgesamt 5.951 DOM-Elementen, was viel zu viel ist.

Wir zeigen möglicherweise 50-60 verschiedene Graphen gleichzeitig an, alle mit komplexen DOM-Bäumen.

Dies ist aus mehreren Gründen nicht optimal

  • Ein großes DOM erhöht den Speicherverbrauch, längere Stilberechnungen und kostspielige Layout-Reflows.
  • Es vergrößert die Dateigröße auf der Client-Seite.
  • Lighthouse bestraft die Performance- und SEO-Scores.
  • Die Wartbarkeit ist ein Albtraum – selbst wenn wir ein Templating-System verwenden –, da immer noch viel Müll und Wiederholungen vorhanden sind.
  • Es ist nicht skalierbar. Das Hinzufügen weiterer Graphen verschärft diese Probleme nur.

Wenn wir uns die Graphen genauer ansehen, sehen wir viele wiederholte Elemente.

Jeder Graph teilt letztendlich viele wiederholte Elemente mit den anderen.

Hier ist ein Dummy-Markup, das den verwendeten Graphen ähnelt

<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>

Und hier ist eine Live-Demo. Während die Seite einwandfrei gerendert wird, wird das Markup des Graphen-Footers ständig neu deklariert und alle DOM-Knoten werden dupliziert.

Die Lösung? Das SVG-Element.

Glücklicherweise verfügt SVG über ein <use>-Tag, mit dem wir etwas wie unseren Graphen-Footer nur einmal deklarieren und dann von überall auf der Seite einfach darauf verweisen können, um es beliebig oft darzustellen. Von MDN

Das <use>-Element nimmt Knoten aus dem SVG-Dokument und dupliziert sie an anderer Stelle. Die Auswirkung ist dieselbe, als ob die Knoten tief in ein nicht zugängliches DOM geklont und dann dort eingefügt würden, wo sich das use-Element befindet.

Das ist genau das, was wir wollen! In gewisser Weise ist <use> wie eine modulare Komponente, die es uns ermöglicht, Instanzen desselben Elements überall einzufügen, wo wir möchten. Anstatt jedoch Props und Ähnliches zum Befüllen des Inhalts zu verwenden, verweisen wir darauf, welcher Teil der SVG-Datei wir anzeigen möchten. Für diejenigen unter Ihnen, die mit Grafikprogrammier-APIs wie WebGL vertraut sind, wäre eine gute Analogie Geometry Instancing. Wir deklarieren das zu zeichnende Element einmal und können es dann als Referenz wiederverwenden, während wir die Position, Skalierung, Rotation und Farben jeder Instanz ändern können.

Anstatt die Fußzeilen und Beschriftungen unseres Graphen für jede Graphinstanz einzeln zu zeichnen und sie dann immer wieder mit neuem Markup neu zu deklarieren, können wir den Graphen einmal in einer separaten SVG rendern und einfach darauf verweisen, wenn er benötigt wird. Das <use>-Tag ermöglicht es uns, Elemente von anderen Inline-SVG-Elementen problemlos zu referenzieren.

Lassen Sie uns es in Gebrauch nehmen

Wir werden die SVG-Gruppe für den Graphen-Footer – <g class="graph-footer"> – in ein separates <svg>-Element auf der Seite verschieben. Es wird auf dem Frontend nicht sichtbar sein. Stattdessen wird dieses <svg> mit display: none versteckt und enthält nur eine Reihe von <defs>.

Und was genau ist das <defs>-Element? MDN rettet uns wieder einmal.

Das <defs>-Element wird verwendet, um Grafikobjekte zu speichern, die später verwendet werden sollen. Objekte, die innerhalb eines <defs>-Elements erstellt werden, werden nicht direkt gerendert. Um sie anzuzeigen, müssen Sie sie referenzieren (z. B. mit einem <use>-Element).

Bewaffnet mit diesen Informationen, hier ist der aktualisierte SVG-Code. Wir werden ihn ganz oben auf der Seite platzieren. Wenn Sie Templates verwenden, würde dies in einer Art globalem Template, wie einem Header, landen, damit es überall enthalten ist.

<!--
  ⚠️ 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>

Beachten Sie, dass wir unserer Gruppe die ID graph-footer gegeben haben. Das ist wichtig, da es der Haken ist, wenn wir nach <use> greifen.

Wir fügen also ein weiteres <svg> auf der Seite ein, das die benötigten Graph-Daten enthält, und referenzieren dann #graph-footer in <use>, um den Footer des Graphen zu rendern. Auf diese Weise müssen wir den Code für den Footer nicht für jeden einzelnen Graphen neu deklarieren.

Sehen Sie, wie viel sauberer der Code für eine Graphinstanz ist, wenn <use> in ... äh, Gebrauch ist.

<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>

Und hier ist ein aktualisiertes <use>-Beispiel ohne visuelle Änderung

Problem gelöst.

Was, Sie wollen Beweise? Vergleichen wir die Demo mit der <use>-Version mit der Originalversion.

DOM-KnotenDateigrößeDateigröße (GZIP-Kompression)Speichernutzung
Kein <use>5,952664 KB40,8 KB20 MB
Mit <use>2,572294 KB40,4 KB18 MB
Einsparungen56 % weniger Knoten42 % kleiner0,98 % kleiner10 % weniger

Wie Sie sehen, ist das <use>-Element sehr nützlich. Und obwohl die Leistungsverbesserungen hier im Vordergrund standen, macht allein die Tatsache, dass es riesige Codeblöcke aus dem Markup entfernt, die Wartung des Ganzen zu einer wesentlich besseren Entwicklererfahrung. Doppelt gewonnen!

Weitere Informationen