Blog

HTML5, CSS3 und ECMAScript als Baseline

Im Frühjahr 2017 tragen CSS Grid, Flexbox, fetch und natives ES2015 so weit, dass die erste Architekturfrage im Frontend neu gestellt gehört: Wie weit komme ich ohne Framework?

≈ 8 Min. Lesezeit

Diesen Beitrag anhören (12 Min.)

MP3 herunterladen

Baseline meint hier etwas Bestimmtes: die Menge an HTML-, CSS- und JavaScript-Fähigkeiten, die in allen Browsern eines Projekts ohne eine einzige Abhängigkeit läuft. Kein npm install, kein Framework, kein Plugin – nur das, was die Plattform von Haus aus mitbringt. Diese Menge ist keine feste Größe. Sie hängt davon ab, welche Browser ein Projekt tatsächlich bedienen muss, und sie verschiebt sich mit jedem Browser-Release. Wer sie kennt, kann eine Frage präzise beantworten, die sonst reflexhaft beantwortet wird: Wofür genau brauche ich eine Abhängigkeit – und wofür nicht?

Diese Frage lohnt sich gerade jetzt besonders, denn die Baseline hat sich in den letzten Monaten so weit nach oben geschoben wie lange nicht.

Ein ungewöhnliches Frühjahr

Im März sind Chrome 57, Firefox 52 und Safari 10.1 innerhalb weniger Wochen mit CSS Grid Layout erschienen. Dass ein großes Layout-Feature praktisch zeitgleich und ohne Präfixe in drei großen Browsern landet, habe ich in dieser Form noch nicht erlebt. Sonst zog sich so etwas über Jahre – man denke an die drei verschiedenen Flexbox-Syntaxen, die sich lange nebeneinander gehalten haben. Grid ist von Tag eins an in der finalen Syntax nutzbar.

Grid ist aber nur der sichtbarste Teil. Wer die Zielbrowser auf aktuelle Versionen von Chrome, Firefox, Safari und Edge eingrenzen kann, hat im Mai 2017 ohne jede Abhängigkeit zur Verfügung:

  • Flexbox und CSS Grid für Layout, dazu @supports für saubere Feature-Abfragen im Stylesheet
  • CSS Custom Properties (seit letztem Jahr breit unterstützt, IE11 ausgenommen)
  • ES2015 nativ: Klassen, Arrow Functions, Destructuring, Template Literals, Promises
  • async/await aus ES2017 in den aktuellen Versionen der Evergreen-Browser
  • fetch als eingebauten HTTP-Client, seit Safari 10.1 auch dort
  • Custom Elements v1 und Shadow DOM v1 in Chrome und Safari
  • Service Worker in Chrome und Firefox – Safari und Edge fehlen hier noch, das gehört zur Ehrlichkeit dazu

Vor zwei, drei Jahren war jeder einzelne Punkt dieser Liste ein Grund, eine Bibliothek zu installieren. Genau deshalb ist mir die Frage wieder wichtig geworden. In einem Code-Review bin ich kürzlich durch ein Frontend gegangen, das jQuery, ein Grid-System, eine AJAX-Hilfsbibliothek und einen Klassen-Emulator mitschleppte – zusammen deutlich über 100 KB, bevor die erste eigene Zeile lief. Für jede dieser Abhängigkeiten gab es 2013 einen guten Grund. 2017 erledigt die Plattform alle vier Aufgaben selbst. Niemand hatte die Entscheidung je zurückgenommen, weil niemand sie je als Entscheidung betrachtet hatte.

Layout gehört jetzt der Plattform

Das Beispiel, an dem ich das durchspiele, ist bewusst gewöhnlich: eine Produktübersicht, Karten in einem Raster, Daten von einer HTTP-Schnittstelle. Der Klassiker, für den man sonst Bootstrap samt jQuery-Abhängigkeit installiert.

Das Raster zuerst. Mit Grid ist das Layout eine Handvoll Zeilen, und der Fallback für ältere Browser ist keine zweite Codebasis, sondern eine Schicht darunter:

/* baseline for every browser: a simple flex wrap */
.products {
  display: flex;
  flex-wrap: wrap;
}

.product-card {
  flex: 1 1 240px;
  margin: 8px;
}

/* enhancement for browsers with grid support */
@supports (display: grid) {
  .products {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    grid-gap: 16px;
  }

  .product-card {
    margin: 0;
  }
}

Zwei Dinge daran finde ich bemerkenswert. Erstens: repeat(auto-fill, minmax(240px, 1fr)) ersetzt das, wofür Grid-Systeme jahrelang Klassen wie col-md-4 und eine Media-Query-Kaskade gebraucht haben – ohne eine einzige Media Query. Zweitens: @supports macht die Browser-Matrix zu einer Design-Entscheidung statt zu einem Kompatibilitäts-Hack. IE11 und der aktuelle Edge bekommen das Flexbox-Raster, moderne Browser das Grid. Die Seiten sehen nicht pixelgleich aus, und das ist in Ordnung – die Vorstellung, jede Browser-Version müsse identisch rendern, hat uns jahrelang unnötig viel Code gekostet.

