Code.Movie

Veröffentlicht am 1. Juli 2024

Mein animierter Syntax-Highlighter Code-Movie ist veröffentlicht! Eine einfache JavaScript-API generiert aus Codeschnipseln HTML und CSS, das ohne Laufzeit-JS Schritt-für-Schritt-Animationen darstellt. Einfach durch die Klassen frame0 bis frameN durchschalten (oder dazu die Wrapper-Komponente verwenden) und den Rest übernehmen standardkonforme CSS-Transitions:

Jeder Aspekt des Outputs (Farben, Typografie, Zeilennummern etc.) ist per CSS anpassbar und mit ein bisschen Wrapper-HTML und -CSS bleibt kein Gestaltungswunsch offen. Die meisten Styling-Optionen sind auf der Dokumentations-Seite aufgelistet. Der Code des aktuell angezeigten Frames kann ganz normal markiert und kopiert werden.

Die ersten Iterationen von Code.Movie entstanden ca. 2017, als ich für meine Trainer-Tätigkeit absurd viele Präsentationen rund um Code aus dem Boden stampfen musste. In Powerpoint o.Ä. mit Code-Screenshots zu hantieren, war mir einerseits zu umständlich und andererseits erschienen mir statische Code-Beispiele auch didaktisch eher supoptimal. Statischer Code sieht nicht nur langweilig aus, sondern harte Wechsel zwischen verschiedenen Code-Beispielen sind schlecht nachzuvollziehen. Moderne User Interfaces sind voll mit Animationen, damit wir verstehen, was im UI vor sich geht – und ebenso sollte präsentierter Code animiert sein, damit Refactorings, Tutorials und der Aufbau von Beispielen nachvollziehbar ist. Also schrieb ich eine animierte Syntax-Highlighter-Software, deren dritte Iteration nun erstmals öffentlich zugänglich ist.

Code.Movie ist der dritte kompletter Rewrite des Grundkonzepts und sehr unfertig. Es gibt unzählige Features, die bisher nicht reif für die Öffentlichkeit sind (z. B. Code-Dekorationen und Animations-Beeinflussung) und es gibt nur sehr wenige unterstützte Programmiersprachen. Auch die Webseite ist eher rudimentär, aber dokumentiert alles Wesentliche und enthält sogar einen Online-Playground! Sollte sich Interesse am Projekt manifestieren, können die Features und unterstützten Sprachen schnell mehr werden. Lasst mich wissen, welche Features und Sprachen ihr gebrauchen könntet und was eure Use Cases sind!

TIL-Roundup, Februar 2024: Formulare, Compression Streams, Adopted Stylesheets

Veröffentlicht am 2. April 2024

Auch im letzten Monat haben lautes Nachdenken und lebhafter Austausch auf Mastodon dazu geführt, dass ich einiges über HTML, CSS und JavaScript/DOM erfahren habe, das mir vorher nicht klar war. Und da Mastodon noch immer nicht die intergalaktische Total-Dominanz ausübt, die ihm eigentlich zusteht, kehre ich die gesammelten Erkenntnisse an dieser Stelle nochmals zusammen. Das behalte ich ab nun auch bei, bis ihr alle mir dort folgt.

Formulare in Formularen? Jain! (und mit Browserbugs)

Ich habe schon vor langer Zeit mal einen Workflow für Form-Value-Handling in Formular-Web-Components ausgebrütet, der darauf basierte, im Shadow DOM der Komponente ein inneres <form>-Element zu haben. Dieses Element lässt sich zu FormData serialisieren, was dann wiederum bequem in Submit-Values, value-Attribute und alle sonstigen für die Komponente relevanten Aspekte transformiert werden kann. Im Firefox funktionierte das auch hervorragend, aber bei einigen Komponenten streikte Chrome. Warum? Weil mein Form-Handling-Workflow ungültiges HTML verwendet (wenn man denn HTML verwendet).

