Viele Webkomponenten und ein Style - ein Performanceproblem oder eine Lösung?

huluvu424242

Thomas Schubert

Posted on October 2, 2021

Viele Webkomponenten und ein Style - ein Performanceproblem oder eine Lösung?

Webkomponenten sind eine große Errungenschaft bei der Erstellung von Weboberflächen. Sie stellen autonom arbeitende Bündel dar welche alles zum Funktionieren enthalten und können auf jeder Webseite beliebig oft eingesetzt werden. Einzige Bedingung die Webkomponente muss dem Browser per URL bekannt gegeben werden, damit dieser diese Laden und Ausführen kann.

Autonom heißt aber auch, jeder Hersteller von Webkomponenten kann diese stylen wie er gern möchte und der verwendete Stil lässt sich von außen nicht mehr ändern. Diese Eigenschaft ist aber bei großen Portalen eher unerwünscht. Hier möchte man gern die Wiederverwendbarkeit der Webkomponenten einsetzen aber erwartet zugleich, dass der Style aller Webkomponenten zueinander passt und sich harmonisch verhält. Sprich die Corporate Identity oder das zentral vorgegebene Styling soll in jeder Webkomponente angewendet werden.

Um auf einer Webseite mit vielen Webkomponenten einen einheitlichen Style zu realisieren, sind einige Möglichkeiten denkbar. Der hier verlinkte Artikel arbeitet heraus welche Möglichkeiten sich anbieten und welche Vor- und Nachteile diese besitzen: Styling Web Components Using A Shared Style Sheet. Trotzdem der Artikel noch aus der Zeit der Spezifikation v1 von Webkomponenten stammt, sind die Betrachtungen bis heute gültig, da sich am Wesen der Webkomponenten seit dem nichts geändert hat. Die Änderungen zur Spezifikation v2 betrafen nur technische Raffinessen (Stark vereinfacht war die wesentliche Neuerung die Ersetzung der HTML Importe durch ESM Importe). Nachfolgend werde ich einige Möglichkeiten kurz aufzeigen um letztlich eine von mir bevorzugte Lösung zu präsentieren.

Die erste und plumpeste Lösung wäre natürlich, den einheitlichen Style in jede Webkomponente durch Kopieren zu integrieren. Das gelingt natürlich nur bei Komponenten, welche man selbst herstellt und führt zu einer extremen Vergrößerung aller Komponenten. Damit einher geht natürlich auch eine höhere Ladezeit je Komponente. Dieser Weg scheidet auf Grund offensichtlicher Sinnlosigkeit aus.

Es gibt noch die Möglichkeit den Style von Webkomponenten durch CSS Variablen (Custom Properties) von außen zu ändern. Diese Möglichkeit eignet sich aber eher zum marginalen customizing als zur Definition vollständiger Stile. Hierunter fällt auch die Anwendung von @apply rules, wobei man speziell zu @apply rules sagen muss, dass diese auch kaum von Browserherstellern unterstützt werden.

Ein zweiter Weg besteht dann darin, die Webkomponenten ohne Shadow DOM zu bauen. Aus meiner Sicht führt das das Konzept von Webkomponenten an sich ad absurdum. Die Idee dabei ist, die Webseite gibt den global anzuwendenden Style vor und da kein Shadow DOM vorhanden ist, wirkt sich dieser direkt auf eingebettete Webkomponenten aus. So kann eine Style Bibliothek wie bootstrap in die Webseite eingebunden werden und alle Webkomponenten müssen nur konform zur Bibliothek implementiert werden.
Die Probleme lassen hierbei auch nicht lange auf sich warten. Das Hauptproblem sind die Abhängigkeiten. Beispielsweise technische - das Layout muss schon geschickt gewählt werden, damit eine untergeordnete Komponente nicht alles zerschießt. Aber auch organisatorische Abhängigkeiten spielen eine Rolle. Wenn nur genügend Webkomponenten von genügend verschiedenen Teams erstellt werden und die zentrale Style Bibliothek in einer neuen aber inkompatiblen Version bereitgestellt wird. Die einen Sprachkonstrukte gehen noch, die anderen nicht mehr. Nicht jedes Team schafft es zeitgleich eine neue Version aller ihrer Webkomponenten auszuliefern. Das eine Team hat gar nicht erst gemerkt, dass die zentrale Lib geändert wurde und betrachtet am nächsten Tag in PROD verwundert ihre Komponente mit der Bemerkung: "Also gestern sah es top aus, da muss gestern Abend jemand zentral was geändert haben.". Dieses System funktioniert eine Weile, muss bei großen Projekten mit kontinuierlicher Weiterentwicklung aber früher oder später aufgegeben werden. Auch erzeugt es dann beim Umbau auf Shadow DOM einiges an Aufwand. Außerdem hatte ich das Gefühl, dass der Kode sich ständig in Richtung big balls of mud entwickelt.

