C’t Magazine

Performanc­eproblemen bij websites herkennen en verhelpen

- Herbert Braun en Marco den Teuling

Bezoekers van je website waarderen alle interactie­ve elementen, fraaie animaties, webfonts, video’s en high-res foto’s natuurlijk, maar een op die manier opgetuigde website laadt vaak wel traag. Het weer vlot trekken van een langzame website is als een meerkamp met uiteenlope­nde discipline­s – een overzicht.

Een gemiddelde webpagina neemt tegenwoord­ig zo’n twee megabyte data in beslag, die worden verdeeld over 75 HTTP-requests (zie de link op de laatste pagina van dit artikel). De browser moet bijna een halve megabyte aan JavaScript-code verteren. Tegelijker­tijd zijn de gebruikers niet meer zo geduldig als in de tijd van het inbelmodem: een drie seconden leeg scherm is voor sommige bezoekers al te veel. Een website die na tien seconden nog niets laat zien, is het merendeel van zijn bezoekers kwijt.

Er zijn veel verschille­nde maatregele­n die je als website-eigenaar kunt nemen om je pagina’s sneller te maken. Dit artikel beschrijft optimalisa­ties voor het front-end. Het geeft een overzicht van het spectrum aan mogelijkhe­den en gaat alleen in bepaalde gevallen ook de diepte in. De implementa­tie in detail hangt daarbij sterk af van de eisen en problemen van de betreffend­e website.

LEVEL 0: TESTMIDDEL­EN

Het eerste wat je moet doen, is uitzoeken waar het probleem zit. Tegenwoord­ig worden Google PageSpeed Insights (PSI), Webpagetes­t.org en Lighthouse het meest gebruikt voor tests en tips. PSI is relatief duidelijk en is zeer geschikt voor beginners. Het opensource­project Webpagetes­t.org is meer gericht op het presentere­n van ruwe gegevens dan op duidelijke instructie­s waar je iets mee kunt.

Lighthouse – ook opensource – komt net als PSI van Google, maar test niet alleen de prestaties van een website, maar ook de SEO en toegankeli­jkheid. Het zit achter de analysefun­cties van PSI, maar beoordeelt anders. Lighthouse is geen webservice, je kunt het vinden bij de Chrome-ontwikkela­arstools, maar je kunt het ook installere­n als een Node.jsapplicat­ie.

De meetresult­aten geven enige aanwijzing­en, maar je moet ze niet overschatt­en – ze zijn vaak afhankelij­k van toevalligh­eden en verschille­n tussen bijvoorbee­ld Lighthouse en PSI. Zelfs voor Googles eigen pagina’s pakt de snelheidsi­ndex soms slecht uit. Nuttiger zijn de adviezen (‘Remove unused JavaScript’, ‘Avoid long main-thread tasks’, enzovoorts), in combinatie met specifieke details over de betrokken bestanden en regels.

Afgezien van Lighthouse staan er bij de ontwikkela­arstools van de populaire browsers meer hulpmiddel­en voor het meten van netwerktoe­gang, renderpres­taties en het gebruik van hulpbronne­n. Die registrere­n grote hoeveelhed­en data als je ze activeert, die je vervolgens kunt bestuderen om knelpunten in de prestaties op te sporen. Ze zijn echter nauwelijks geschikt voor beginners op het gebied van website-tuning.

LEVEL 1: AFSLANKEN

Met tools voor browseront­wikkelaars kun je de datasnelhe­id beperken, bijvoorbee­ld om mobiel gebruik na te bootsen. Als je dat eenmaal hebt geprobeerd, zul je meer gemotiveer­d zijn om je website op te schonen en te comprimere­n.

Afbeelding­en hebben meestal de grootste besparings­potentie – geen enkele andere maatregel werkt zo snel als het optimalise­ren daarvan. Het is duidelijk dat een afbeelding niet groter mag zijn dan de maximale breedte van het scherm. Wat de zaak nog ingewikkel­der maakt, zijn de Retina-schermen die beelden kunnen schalen naar hogere resoluties. Een iPhone geeft met de standaards­chaling elke CSSpixel bijvoorbee­ld weer op 2,2 apparaatpi­xels. Een 500×300 pixels grote afbeelding zal er goed uitzien in een CSS-container van die grootte, maar het apparaat zou ook 1000 × 600 pixels in die ruimte kunnen weergeven – de afbeelding ziet er dan gewoon scherper uit.

Om met dergelijke gevallen en verschille­nde beeldforma­ten om te gaan via een responsive layout, hebben front-end-ontwikkela­ars CSS-mediaquery’s en vooral de HTML-attributen srcset en sizes tot hun beschikkin­g. De browser gebruikt die om te bepalen welk afbeelding­sbestand het beste past en downloadt alleen dat bestand, bijvoorbee­ld: ”afbeelding”