Die HTML-Standards verbieten verschachtelte Form-Elemente und (was ich nicht auf dem Schirm hatte) Formulare in Shadow Roots in Formularen gelten als verschachtelte Form-Elemente! Allerdings besteht diese Regel auch nur für HTML, nicht für das DOM. Der folgende Code resultiert in nur einem <form>-Element, da der HTML-Parser das innere Element verwirft:

<form>
  <form></form>
</form>

<!-- Ergebnis: ein <form> im DOM -->

Wenn wir aber gar keinen HTML-Parser involvieren, sondern per JS direkt das DOM manipulieren, erhalten wir verschachtelte Formulare:

let outer = document.createElement("form");
let inner = document.createElement("form");
outer.append(inner);
document.body.append(outer);

// Ergebnis: zwei <form> im DOM

Das ist auch nicht so besonders bizarr: HTML ist nur eine Serialisierung des DOM und hat daher die Freiheit, sich bestimmter DOM-Konstrukte zu verweigern, genau wie JSON mit zahlreichen Aspekten von JavaScript nichts anfangen kann.

Mein Komponenten-Fail in Chrome kam dadurch zustande, dass ich bei den Problem-Komponenten innerHTML für das Shadow-DOM-Setup verwendet habe (und andere DOM-Tools bei den unproblematischen Komponenten). innerHTML verwendet natürlich seinerseits den HTML-Parser, der allerdings offenbar in meinem Haupt-Browser Firefox den Verschachtelungs-Überblick verliert, sobald Shadow DOM involviert ist. Bedeutet: im Firefox funktioniert etwas, das laut HTML-Standard nicht funktionieren dürfte. Endlich kann ich mal einen Bug melden, der nicht einfach nur Gebettel um eine Implementierung von Feature X ist!

Meinen Web-Component-Ansatz mit inneren Formularen werde ich beibehalten, obwohl er sich nicht in HTML serialisieren lässt. Solange die inneren Formulare im Shadow DOM bleiben, stören sie nicht, und solange das Shadow DOM ohne einen (bugfreien) HTML-Parser aufgesetzt wird, sollten sie auch funktionieren. Und ich denke nicht, dass deklaratives Shadow DOM ein sinnvolles Einsatzgebiet für Custom Formular-Inputs sein wird, weswegen ich mir erlaube, die Regeln von HTML an dieser Stelle zu ignorieren.

Kompatibilitätsprobleme von CompressionStreams (und deren Zubehör)

Nachdem ich im Januar CompressionStreams über den grünen Klee gelobt hatte, fielen mir im Folgemonat einige Kompatibilitätsprobleme auf. Seit dem LTS-Release von Node 20 herrscht in Hinblick auf die Kompressionsalgorithmen durch die Bank die gleiche Unterstützung, aber Chrome und Chrome-Derivate implementieren nicht @@asyncIterator auf ReadableStream, sodass für diese Browser folgender Polyfill benötigt wird:

ReadableStream.prototype[Symbol.asyncIterator] ??= async function* () {
  const reader = this.getReader();
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        return;
      }
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
};

TypeScript-kompatible Versionen der compress()- und decompress()-Funktionen aus dem Artikel, mit dem o.g. Polyfill und besserem Error Handling und URL-sicherem Base64 gibt es in meiner Toolsammlung.

Die Reihenfolge von Adopted Stylesheets ist egal

Mit new CSSStyleSheet() erstellte Stylesheet-Objekte können der adoptedStyleSheets-Property eines Shadow Root (oder eines Document) zugewiesen werden, um dem entsprechenden Objekt ein bisschen Style überzuhelfen. adoptedStyleSheets kommt als Array daher und trotzdem ist – für mich überraschend – der Array-Index eines gegebenen Stylesheets für die CSS-Anwendung irrelevant. Es zählt allein die Reihenfolge des Hinzufügens:

function createSheet(css) {
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(css);
  return sheet;
}