Eine dritte Möglichkeit eröffnet sich, wenn man selbst alle verwendeten Webkomponenten herstellt und diese in einem Projekt verwaltet. Dann kann man auf der obersten Ebene im Projekt einen assert Folder anlegen, in diesem legt man die Stylesheets der Designvorlagen ab und im Style jeder Komponente referenziert man diese Stylevorlagen dann mit @import. Diese Methode funktioniert prinzipiell in Stencil Projekten - zumindest kann ich das für kleine Projekte mit Webkomponenten bestätigen. Aus meiner Sicht sind aber bei größeren Projekten Performanceprobleme zu erwarten. Außerdem ist diese Vorgehensweise gerade bei Webkomponenten ein Antipattern (siehe don't use @import ).

Eine ganze zeitlang gab es keine weiteren Alternativen, da die Spezifikationen nicht mehr erlaubten. Viele bauten sogenannte Style Module beispielsweise mit dem Framework Polymer und nutzen diese dann in jeder ihrer Webkomponenten. Die ofizielle Alternative um das Dilemma der Shared Styles mit Webkomponenten zu überwinden heißt "Constructable Stylesheets" (siehe Constructable Stylesheets: seamless reusable styles ). Doch die Spezifikation und Realisierung der Lösung dauert an und scheint auf nahe Sicht keine praxisrelevanten Ergebnisse abzuwerfen.

Kommen wir jetzt zu meiner bevorzugten Lösung, welche heute funktioniert, welche ich in privaten Stencil Projekten einsetze, welche aber nicht für immer funktionieren muss, da sie möglicherweise tatsächlich später von constructable stylesheets abgelöst wird.

Aus den oben aufgezeigten Möglichkeiten gefällt mir der Gedanke an ein Style Modul, welches selbst eine Webkomponente ist, am besten. Natürlich möchte ich Webkomponenten bauen und diese stylen und das bedeutet für mich, ich werde in meinen Komponenten definitiv einen Shadow DOM verwenden. Wie auch immer ein Style Modul aussieht, es muss in der Lage sein Webkomponenten mit Shadow DOM zu stylen.

Der Autor des oben erwähnten Artikels "Styling Web Components Using A Shared Style Sheet" hatte bereits festgestellt, dass die Spezifikation zunächst die Verwendung eines <link rel=“stylesheet”> Tag im Shadow DOM verboten hatte, dann aber später diese wieder erlaubte. Bei den Betrachtungen im Internet, dass @import ein Antipattern darstellt, wird immer wieder darauf verwiesen, dass die bessere Alternative die Verwendung eines Link Tags ist. Damit bleibt die Steuerung des Ladens des einzelnen Stylesheets in der Hoheit des Browsers und er kann entscheiden wie caching oder paralleles Laden realisiert werden.

Die simple Lösung lautet daher - setze ein <link rel=“stylesheet”> am Anfang des Shadow DOM Deiner Webkomponente ein und lade damit die Shared Style Bibliothek welche Du einsetzen möchtest. Schon hast Du sichergestellt, dass alle Komponenten die gleiche Lib in der gleichen Version nutzen.

Gut offensichtlich gibt es ein Problem, wenn die Version der Lib erhöht werden soll. Dann musst Du manuell in jeder Webkomponente den Link anpassen. Das will man nicht. Webkomponenten sind HTML Tags und lassen sich über Attribute parametrisieren. Aber jeder Webkomponente den zu nutzenden Stylesheet URL als Attribut übergeben will man eigentlich auch nicht.

Also baut man eine eigene Webkomponente, welche nur diesen Stylesheet URL besitzt. Diese Komponente ist jetzt Dein Style Modul. Wichtig beim Style Modul ist, dass es tatsächlich keinen Shadow DOM besitzt, denn es soll ja seine Umgebung explizit beeinflussen und in die umgebende Webseite oder den umgebenden Shadow DOM "ausbluten". Außerdem sollte die CSS Style Bibliothek über das Style Modul mit ausgeliefert werden und nicht aus dem Internet geladen werden um keine zusätzliche Abhängigkeit aufzubauen. Der Gedanke wäre, dass das Link Tag in dem Fall die gleiche Performance und die gleichen Vorteile bietet wie sonst auch.

Das Bild zeigt schematisch die Projektstruktur.

Projektstruktur des Style Modules

Der folgende Kode Schnipsel zeigt meine Realisierung des <honey-papercss-style> Style Modules in Stencil.

import {Component, getAssetPath, h} from '@stencil/core';

@Component({
  tag: 'honey-papercss-style',
  assetsDirs: ['assets']
})
export class HoneyPapercssStyle {

render() {
    const stylePath: string = getAssetPath('./assets/paper.min.css');
    return <link rel="stylesheet" href={stylePath}/>
  }
Enter fullscreen mode Exit fullscreen mode

Das vom Style Modul definierte Tag verwendest Du einfach am Anfang jedes Shadow DOM Deiner eigenen Komponenten und body Deiner Webseite und schon hast Du es geschafft. Hier ein Beispiel:

Anwendung des honey-papercss-style Tags

Ändert sich der URL musst Du nur den URL Wert im Style Modul anpassen und eine neue Version des Style Moduls bereitstellen. Da dem Browser zentral im Header einer Webseite bekannt gegeben wird welche Tags Webkomponenten sind und von wo diese zu laden sind, müssen nun nur noch die URLs zum Laden der Webkomponente des Style Moduls aktualisiert werden.

Einbinden des Style Moduls

In meinem ersten Versuch habe ich mein Style Modul "honey-papercss-style" genannt. Somit habe ich in allen meinen Webkomponenten zu Beginn des Shadow DOM und in den umgebenden Webseiten am Beginn des Body das Tag <honey-papercss-style /> eingefügt. Das hat super funktioniert und ist meine bevorzugte Lösung.
Diese eignet sich, wenn man eigene Webkomponenten baut und das Style Modul wirklich nur aus einer CSS Lib wie Bulma oder PaperCSS besteht. Wie das mit Javascript Style Bibliotheken funktioniert (beispielsweise bootstrap) kann ich nicht sagen. Auf jeden Fall wird die Implementierung da etwas kniffliger und aufwändiger. Möglicherweise stellt sich auch heraus, dass diese Lösung für Style Bibliotheken mit Javascript Anteil nicht sinnvoll ist. Nachfolgend beschreibe ich noch ein paar Verbesserungen meiner Lösung um diese generischer einsetzen zu können aber immer nur mit reinen CSS Style Bibliotheken.

Da ich meine Webkomponenten auch für Andere bereitstellen möchte und diese ihre Style Module vielleicht anders nennen, beispielsweise <lovely-bulma-style /> war ich noch nicht zufrieden mit meiner Lösung. So wollte ich eine Lösung bei der ich als erstes auf der Webseite das verwendete Style Modul festlegte und dann in den Webkomponenten ein Tag einbaue welches dynamisch diesen definierten Style ermittelt und das konkrete Style Modul anzieht. Bei der Erarbeitung der Lösung habe ich mir eine uralte Eigenschaft der Browser zu nutze gemacht: Unbekannte Tags werden lautlos ignoriert.

Die in einer Webseite angewendete Lösung sieht so aus:

<honey-define-style>
  <honey-papercss-style/>
</honey-define-style>
Enter fullscreen mode Exit fullscreen mode

Damit wird die Benutzung des papercss Style Moduls festgelegt. In den Webkomponenten selbst wird das Style Modul dann über das Tag <honey-apply-style/> angewendet. Die Komponente <honey-apply-style/> sucht über window.document nach dem Tag <honey-define-style> und ruft eine dort definierte Methode auf, welche den Tagnamen des zu verwendenden Style Moduls zurück gibt. Dieser Tagname wird dann nur noch als Tag in den Shadow DOM eingehangen. Wichtig dabei ist, dass weder das Style Modul noch die <honey-apply-style> Komponente über einen Shadow DOM verfügen. Hier ein Beispiel der Anwendung.

Anwendung des honey-apply-style Tags

Bei der Realisierung der Webkomponente <honey-apply-style> gibt es 3 Knackpunkte:
1) Da sie eine Methode von <honey-define-style> nutzt, sollten beide Komponenten in einem Projekt bereitgestellt werden, wodurch die Kompatiblität auch bei Versionswechsel sichergestellt werden kann.
2) Die Komponente <honey-apply-style> muss vor dem Aufruf der Methode getStyleModulname solange warten, bis die Komponente <honey-define-style> im DOM eingehängt und vollständig initialisiert ist (speziell auch das Style Modul eingehängt ist).
3) Der Tagname im DOM muss aus einer String Variablen zusammengebaut werden.