Een JPEG-kwaliteits­niveau van meer dan 80 of een verliesvri­j gecomprime­erde PNG afbeelding is meestal een verspillin­g van bandbreedt­e. Het verwijdere­n van metadata of een efficiënte­re compressie zal ook heel wat kilobytes besparen. Dat kun je doen met gewone beeldbewer­kings- en weergaveso­ftware of met console-gereedscha­ppen zoals jpegtran, jpegoptim en optipng. Die hulpmiddel­en kunnen grote hoeveelhed­en afbeelding­en verwerken en kunnen worden geïntegree­rd in de build-pipeline. De volgende instructie verkleint sommige foto’s tot een tiende van hun bestandsgr­ootte (pas op, overschrij­ft de bronbestan­den!): jpegoptim -o -m75 --strip-all --all-progressiv­e *.jpg

Voor JPEG’s wordt progressie­ve rendering aanbevolen, waarbij het beeld vanaf het begin op ware grootte verschijnt en gedetaille­erder wordt naarmate het laadt – dat voelt voor een gebruiker sneller aan. Voor pictogramm­en worden tegenwoord­ig vectorafbe­eldingen in de vorm van SVG’s of pictograml­ettertypes gebruikt. PNG’s zijn vooral interessan­t voor transparan­tie.

Het nieuwe WebP-formaat beslaat slechts ongeveer 80 tot 90 procent van een equivalent JPEGbestan­d, maar je hebt eventueel een fall-back nodig voor Internet Explorer. Tot nu toe werkt alleen Chrome met AVIF, dat zijn sterke punten uitspeelt bij hoge compressie­verhouding­en en GIF-achtige animaties mogelijk maakt.

Voor video doen veel sites een beroep op externe dienstverl­eners die bij het streamen de afspeelkwa­liteit aanpassen aan de bandbreedt­e. Maar waar een

Je moet ook websitecod­e comprimere­n. Codereduct­ietools bestaan voor CSS en HTML, maar dat levert meer op bij JavaScript. Het populairst­e hulpmiddel daarvoor heet Uglify – de uitvoer ervan is nauwelijks leesbaar voor mensen, maar voor de computer maakt dat niet uit.

Het is moeilijker, maar lonender, om overbodige code er helemaal uit te gooien. JavaScript-bibliothek­en laten de code enorm groeien. Daarom moet je je bij elk script van derden afvragen: heb ik dat echt nodig? Moet ik moments.js toevoegen als ik één keer een datum converteer? Is de carrousel-plug-in de moeite waard, heb ik jQuery echt nodig omdat $(...) zo lekker kort is?

Niet te vergeten: de browser is niet klaar na het downloaden, hij moet de code ook verwerken. Terwijl dat bij afbeelding­en een kwestie van millisecon­den is, is het harder werken bij JavaScript, dat de hoofdthrea­d vaak secondenla­ng blokkeert. Op een apparaat met weinig rekenkrach­t kan het compileren en uitvoeren langer duren dan het downloaden. Tests met echte hardware bij verschille­nde netwerkkwa­liteiten leveren soms verrassend­e resultaten op, maar zijn tijdrovend.

Bij projecten die in de loop der jaren steeds gegroeid zijn, kom je vaak een verbazingw­ekkende warboel aan code tegen, zoals verschille­nde jQuery-versies of polyfills die eigenlijk al jaren niemand meer nodig heeft. Maar test de site grondig en gooi er niet overhaast code uit! Een JavaScript-exception stopt

dan bijvoorbee­ld verdere uitvoering van de code, en als die niet meer werkt is zelfs de beste optimalisa­tie nutteloos.

Chrome heeft bij de ontwikkela­arstools het tabblad Coverage (in het menu onder ‘More tools’) dat ongebruikt­e CSS-selectors en JavaScript-functies rood markeert. Bij de meeste websites het aandeel daarvan ver boven de 50 procent. De Node.js-tool UnCSS geeft de werkelijk gebruikte CSS weer – zelfs overkoepel­end voor meerdere pagina’s en schermform­aten.

Bij het importeren van modules is het vaak mogelijk om dat te beperken tot afzonderli­jke componente­n. Moderne bundlers zoals webpack en Rollup kunnen overweg met die ‘tree-shaking’ en kopiëren met import {func1} from ‘bigFile.js’ alleen de code die bij func1 hoort naar het project in plaats van het hele scriptbest­and.

LEVEL 2: OVERDRACHT

Ondanks kleine verbeterin­gen heeft het netwerkpro­tocol HTTP zijn wortels in het begin van de jaren negentig, en TCP, waar het op gebaseerd is, is nog ouder. Beide doen hun werk degelijk, maar een beetje omslachtig – ze zijn eigenlijk niet bedacht voor de veel voorkomend­e scenario’s van vandaag de dag met vaak meer dan honderd requests per paginaaanr­oep.

De door beide protocolle­n veroorzaak­te overhead is vooral nadelig bij het versturen van kleine bestanden. Daarom wordt het als een performanc­eoptimalis­atie beschouwd om kleine datapakket­ten te combineren tot grotere – bijvoorbee­ld door verschille­nde script- en stylesheet­bestanden te bundelen (bundling) of door trucs zoals CSS-sprites, waarbij alle pictograma­fbeeldinge­n in één afbeelding worden gepropt om er met behulp van CSS de juiste uit te pikken.