const host = document.querySelector(".host");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = "<span>Text</span>"
shadow.adoptedStyleSheets[1] = createSheet("span { color: red }");
shadow.adoptedStyleSheets[0] = createSheet("span { color: green }");
// Ergebnis: grün

Ich hätte mich nicht gewundert, wenn der Text rot geworden wäre, da im Array color: red die letzte Regel ist. Aber da sie zuerst hinzugefügt wurde, gewinnt Grün.

Safari bleibt der neue IE6: kein d in CSS

Ich musste entsetzt zur Kenntnis nehmen, dass Safari d in CSS nicht unterstützt – als einziger relevanter Browser diesseits der Andromeda-Galaxie. Eigentlich ist d ein Attribut des SVG-Elements <path>, das den zu zeichnenden Pfad beinhaltet. Wie so ziemlich jedes SVG-Attribut (fill, stroke etc.) kann auch d als CSS-Eigenschaft ausgedrückt werden und d wird in dieser Rolle mit einem path()-Wert gefüttert, genau wie u. a. clip-path. Einziges Problem: Safari mag d in CSS nicht.

Das ist ziemlich verheerend, da damit ein CSS-Manöver verhindert wird, für das ich mich ansonsten ziemlich gefeiert hätte: per Custom Properties konfigurierbare Inline-SVGs! Jetzt muss ich mich damit begnügen, ein endliches Set von vordefinierten d-Werten über Bande per Custom Property Toggles bereitzustellen. Ein ziemlich enttäuschendes Downgrade.

Weitere Erkenntnisse und Fundstücke

TIL-Roundup Januar 2024: TextEncoder, @import, type-Attribute

Veröffentlicht am 13. Februar 2024

Ich nutze meine Mastodon-Präsenz vor allem, um dumme Fragen zu stellen und laut nachzudenken. Fragenstellen und Nachdenken führt zu Erkenntnisgewinn (in meinem Fall meist in Sachen Browserbugs und Webstandards) und dieser Artikel fasst meine Erkenntnis-Highlights aus dem Januar in etwas organsierterer Form zusammen. Top-Fundstück des Monats waren definitiv die bereits in einem eigenen Artikel verarbeiteten CompressionStreams, aber der Rest kann sich auch sehen lassen!

TextEncoder und TextDecoder

Mir war neu, dass alle Browser unter der Sonne TextEncoder und TextDecoder unterstützen. Der Decoder schluckt Bytes und produziert Strings, der Encoder macht das Gegenteil:

const utf8bytes = new TextEncoder().encode("????");
// > Uint8Array(4) [ 240, 159, 164, 161 ]
const string = new TextDecoder().decode(utf8bytes);  
// > "????"

Der Decoder kann natürlich auch mit mehr als UTF-8 umgehen und lief mir beim Austüfteln von CompressionStreams über den Weg.

Kein @import im CSSStyleSheet()-Constructor (und CSS-Modulen)

Ein Ansatz für CSS in Shadow DOM besteht darin, mit new CSSStyleSheet() Stylesheets aus heißer Luft zu erzeugen und diese einem ShadowRoot (oder Document) zuzuweisen:

const host = document.querySelector("div");
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `Hello`;
const sheet = new CSSStyleSheet();
sheet.replaceSync(":host { color: red }"); // async-Alternative: replace()
root.adoptedStyleSheets.push(sheet);

Die in diesem Stylesheet enthaltenen Regeln gelten dann ausschließlich in den ShadowRoots oder Dokumenten, deren adoptedStyleSheets sie zugewiesen wurden. Zwar handelt es sich hierbei um grundsätzlich zweifelhafte JS-Hexerei auf CSS-Territorium, aber es mangelt nicht an Vorteilen:

  • Web Components können ihre eigenen kleinen CSS-Dateien haben (anstelle von Strings in JavaScript)
  • Wer sich etwas Mühe gibt, Memory Leaks zu umgehen, kann ein CSSStyleSheet-Objekt über mehrere Komponenten-Instanzen teilen
  • Unter Zuhilfenahme von Build-Tools kann CSS zur Compile-Zeit ins JavaScript gebundlet werden, falls das gewünscht ist