Punkt 3 lässt sich in JSX lösen, indem der Variablenname mit einem Großbuchstaben beginnt, mit einem Kleinbuchstaben funktioniert es nicht (Vielen Dank an die Schöpfer der hybriden Sprache JSX - es ist ja nicht so, dass das Konzept von 4GL Sprachen spätestens mit JSP grandios gescheitert wäre. Einfach nochmal probieren - wie sinnvoll: Und der Imperator sprach: "Kommt wir bauen noch einen Todesstern - hat ja beim ersten Mal schon so gut geklappt. Was soll da schon schief gehen.")

Punkt 2 lässt sich erfüllen indem wie folgt gewartet wird:

await customElements.whenDefined('honey-define-style');

Die ganze Implementierung findet sich hier: https://github.com/Huluvu424242/honey-style-it/blob/release/0.0.5/src/components/honey-apply-style/honey-apply-style.tsx Natürlich werde ich an dem Projekt weiterentwickeln und es vielleicht irgendwann ruinieren. In der Version 0.0.5 hat es aber noch so wie hier beschrieben funktioniert :)

Nur falls Github mal ausfällt, hier als Backup die Implementierung von <honey-apply-style>

import {Component, h, State} from '@stencil/core';
import {HoneyDefineStyle} from "../honey-define-style/honey-define-style";
import {Subscription} from "rxjs";
import {printDebug, printError, ThemeListener} from "../../shared/helper";