Ehrlich bleiben muss man bei Edge: Der unterstützt bislang nur die alte Grid-Syntax mit -ms-Präfix, die noch aus IE10-Zeiten stammt. Wer Edge im Zielkorb hat, behandelt ihn beim Grid vorerst wie IE11 und fährt den Fallback. Genau dafür ist die @supports-Abfrage da.

Daten laden ohne Bibliothek

Der zweite Baustein ist der Datenabruf. Wofür früher $.ajax oder eine eigene XHR-Kapselung nötig war, reicht heute fetch – und mit async/await liest sich der Kontrollfluss endlich wie synchroner Code. Wie mühsam dieselbe Aufgabe vor drei Jahren mit Callback-Ketten war, habe ich damals in einem eigenen Beitrag festgehalten; die Distanz zwischen diesen beiden Welten ist erstaunlich kurz.

async function loadProducts() {
  const response = await fetch('/api/products', {
    headers: { 'Accept': 'application/json' }
  });

  // fetch resolves on HTTP errors too, so check explicitly
  if (!response.ok) {
    throw new Error('Request failed with status ' + response.status);
  }

  return response.json();
}

Der Kommentar im Code markiert die Stolperstelle, über die ich in Reviews am häufigsten stolpere: fetch lehnt das Promise bei einem 404 oder 500 nicht ab, sondern nur bei Netzwerkfehlern. Wer von jQuery kommt, erwartet das Gegenteil. Solche Eigenheiten muss man einmal lernen – aber man lernt dabei die Plattform, nicht die Meinung einer Bibliothek, und dieses Wissen veraltet deutlich langsamer.

Für IE11 gilt: kein fetch, kein Promise, kein async/await. Wer ihn bedienen muss, kompiliert mit Babel und lädt zwei kleine Polyfills. Das ist ein Build-Schritt, den ich sonst gern vermeide – aber er ist überschaubar, gezielt und vor allem befristet. Der Polyfill fliegt raus, sobald IE11 aus der Matrix fällt. Eine Framework-Abhängigkeit fliegt erfahrungsgemäß nie raus.

Komponenten direkt im Browser

Der dritte Baustein ist der interessanteste, weil er am dichtesten an dem liegt, wofür man sonst zu einem Framework greift: wiederverwendbare Komponenten. Custom Elements v1 sind in Chrome und Safari angekommen, und damit lässt sich die Produktliste als eigenes HTML-Element definieren:

class ProductList extends HTMLElement {
  async connectedCallback() {
    this.textContent = 'Loading products ...';

    try {
      const products = await loadProducts();
      this.render(products);
    } catch (error) {
      this.textContent = 'Products are currently unavailable.';
    }
  }

  render(products) {
    this.innerHTML = '<div class="products"></div>';
    const container = this.querySelector('.products');

    products.forEach(product => {
      const card = document.createElement('article');
      card.className = 'product-card';
      card.textContent = product.name;
      container.appendChild(card);
    });
  }
}

customElements.define('product-list', ProductList);

Im Markup steht danach schlicht <product-list></product-list>, und der Browser ruft die Lifecycle-Methoden selbst auf. Kein virtueller DOM, kein Data-Binding, keine Render-Schleife – für eine Komponente dieser Größe braucht es das alles nicht. Firefox und Edge benötigen dafür noch das Polyfill aus dem webcomponents.org-Projekt, und das ist nicht gratis: Es bringt spürbar Gewicht mit und muss in der Browser-Matrix mitgetestet werden. Wer heute produktiv auf Custom Elements setzt, sollte diese Kosten kennen und nicht nur die Chrome-Demo.

Am Rand notiert: Native ES-Module im Browser stehen ebenfalls kurz bevor – Safari 10.1 kann <script type="module"> bereits, bei den anderen liegt es hinter Flags. Für den Moment bleibt das Bundling beim Build; aber auch diese Lücke schließt sich gerade.

Die Prüfreihenfolge

Aus diesen Bausteinen ergibt sich eine Reihenfolge, die ich inzwischen bei jeder Frontend-Anforderung durchgehe – nicht als Dogma, sondern als Prüfweg, der Abhängigkeiten zu begründeten Entscheidungen macht statt zu Reflexen:

flowchart TD
  A[Anforderung im Frontend] --> B{Kann die Plattform das?<br/>caniuse gegen die Zielbrowser}
  B -- ja, in allen Zielbrowsern --> C[Baseline nutzen<br/>keine Abhängigkeit]
  B -- ja, bis auf Ausnahmen --> D{Fallback per @supports<br/>oder Polyfill vertretbar?}
  D -- ja --> E[Gezielt nachrüsten<br/>Polyfill ist befristet]
  D -- nein --> F[Bibliothek für genau<br/>diese eine Lücke]
  B -- nein --> F
  F --> G{Bilden mehrere Lücken<br/>zusammen ein Muster?}
  G -- ja --> H[Framework – mit klarem Wofür]
  G -- nein --> C