Allerdings musste ich feststellen, dass @import in mit new CSSStyleSheet() und CSS-Modulen genutzten Stylesheets nicht funktioniert.. Das Problem ist, dass jedes @import traditionell ein Stylesheet von einer URL lädt und für mehrere Requests auf die gleiche Adresse komplett unterschiedliches CSS geliefert bekommen kann. Für ECMAScript-Module hingegen baut der Browser einmal einen Modul-Graph auf und lädt keine URL zweimal – zwei unterschiedliche Antworten innerhalb eines Ladezyklus sind also ausgeschlossen. CSS-Module wollen Syntax wie import css from './foo.css' ermöglichen, doch hier kollidiert die Funktionsweise von ECMAScript-Modulen mit der von @import. Wir erwarten von import-Statements deterministische Ergebnisse und von @import-Regeln das genaue Gegenteil. Die erwartbare Konsequenz: kein @import in CSS-Modulen und auch kein @import in mit new CSSStyleSheet() erzeugten Stylesheets, in denen sich ein vergleichbarer Widerspruch manifestiert.

type auf <textarea> und <select> (und mehr)

Beim Zusammenstecken einiger Debug-Strings fiel mir auf, dass auf <textarea> und <select> das IDL-Attribut type existiert:

const textarea = document.createElement("textarea");
console.log(textarea.type); // > "textarea"

const select = document.createElement("select");
console.log(select.type); // > "select-one"

const multiSelect = document.createElement("select");
multiSelect.multiple = true;
console.log(multiSelect.type); // > "select-multiple"

Anders als bei <input>, wo das Content-Attribut type den Input-Typ bestimmt, ist das IDL-Attribut type bei <textarea> und <select> read-only. Die Idee dahinter scheint zu sein, dass alle Formular-Elemente eine einheitliche API zum Ermitteln ihres Typs haben sollen, denn auch <output> und <fieldset> haben dieses Feature. Unter den verbliebenen form-associated elements haben <input>, <button> und <object> ohnehin type-Attribute und die einzigen Ausreißer sind <img> (warum ist das überhaupt form-associated?) und eventuelle form-associated custom elements. Was lernen wir daraus?

  1. Formular-Elements können wir allein anhand ihres type auseinanderhalten.
  2. Wenn wir form-assoicated custom elements bauen, sollten sich diese auch die Mühe machen, einen type-Getter zu implementieren, denn sonst funktioniert Punkt 1 nicht mehr.

Punkt 2 ist schon erfüllt, wenn wir einfach nur den folgenden Codeschnipsel in unsere Form-Element-Basisklassen einbauen:

export class FormBaseElement extends HTMLElement {
  // Boilerplate...
  
  get type() {
    return this.tagName.toLowerCase();
  }
  
  // ... mehr Boilerplate...
}

Damit funktioniert type praktisch wie bei <textarea> und erfüllt damit in 99% aller Fälle schon locker seinen Zweck!

Weitere Erkenntnisse und Fundstücke

Folgt mir auf Mastodon, wenn ihr dem nächsten Erkenntnis-Paket live beim Entstehen zusehen wollt!

Natives GZIP in Browsern, Node und Deno

Veröffentlicht am 24. Januar 2024