@Component({
  tag: 'honey-apply-style',
})
export class HoneyApplyStyle {

  themeSubscription: Subscription;


  /**
   * tagName of honey style sheet to apply e.g. 'honey-papercss-style'
   */
  @State() theme: string;

  async connectedCallback() {
    try {
      await customElements.whenDefined('honey-define-style');
      const styleElements: HoneyDefineStyle = document.querySelector('honey-define-style') as unknown as HoneyDefineStyle;
      const listener: ThemeListener = {
        next: (styleName: string) => this.theme = styleName,
        error: (error) => printError(error),
        complete: () => printDebug("subcription completed")
      };
      this.themeSubscription = await styleElements.subscribeThemeChangeListener(listener);
    } catch (error) {
      this.theme = 'honey-default-style';
    }
  }

  disconnectedCallback() {
    this.themeSubscription.unsubscribe();
  }

  render() {
    // Grossbuchstabe für Variable notwendig für JSX
    const TagName = this.theme;
    return (<TagName/>)
  }
}
Enter fullscreen mode Exit fullscreen mode

Zum Schluss möchte ich unbedingt erwähnen, dass ich natürlich nicht der Erste bin welcher auf diese Lösung gekommen ist. Gerade eben (nachdem ich dieses Posting schon einige Tage online gestellt hatte) habe ich noch folgenden Artikel gefunden: https://dev.to/lamplightdev/how-to-share-styles-in-the-shadow-dom-4ag6 Der Autor kommt auf die gleiche Lösung mit der Verwendung des Link Tags. Der Artikel ist aus meiner Sicht sehr lesenswert, da der Autor dort erstens das Ganze auf englisch beschreibt und zweitens den Sachverhalt kompakter erklärt als ich in meinem Artikel.

Trotzdem halte ich meinen Artikel weiterhin nicht für überflüssig, weil ich noch einen kleinen Schritt weiter gehe und die Lösung in ein eigenes Style Modul verpacke. Hier würde es mich freuen, wenn in der Kommentar Region Anmerkungen kämen ob das eine gute Idee ist beispielsweise aus Performance Sicht. Selbst halte ich es natürlich für eine gute Idee, bin mir aber unsicher da ich mit den internen Abläufen im Browser nicht so vertraut bin.

💖 💪 🙅 🚩
huluvu424242
Thomas Schubert

Posted on October 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related