High Performance Web Apps

von Stefan Hillebrand
13.04.2018

Viele von euch kennen noch die klassischen goldenen Web Performance Rules von Steve Souders unter dem Titel "High Performance Web Sites" aus dem Jahre 2008. Die darin enthaltenen Performance Guides prägten Browser Add-Ons wie PageSpeed und YSlow bis heute und haben immer noch ihre Relevanz. Doch mittlerweile sind wir bei Smartphones angekommen die im Durchschnitt keine High-End Rechner sind und mit einer oftmals schwachen Konnektivität auf unsere Web Sites zugreifen. Diese Web Sites sind inzwischen zahlreich zu Single Page Apps mutiert und verlangen nach einem Performance Update. Nennen wir das Update also High Performance Web Apps.

High Performance Web Apps zeichnen sich dadurch aus, das sie fast by default sind, indem Key Frames wie 

  • First Meaningful Paint 
  • Time to interactive  
  • DOMContentLoad 

und andere, so früh wie möglich erreicht werden. Nicht optimierte mittelgroße Apps brauchen für eine Time to Interactive mitunter 35 Sekunden bis hinauf zu einer Minute, was natürlich einer Katastrophe für jede Conversion und User Experience gleichkommt. Wie können wir das also vermeiden - hier ein kleiner Guide wie ihr eure Apps als fast by default auszeichnen könnt: 

Befolgt das PRPL(„Purple“) Pattern

PRPL ist noch ein recht frisches Pattern am WPO (Web Performance Optimization) Himmel und besagt:

P (Push & Preload)

Vermeidet Network Roundtrips. Macht einen initialen Request der das HTML als (Initial-)Dokument zurückgibt. Sendet danach alle für die aktuelle Seite oder View benötigten Ressourcen wie Bilder, JS und CSS via Server Push (HTTP2) zum Client, statt alle - im schlimmsten Fall monolithisch gebauten - Ressourcen in einem neuen Call anzufordern.  

Dieses Verfahren ist zugegebenermaßen nicht ganz neu. Facebook sprach im HTTP1 Zeitalter von einer Big Pipe

Nutzt außerdem die Vorteile vom Pre-Loading. Das Preload Attribut für kritische Ressourcen oder das Prefetch Attribut für statische Assets. In Kombination mit Route Based Code Splitting können nachfolgende Komponenten einer View vorgeladen werden, was einen signifikanten Performance Boost ermöglicht. Preloading/Prefetching ist außerdem nützlich für Multimedia Content, da gerade das Laden von Videos viel Zeit verschlingen kann.

<code><link rel="preload" href="some/fancy/video.mp4" as="video" type="video/mp4"> <link rel="preload" as="fetch" href="example.com/next/site"> <link rel="prefetch" as="style" href="some/nice/stylesheet.css"></code>

„Verfügt eine Web-App über viel externen Content oder Services ist es sinnvoll deren Adressen via dns-prefetch im Voraus über link tags im html head aufzulösen. “

Der Vorteil besteht darin, dass Komponenten und Assets vom Browser vorgeladen und im Cache abgelegt werden ohne das onload Event des Browsers zu blockieren. Werden sie dann durch einen konkreten Call angefordert, liegen sie bereits lokal am Client vor und können deutlich schneller ausgespielt werden. 

Ebenfalls effektiv ist das Verwenden von dns-prefetch: 

<code><link rel="dns-prefetch" href="//external.api.com"></code><code><link rel="dns-prefetch" href="//external.font-service.com"></code><code><link rel="dns-prefetch" href="//external.tracking-service.com"></code>

R (Render)

Beim Rendern geht es darum, dem User so schnell wie möglich etwas von Bedeutung ausliefern zu können. Man spricht auch vom first meaningful paint. Also einem Rendering-Ergebnis im Browser, der die ersten Content-Elemente bereits enthält (Stichwort: above the fold), wodurch der Benutzer diese Inhalte schon betrachten kann.

 
Ein frühzeitiger first meaningful paint wird schon durch den Einsatz von requestIdleCallback (leider noch nicht in Edge und WebKit verfügbar) erreicht. Diese „kleine“ nützliche Helper-Funktion ermöglicht es dem Browser mitzuteilen, was er erledigen soll, wenn der Benutzer inaktiv ist oder der Client selbst am Ende eines Frames angekommen ist. Eben wie es der Name schon sagt über eine Callback-Funktion. Auf diese Weise können über Fetch-Requests Ressourcen wie bspw. Bilder und Videos vorgeladen werden ohne die aktuelle Performance zu beeinträchtigen, da sie eben nur stattfinden wenn eure App untätig - also idle - ist. Man spricht auch vom zeitlich verschobenen Laden von Ressourcen (=> defer loading of resources). Also schnell zu Microsoft und WebKit auf die Projekt Status Page und Druck machen, dass das Feature auch hier eingebaut wird. Denn im Prinzip können damit auch ganze Komponenten planmäßig vorgeladen werden.