Das Komprimieren von mittelgroßen JSON-Payloads ist im Web-Frontend keine so ungewöhnliche Anforderung. Ein ausreichend kleines JavaScript-Objekt kann in eine Base64-Repräsentation eines JSON-Strings verwandelt werden, was es wiederum ermöglicht, Applikationsdaten in URLs vorzuhalten. Auf diese Weise können Webapps Speicher- (via Bookmark) und Sharing-Features (via Copy-and-paste der URL) anbieten, ohne ein komplexes Backend zu benötigen, denn ihre URLs sind die Datenbanken. Beispiele für solche Apps sind Babel-Sandbox und der TypeScript-Playground. Beide speichern ihre Applikations-States in ihren URLs – den Source Code Base64-codiert, den Rest als ganz normale Query-Parameter. Und da ich im Moment etwas ganz Ähnliches vorhabe, machte ich mich auf den Weg, die Implementierungen von Babel- und TypeScript-Sandbox zu erforschen und den State-in-URL-Ansatz 1:1 zu stibitzen. Jedenfalls war das der Plan.

lz-string

Dank Open Source war schnell klar, wie die Babel- und TypeScript-Sandboxes funktionieren: Beide verwenden die extrem gut abgehangene JavaScript-Library lz-string, die mithilfe des ebenfalls extrem gut abgehangenen LZW-Algorithmus JavaScript-Strings komprimiert und die Ergebnisse direkt als URL-kompatibles Base64 ausspuckt. Die Kompression reduziert die Länge der jeweiligen Code-Samples und die Base64-Codierung macht das Endergebnis für URLs besser verdaulich. Im Prinzip können URLs auch Unicode enthalten und laut eines Papers der University of Stack Overflow können URLs im Prinzip sehr sehr lang werden, aber im Sinne der besseren Handhabbarkeit sind Kompression und Base64 schon sehr sinnvoll.

lz-string ist ziemlich großartig. Die Library ist extrem stabil, einfach zu benutzen, dabei trotzdem vielseitig und sie kommt mit exakt null eigenen Dependencies daher. Leider stellte sie sich aber als trotzdem nicht wirklich brauchbar für meine Zwecke heraus … denn der Applikations-State, den ich in URLs unterzubringen gedenke, ist viel größer als der State der Babel- und TS-Sandboxes.

Bei allen Vorzügen hat die lz-string-Library doch eine Eigenschaft, die nicht nur positiv ist: Der LZW-Algorithmus ist über 40 Jahre alt und nicht ganz so leistungsstark wie modernere Verfahren. Also sah ich mich veranlasst, NPM nach Alternativen zu durchwühlen und fand wenig Brauchbares. Abgesehen vom allgegenwärtigen Qualitätslimbo ist eine Grundregel von Datenkompression, dass Verbesserungen Kosten haben. Moderne Algorithmen sind viel komplexer als LZW und bezahlen für ihre besseren Kompressionsraten mit teilweise signifikant längeren Laufzeiten und/oder erheblich größeren JS-Bundles. Für Web-Frontends stellt lz-string offenbar einen ziemlich optimalen Kompromiss dar – dumm nur, dass dieser Kompromiss für meine Pläne schlichtweg nicht genug Kompressionsleistung mitbrachte. Mir grauste es bereits vor der Auseinandersetzung mit Web Workers oder WASM, bis mir per Zufall auffiel, dass ich für mein Vorhaben nicht eine andere JavaScript-Libraray benötigte, sondern einfach gar keine!

Compression Streams API

Alle moderneren Browser (sowie Deno und Node) unterstützen offenbar seit Ewigkeiten die Compression Streams API, mit der wir JavaScript-Autor:innen ohne irgendwelche Dependencies Zugriff auf DEFLATE und die DEFLATE-Wrapper gzip und zlib bekommen! Der Deflate-Algoritmus und seine Wrapper-Formate sind schließlich für HTTP-Kompression ohnehin in jedem Browser vorhanden und eine JavaScript-Durchbindung anzubieten ist nicht die verrückteste Idee. Unbekannt war mir die API trotzdem und leicht zu entdecken war sie in der Flut der mittelmäßigen NPM-Packages und AI-generiertem SEO-Spam auch nicht.