Der entscheidende Punkt ist der letzte Knoten. Ein Framework ist in diesem Bild nicht verboten – es steht nur am Ende der Prüfung statt am Anfang. Wer den Weg durchlaufen hat, kann das Wofür präzise benennen: „Wir brauchen React, weil wir zwanzig Ansichten mit geteiltem Zustand haben und das Data-Binding von Hand nicht mehr beherrschbar ist" ist eine Architekturentscheidung. „Wir nehmen React, weil man das heute so macht" ist keine.

Die ehrliche Gegenseite

Damit das nicht wie ein Plädoyer für Frontend-Askese klingt: Es gibt gute Gründe, 2017 weiterhin Abhängigkeiten zu installieren, und die gehören auf den Tisch.

Die Browser-Matrix ist der größte. Wer ein internes System für einen Konzern baut, in dem IE11 per Gruppenrichtlinie gesetzt ist, hat eine andere Baseline als jemand, der eine Website für ein technikaffines Publikum betreibt. Für die IE11-Welt bedeutet der Plattform-Ansatz: Babel, mehrere Polyfills, doppelte Layout-Pfade, doppeltes Testen. Ab einem gewissen Punkt ist eine Bibliothek, die diese Unterschiede wegabstrahiert, schlicht die billigere Lösung – und genau dafür ist jQuery auch in diesem Jahr noch legitim:

  • wenn IE-Versionen im Zielkorb sind, deren Eigenheiten jQuery seit Jahren zuverlässig glattzieht
  • wenn ein bestehendes Plugin-Ökosystem – Datepicker, Tabellen, Upload-Widgets – produktiv im Einsatz ist und ein Neubau keinen fachlichen Wert schafft
  • wenn ein Team eine große, gewachsene jQuery-Codebasis pflegt und ein Technologiewechsel mehr Risiko brächte als Nutzen

Auch die Polyfill-Rechnung muss ehrlich sein. Ein fetch-Polyfill ist klein; das Web-Components-Polyfill ist es nicht. Wer fünf Plattform-Features nachrüstet, hat am Ende womöglich mehr Kilobytes geladen als mit einer kompakten Bibliothek, die alles mitbringt – und trägt die Testlast für jede Kombination selbst. Die Baseline-Frage lautet eben nicht „Plattform um jeden Preis", sondern: Was läuft ohnehin überall, und was kostet der Rest wirklich?

Und schließlich: Für große, langlebige Anwendungen mit viel geteiltem Zustand löst die Plattform das Kernproblem nicht. HTML, CSS und ECMAScript sagen nichts darüber, wie Zustand und Darstellung konsistent bleiben, wenn dreißig Komponenten dieselben Daten zeigen. Dieses Problem ist real, und Frameworks adressieren es zu Recht. Nur ist es eben ein spezifisches Problem – nicht jede Website hat es, und eine Produktübersicht mit drei Ansichten hat es ziemlich sicher nicht.

Eine Materialsammlung als Nebenprodukt

Beim Sortieren dieses Themas ist bei mir im April eine öffentliche Materialsammlung entstanden: introduction-html5-css3-ecma auf GitHub. Dort liegt der Plattform-Bestand in Abschnitten beieinander – HTML5-Markup und die APIs von Geolocation über Canvas, Web Worker und WebSockets bis Server-Sent Events, dazu CSS3-Selektoren und -Animationen, die JavaScript-Grundlagen um Prototypen und Closures und die ES2015+-Syntax bis zu Promises, async/await und Generatoren. Nichts davon ist exotisch; der Wert liegt für mich im Nebeneinander. Erst wenn der Bestand einmal aufgeschrieben vor einem liegt, sieht man, wie viel davon im eigenen Projekt gerade doppelt vorhanden ist – einmal nativ im Browser und einmal als Abhängigkeit im Bundle.

Unterm Strich

Die Baseline aus HTML5, CSS3 und aktuellem ECMAScript ist 2017 kein Bastel-Kompromiss mehr, sondern ein tragfähiges Fundament: Grid und Flexbox für Layout, fetch und async/await für Daten, Custom Elements für Komponenten im Kleinen. Wer damit anfängt, senkt drei Kosten auf einmal – weniger Abhängigkeiten, die gepflegt und aktualisiert werden wollen, weniger Build-Komplexität, und eine niedrigere Einstiegshürde für jeden, der neu ins Projekt kommt und erst einmal nur Webstandards können muss.

Vor allem aber verändert die Baseline die Qualität der Framework-Entscheidung. Sie fällt nicht mehr am Projektanfang aus Gewohnheit, sondern später, gezielt, mit einem benennbaren Wofür. Manchmal lautet die Antwort dann trotzdem React oder Angular – und das ist völlig in Ordnung, weil jetzt klar ist, welches Problem damit gelöst wird und welches die Plattform längst selbst löst. Öfter, als ich noch vor zwei Jahren gedacht hätte, lautet die Antwort aber: Es braucht gar keins.

Weiterführende Quellen

Wie fandest du diesen Beitrag?

Kommentare