Vermeidet außerdem das häufige Re-Rendering von Child-Komponenten, wenn sich eine Parent-Komponente geändert hat. Oftmals ist das nämlich nicht nötig, da der neue State nicht für alle Komponenten von Relevanz ist. Nutzt dazu z. B. Funktionen wie Reacts shouldComponentUpdate, die von Frameworks mitgeliefert werden.

Ein Klassiker beim Erreichen eines frühen first meaningful paint darf nicht fehlen: Liefert die Bilder in den richtigen Dimensionen aus. Sind Bilder im Original größer als der Container auf der Seite, indem es dargestellt wird (was ja keine Seltenheit ist im Zeitalter von Responsive Webdesign), kostet es dem Browser unnötige Rechenzeit und verzögert das Rendering.   

„Das Beste daran ist, dass ihr mit diesen Tweaks nicht nur eure Performance optimiert, sondern auch gleich das Suchmaschinenranking verbessert.“

Aber letztendlich gilt das ja inzwischen für die Site Performance als Ganzes. Schlechte Performance geht auch immer mit schwachem SEO-Ranking einher. Auch wenn der Content und das Cross-Linking noch so super ist. 

Ein sehr gutes Rendering Ergebnis erzielt man auch mit Static Websites oder mit Server Side Rendering. Es gibt auf dem Markt bereits einige Tools wie Gatsby.js oder Next.js mit deren Hilfe statische Websites generiert werden können. In diesem Verfahren werden die Views und deren State vorab generiert, ohne das ein Compiler diese am Client erstellen muss. Dynamische Inhalte wie Model-Bindings werden ebenfalls aufgelöst. Es handelt sich also gewissermaßen um statische Webseiten der ersten Generation welche vom Client zügiger dargestellt werden können. Genau darum ist dieser Ansatz nicht in jedem Fall realisierbar, da nicht wenige Apps zur Laufzeit auf dynamische Updates (gerne auch mal in Echtzeit) angewiesen sind. Prüft eure Anforderungen dahingehend. Auch das seit einiger Zeit hochgelobte Server Side Rendering kann einen signifikanten Performance-Gewinn mitbringen. Schließlich wurden die Templates und die Zugehörigen Daten in diesem Fall schon auf dem Server generiert und die komplexen DOM und Binding Operationen am Client entfallen, wodurch die Seite schneller gerendert werden kann. Das ist jedoch nicht der Fall, wenn die Operationen zur Datengewinnung zu aufwändig werden oder die vermehrten Network-Roundtrips die Performance belasten.

P (Precache)

Hier können wir gleich beim schon erwähnten Vermeiden von Network-Requests anschließen, auch wenn eine Auslieferung aus dem Cache stattfinden kann. Umgeht das in diesem Falle unnötige „Anklopfen“ und bezieht die Ressourcen direkt über den Service Worker. Der Service Worker (häufig auch einfach nur noch SW genannt) platziert sich als Proxy zwischen Website und Server. Transferiert Assets in den SW und fragt diese dann direkt von diesem an, was wiederum weniger Network-Roundtrips bedeuten. Natürlich erfordert diese Variante des Asset-Managements eine Strategie des Ausrollens von Updates in den Assets. Aber das war ja auch schon beim Expires-Header kein Hindernis. 

Mit dem Einsatz des Service Worker ist eure App damit auch gleich dafür gewappnet, wenn der Benutzer einmal offline ist, denn schließlich liegt der SW am Client und es müssen keine Network Requests für das Anfragen der Assets abgefeuert werden. Also neben fast by default auch noch offline first erreicht! 

Service Worker werden inzwischen auch vom WebKit-Universum unterstützt. Dieser fehlte noch. Anders als bei requestIdleCallback sieht der Browsersupport hier also wieder glänzend aus.

L (Lazy-Load)

Lazy-Loading gehört schon seit einiger Zeit zu den Best Practices. Denken wir doch nur mal an eine PRV (Produkt-Listing) in einem Shop oder an einen Image-Slider. Lange Zeit war es üblich, die Bilder einfach bei onload alle schon da zu haben. Doch dann wurden die Bilder on-demand angezeigt, wenn der Benutzer den entsprechenden Bereich erreicht hatte.

 
Dieses Prinzip des Lazy-Loadings können und sollten wir auch auf Komponenten anwenden. Was bringt es, eine Success-Page oder ein Kontaktformular inklusive aller Assets schon beim Laden der Homepage im Bundle zu haben? Verwendet Route Based Code Splitting in dem SPA-Framework eures Vertrauens und ladet Views erst dann wenn sie auch benötigt werden.