Die API ist noch kein fertiger Webstandard und es gibt diverse offene Fragen und Feature Requests, aber die grundsätzliche Funktionalität ist in allen Browsern und JS-Runtimes verfügbar:

const deflateStream = someStream.pipeThrough(
  new CompressionStream("deflate-raw")
);
const compressedData = new Blob(
  await Array.fromAsync(deflateStream)
);

Zur Erklärung:

  • someStream im obigen Beispiel ist ein ReadableStream, der u. a. aus Responses oder Blobs stammen kann
  • neben deflate-raw (DEFLATE, RFC1951) stehen im Moment nur die DEFLATE-Wrapper-Formate deflate (ZLIB, RFC1950, benannt nach seinem entsprechenden HTTP Content-Encoding) und gzip (GZIP, RFC1952) zur Verfügung. Weitere Formate wie Brotli sind in die Diskussion. Die aktuellen APIs erlauben keine Änderung der Standard-Parameter der diversen Formate, insbesondere können keine Dictionaries angegeben werden (Bug #27)
  • Der Blob compressedData verwendet Array.fromAsync(), um den ReadableStream via Async Iteration zu konsumieren und den Inhalt in ein Array zu überführen.

Um mit der API meinen Use Case der App-State-Kompression umzusetzen, brauchte es noch ein paar weitere Zeilen für das Handling von JSON, Base64 und Unicode, aber nicht besonders viele:

async function compress(inputData) {
  const json = JSON.stringify(inputData);
  const deflateStream = new Blob([json])
    .stream()
    .pipeThrough(new CompressionStream("deflate-raw"));
  let binString = "";
  for await (const bytes of deflateStream) {
    binString += String.fromCodePoint(...bytes);
  }
  return btoa(binString);
}

async function decompress(inputBase64) {
  const binString = atob(inputBase64);
  const inputBytes = Uint8Array.from(binString, (s) => s.codePointAt(0));
  const inflateStream = new Blob([inputBytes])
    .stream()
    .pipeThrough(new DecompressionStream("deflate-raw"));
  let json = "";
  for await (const bytes of inflateStream) {
    json += new TextDecoder().decode(bytes);
  }
  return JSON.parse(json);
}

const appState = { user: "foo@bar.de", password: "hunter2" };

const compressedState = await compress(appState);
// Ergebnis: "q1YqLU4tUrJSSsvPd0hKLNJLSVXSUSpILC4uzy9KAYpnlOaVpBYZKdUCAA"

const decompressedState = await decompress(compressedState);
// Ergebnis:  { user: "foo@bar.de", password: "hunter2" }

Diese Implementierung verzichtet auf das Stand Anfang 2024 noch nicht universell unterstützte Array.fromAsync() sowie auf FileReader. Mit FileReader ist das Lesen von Blobs als Base64 und Text nicht nur asynchron, sondern im Prinzip einfacher, aber mangels Promise-Unterstützung auch nicht gänzlich unumständlich. Außerdem stellt sich schnell heraus, dass wir mit Compression Streams auf den Performance-Bonus des asynchronen FileReader bequem verzichten können.

Compression Streams und lz-string im Vergleich

Für meine Bedürfnisse gewinnt Compression Streams gegen lz-string schon allein dadurch, dass es (de facto) ein Webstandard ist. Zwar ist lz-string als Library ein Traum – klein, einfach zu benutzen, vielseitig, ohne eigene Dependencies – aber null Dependencies sind immer noch besser als eine Dependency. Einen hemdsärmligen Performance-Vergleich habe ich aber trotzdem mal angestellt und meine compress()-Funktion gegen compressToEncodedURIComponent() aus lz-string antreten lassen. Als Inputs dienten:

  • Das Winz-Objekt { user: "foo@bar.de", password: "hunter2" }
  • 23k mit json-generator.com erzeugte Zufallsdaten mit vergleichsweise wenig Redundanzen
  • Ein 14k großes Objekt, das einen App-State von meinem Projekt enthält … mit vergleichsweise vielen Redundanzen, v.a. in den Objekt-Keys

Die Laufzeit-Performance von Compression Streams ist immer um ein Vielfaches besser als die von lz-string. Das soll nicht bedeuten, dass lz-string langsam ist, sondern nur, dass Compression Streams noch schneller sind. Für meinen Use Case der periodischen App-State-Speicherung wäre beides locker ausreichend.

In Hinblick auf die Kompressionsrate schlagen Compression Streams auch immer lz-string, aber der Vorsprung ist nicht immer identisch ausgeprägt. Beim sehr kleinen Objekt ist die Differenz vernachlässigbar, bei den zufälligen Daten sind Compression Streams ca. 30% besser und bei meinem hochredundanten App-State fast 3x besser. Die Struktur des letztgenannten Objekts enthält eine ganze Reihe von Redundanzen und komprimiert daher natürlich besonders gut.

Letztlich sind für mich und meinen Use Case Compression Streams ein klarer Sieger gegen lz-string: Sie sind schneller, besser und ohne Dependencies auf jeder Plattform inkl. der Serverseite verfügbar.

Weitere Alternativen zu Compression Streams

Compression Streams haben in meinen Augen drei nennenswerte Vorteile:

  1. Null Dependencies
  2. Cross-Platform-Verfügbarkeit (alle Browser + Node + Deno)
  3. De-Facto-Webstandard (hohe zu erwartende Stabilität)

Fairerweise muss man aber auch sagen, dass es das wirklich alle Vorteilen sind, die Compression Streams zu bieten haben. Wer sich mit der Installation von Dependencies oder der Beschränkung auf einzelne Plattformen anfreunden kann, kann sich eine ganze Reihe anderer Vorteile ins Haus holen:

  1. Außer DEFLATE und seinen Wrappern haben Compression Streams zurzeit keine weiteren Algorithmen anzubieten. Viel moderne Kompressionsverfahren wie etwa Brotli werden von Libraries wie brotli-wasm unterstützt, aber eben um den Preis von Dependency-Installation und WASM-Gefummel.
  2. Compression Streams bieten z.Z. keine API zum Konfigurieren des DEFLATE-Algorithmus, was abhängig von den zu komprimierenden Daten ein ernsthaftes Hindernis darstellen kann. JS-Implementierungen wie deflate-js haben diesbezüglich deutlich mehr zu bieten.
  3. Wer auf Browser-Unterstützung verzichten kann, findet in den Server-JS-Plattformen moderne und konfigurierbare Kompressions-Algorithmen (z.B. Nodes eingebautem Zlib-Modul) vor.

Um es ganz deutlich zu machen: je nach anstehender Aufgabe können Compression Streams mit ihren Einschränkungen bzgl. Algorithmen und Konfigurierbarkeit komplett unbrauchbar sein. Wer für Megabytes an Game-Assets maximale Kompression benötigt, wird mit einer für ein paar Kilobytes JSON ausreichenden Lösung nicht glücklich werden. Wer ohnehin schon über 9000 Dependencies installiert hat und bereits in den Kessel mit WASM-Zaubertrank gefallen ist, hat keine Nachteile durch die zusätzliche Installation von brotli-wasm. Aber wer einfach nur ein bisschen JSON in URLs oder Local Storage quetschen möchte, ist mit Compression Streams durch den Browser out of the box hervorragend versorgt.

Compression Streams auf dem Stand von Anfang 2024 ist definitiv nicht unter allen Umständen die erste Wahl, aber wie so viele Webstandards eine 80/20-Lösung, die bei nüchterner Betrachtung für viele Anwendungsfälle ausreichend ist. Speziell für den Use Case der in URLs (oder Local Storage) hinterlegten App-States sind Compression Streams völlig okay und sogar in allen Belangen besser als das sonst hierfür so populäre lz-string.