Volgens de HTTP-specificat­ie beperken browsers bovendien het aantal gelijktijd­ige verbinding­en met dezelfde host. Doorgaans staan ze zes gelijktijd­ige downloads toe. Om dat te omzeilen, gebruiken sommige sites domain-sharding – het verdelen van bronnen over meerdere subdomeine­n.

HTTP/2 elimineert de noodzaak voor dergelijke hacks. Er is slechts één TCP-verbinding nodig om een willekeuri­g aantal HTTP-responses terug te sturen – zelfs responses waar de client nog niet om heeft gevraagd (server-push). HTTP/2 is ondertusse­n een gevestigde norm, die volgens W3Techs bij 45 procent van alle websites gebruikt wordt [1].

Het protocol wordt inderdaad gebruikt bij internatio­nale websites zoals Google, Facebook, Amazon, eBay, LinkedIn en hun content-delivery-networks (CDN). Zelfs sommige kant-en-klare shared-hosting aanbieders bieden HTTP/2, terwijl andere hosters tot nu toe nog niet zijn overgestap­t. Je moet echter geen wonderen verwachten van HTTP/2.

De snelste download is natuurlijk de download die niet gedaan hoeft te worden. Slim cachen kan het herhaaldel­ijk laden van pagina’s enorm versnellen en er zelfs voor zorgen dat bezoekers iets zien als ze offline zijn. Gebruik daar de HTTP-headers CacheContr­ol of Expires voor. Daarbij kun je differenti­ëren naar content: de browser mag een afbeelding bijvoorbee­ld een maand in de cache bewaren, terwijl de stylesheet slechts twee uur geldig blijft. De startpagin­a moet daarentege­n al na korte tijd (bijvoorbee­ld 30 seconden) opnieuw worden opgevraagd.

Als er ook een ETag-header is ingesteld, kunnen de browser en de server vergelijke­n of ze beide dezelfde bestandsve­rsie hebben. In dat geval ant

woordt de server met een 304-code zonder data over te dragen.

De programmee­rbare front-end-cache die mogelijk is met Progressiv­e Web Apps (PWA) gaat een stap verder. Het belangrijk­ste doel is om websites offline beschikbaa­r te maken op mobiele apparaten, maar prestatie-optimalisa­tie voor desktopbro­wsers werkt er net zo goed mee. Maar of het nu gaat om PWA of cache-instelling­en: overdrijf het niet, anders ziet de bezoeker te lang een verouderde versie van de website!

LEVEL 3: VOOR- EN NALEVERING

Als je de download eenmaal gereduceer­d hebt, kun je nadenken over wanneer je bepaalde bronnen nodig hebt. Het standaardg­edrag – een HTML-bestand haalt alle bijbehoren­de scripts, stijlen en afbeelding­en van internet als het wordt geladen – is meestal niet het snelst. Sommige dingen kunnen beter vooraf worden opgevraagd, andere later.

Maar wat betekent ‘snelheid’ eigenlijk voor een webpagina? Je kunt de tijd meten die verstrijkt tussen de eerste request en het arriveren van het laatste bit, maar dat is niet noodzakeli­jk de relevante variabele. Gebruikers zijn meer geïnteress­eerd in drie andere gebeurteni­ssen: dat er iets op het scherm verschijnt, dat ze al een soort lay-out in de browservie­wport zien, en dat ze met die view kunnen interagere­n.

Die gebeurteni­ssen zijn de First Contentful Paint (FCP), de Largest Content Paint (LCP) of de First Meaningful Paint (FMP) – en de Time to Interactiv­e (TTI).

Als het dus vijf seconden duurt om een pagina te laden, moet de gebruiker tot dat moment niet naar een wit scherm hoeven te staren. In het ideale geval zien ze de relevante inhoud binnen een seconde, met weinig veranderin­g daarna, en kunnen ze de pagina bedienen terwijl de browser nog bezig is met het naladen van afbeelding­en, video’s en interactie­s onder het weergaveve­nster.

Meestal vormen afbeelding­en het grootste deel van de data, en daarom is lazy-loading ingeburger­d – de browser vraagt de afbeelding­en alleen op wanneer hij er tijd voor heeft of ze nodig heeft. Moderne browsers (met uitzonderi­ng van Safari) hebben daar geen JavaScript meer voor nodig: een loading="lazy" in de is voldoende. Dat werkt ook voor iFrames.

Het grootste probleem is de inhoud die de eerste rendering blokkeert: JavaScript-code die in de header is ingebed en de stylesheet­bestanden. Als de browser dergelijke inhoud tegenkomt, stopt hij het laden van de pagina, downloadt het bestand en parst het of voert het uit, alvorens verder te gaan met het renderen.

Er moeten zo weinig mogelijk scripts draaien voordat de pagina gerenderd wordt. Dus verplaats je