„Verwendet Route Based Code Splitting in dem SPA Framework eures Vertrauens und ladet Views erst dann wenn sie auch benötigt werden.“

Nutzt weiterhin beim Bauen der Scripts mit Webpack das CommonsChunkPlugin (Achtung: Breaking Changes in Webpack 4 mit dem neuen SplitChunksPlugin) um verschiedene Packages für unterschiedliche Teile eurer Seite zu erstellen. Empfehlenswert im Einsatz mit dem CommonsChunkPlugin ist noch der Webpack Bundle Analyzer um z. B. redundanten Code zu identifizieren. Schließlich sind separierte Bundles, in Kombination mit einem gut abgestimmten Preloading dieser, ein lohnenswertes Ziel. Doppelter Code darin jedoch nicht.

Noch einmal kurz zurück zum Lazy Loading von Content wie Images auf einer View. Mittlerweile können wir dazu den von Browsern integrierten IntersectionObserver nutzen und müssen nur noch im Fallback („natürlich“ noch nicht in WebKit verfügbar) auf alte selbstgebaute Lösungen wie Scroll-Detections zurückgreifen. 

Performance Budget und Tools

Setzt euch im Projekt ein Performance-Budget. Es ist ratsam zu definieren, dass jede Seite in vier bis maximal fünf Sekunden fertig geladen sein sollte. Mitunter wird schon zu 3 Sekunden geraten. Zuviel Benutzer werden sonst ungeduldig und verlassen die Seite. Fertig meint Time to Interactive. Also ein Zustand indem ein Benutzer mit einer App interagieren kann und sei es zunächst einmal nur mit kritischen Inhalten im Viewport.

Folgende Tools helfen euch beim Handling mit einem Performance Budget:

Und natürlich die üblichen Addons und Performance Metriken in den DevTools eures Browsers. 

Kalkuliert beim Performance-Budget mit der Größe eures Frameworks. Bedenkt, dass bis zu drei Sekunden verstreichen können, um eure App zu initialisieren. Unter anderem deshalb ist Ahead of Time Compiling für Angular-Apps unter Live Bedingungen zur Pflicht geworden. Aufgrund des Gaps zwischen Framework und eigenem Code empfiehlt es sich, auf Code Splitting zu setzen und möglichst wenig Code initial auszuliefern.

Testen mit realistischen Geräten unter echten Bedingungen

Testet eure Web-Apps unter echten Bedingungen, mit real existierenden Devices. Also legt einmal das neueste iDevice oder Android beiseite und drosselt auch mal die Konnektivität auf „Slow 3G“. Denn fast by default meint schnell auf Geräten die eure Benutzer verwenden. Fühlt quasi die Einschränkungen, die die User erleben. 

Ein gutes Hilfsmittel dazu ist die API von webpagetest. Hier könnt ihr z. B. einfach eine Konnektivität für einen Performance-Test mitgeben und müsst nicht ständig zum Device Lab laufen. 

Tools und Frameworks

Benutzt die PWA (Progressive Web App) -Bootstrapper (Generatoren) der bekannten Frameworks, um die hier vorgestellten Best Practices per default zu erfüllen. Diese wären aktuell:

  • create-react-app 
  • vue init
  • preact create

Die CLIs kommen u. a. mit Service Workern, Code Splitting und Support für Dynamic Imports out of the box. 

Holy Grail

Der Heilige Gral einer Site-Performance liegt darin, dass in einem kompletten Call einer Seite oder in der gesamten Codebase einer Applikation, das Framework inkl. Abhängigkeiten weniger ausmachen als der eigentliche Applikationscode einer Web App. Zugegebenermaßen ist das aktuell nicht mit jedem Technologie-Stack zu realisieren. Doch es gibt auch Preact. Preact ist die kleine, schnelle Alternative zu React (das ja im Hinblick auf Performance schon einiges richtig macht - siehe z. B. VirtualDOM) und bietet ein CLI zum Bootstrappen von Anwendungen das die hier beschriebenen Best Practices bereits mitbringt und eben jenen Holy Grail Grundsatz, dass das Framework nicht so groß sein soll, wie der eigene Code aktuell am Besten erfüllen kann.

Good to know

Über die neue Network Information API könnt ihr die Konnektivität eurer Benutzer prüfen und in der Anwendung gracefully darauf reagieren, indem ihr z. B. das HD-Video gegen ein SD-Video austauscht. 

Das könnt ihr einmal via 

<code>navigator.connection</code>

in eurem Browser testen. Allerdings ist der Support hier leider noch sehr dürftig. Zuverlässig läuft das aktuell nur in Chrome. Doch mit genügend Einsatz von allen Seiten kommt da sicherlich bald mehr. 

THANKS TO

Addy Osmani and Steve Souders 

Picture: The Conmunity/ Doug Kline (CC BY 2.0)