blog

Front-end optimization 3.0, een case study

In de zomer van 2010 lanceerden we vol trots sport1.nl. Uiteraard opgeleverd volgens de toen geldende best practices in de front-end wereld.

Op basis van jarenlange ervaring en met de boeken van performance guru Steve Souders ("High Perfomance Web Sites" en "Even Faster Web Sites") in de hand hebben we een ontzettend snelle website opgezet. Het gros van de onderwerpen in dat boek hanteerden we sowieso al intern en zijn dus ook doorgevoerd op de Sport1 site. Denk hierbij aan;

  • Compress je HTTP
  • Maak "minder" HTTP requests
  • Voeg Expires Headers toe
  • Gzip je HTML / JS / CSS
  • Zet je CSS in een apart bestand en in de head
  • Zet je JavaScript in een apart bestand en in de footer
  • Zet je CSS en JavaScript in externe bestanden
  • Minify je JavaScript
  • Voorkom Redirects
  • Remove ETags

Nu zijn we een krappe 2 jaar verder en de site is perfomance technisch gezien toe aan wat opfrissing. We wilden graag het dataverkeer (= kosten) omlaag brengen en de site ook tijdens de echte piek momenten (denk aan El Clásico) vlot & soepel laten reageren.

Waar de bovenstaande technieken ruimschoots voldoen voor iedere normale website is het voor een high-traffic site als Sport1 zeker interessant om de laatste bytes er af te snoepen. 

Jory en ik hebben een week kunnen besteden aan het doorvoeren van diverse front- en back-end optimalisaties. Hieronder een verslag van mijn front-end verbeteringen.

Analyse startpunt

Om het effect te meten heb ik eerst een analyse gedaan van de huidige status. Ik heb dit gemeten door WebPageTest.org 10 runs te laten doen met de 2 meest gangbare browsers (IE9 & IE8). Hoewel deze 2 browsers niet bekend staan als de meest snelle, geeft dit wel een reëel beeld van hoe een bezoeker Sport1 ervaart.

Hier komt uit dat op de homepage 52 requests gedaan worden die tezamen goed zijn voor 1,1 MB. Deze aantallen willen we dus drastisch verlagen.

Afbeelding: resultaten met IE8 - 20 Mbit

 

Afbeelding: resultaten met IE9 - 20 Mbit

De site laadt momenteel in zijn geheel in minder dan 2 seconden, wat naar mijn mening al best snel is. Kanttekening hierbij is wel dat ik heb gekozen voor een behoorlijk snelle internet verbinding (gevorderde desktop gebruiker) en daarbij staat zowel de site van Sport1 als de WebPageTest test server in Amsterdam. De te overbruggen afstand is dus minimaal, wat de response tijden uiteraard ten goede komt. De tijd (en dus ook de uiteindelijk te behalen winst!) om de homepage in te laden zal dus, afhankelijk van je internet verbinding, provider, woonplaats & tijdstip variëren.

Google Webmaster Tools komt met een gemiddelde van 1.7 seconden per pagina, zie onderstaande afbeelding.

Ik heb de site uiteraard ook getest met Google PageSpeed en Yslow. Hier kwam respectievelijk een score van 84/100 en een score van 90/100 uit. Ruimte voor verbetering dus!

CDN / Cookie-free domains

Omdat browsers maar een maximaal aantal gelijktijdige connecties over dezelfde domeinnaam toe staat is het een goed gebruik om statische content, zoals plaatjes, van een ander domein (en evt. server) in te laden. In plaats van 2-6 gelijktijdige downloads verhogen we dit naar 6-18 gelijktijdige downloads. Vooralsnog maken we gebruik van 2 extra domeinnamen, omdat ieder nieuw domeinnaam ook weer een extra DNS lookup betekent. Hier is het dus zoeken naar een gezonde balans tussen die twee.

Aangezien we maximaal gebruik willen maken van de browser cache is het belangrijk dat we altijd hetzelfde domein voor een bepaalde image serveren, anders zou je in het geval van 2 CDN servers gemiddels genomen maar 100 / 2 = 50% van de tijd gebruik maken van de caching mogelijkheden. Gebruik je meer CDN domeinnamen dan neemt dit aantal snel af, naar bijvoorbeeld 25% in het geval van 4 domeinen.

Ik forceer het gebruik van hetzelfde subdomein door de crc32 hash van een afbeeldingspad te nemen en met bitwise AND te kijken of deze true of false is. Dit kan je dan om zetten naar een string waarde. Hoewel je dit kan bestempelen als een 'hack' blijkt na benchmarken dat dit sneller is dan de modulo waarde van deze hash te gebruiken.

Bijkomend voordeel van een nieuwe domeinnamen is dat we die domeinen cookie-loos kunnen houden. Door te forceren dat de Google Analytics cookies alleen maar voor www.sport1.nl wegschrijft, blijven alle andere domeinnamen 'schoon'. Hierdoor zijn de HTTP headers van de requests die over deze domeinen gaan kleiner, wat weer een hogere score op zal leveren.

De CDN domeinnamen zijn protocol relatief opgezet. In plaats van http://img1.sport1.nl begint een url met //img1.sport1.nl. Dit heeft als voordeel dat de browser zelf kiest of deze de content wel of niet over SSL moet serveren en daarnaast scheelt het ook weer wat karakters in je HTML.

Nog minder HTTP requests 

Hoewel we bij de ontwikkeling al zoveel mogelijk hebben bespaard op de HTTP requests (door bijv. de CSS / JS te combinen) blijft 50+ requests erg veel. Een aanzienlijk deel van de content van Sport1 bestaat uit images, dus dat verklaart het hoge aantal. Echter, er zijn hoop sliders gebruikt, en dat betekent dat deze afbeeldingen initieel helemaal niet  zichtbaar zijn.

De site is nu zo aangepast dat deze plaatjes pas ingeladen worden zodra de boeker met zijn muis over een bepaalde slider heen gaat en we er dus van uit mogen gaan dat de bezoeker de intentie heeft om te scrollen.

Dit leverde op de homepage 19 HTTP requests minder op. Op de startpagina van een specifieke sport leverde dit 14 requests minder op. Nu gaat het hier om hele kleine thumbs, maar desalniettemin kost iedere HTTP request minstens 200ms (gemiddeld genomen, volgens Steve Souders). Vier requests betekent dan niet per definitie 800ms, omdat een browser meerdere requests tegelijk doet.

Cufon -> @font-face

Twee jaar geleden werd @font-face nog niet breed genoeg ondersteund om te gebruiken in een productie site. Cufon (en aanverwanten) was zo'n beetje de defacto standaard. Cufon kan heel mooi geantialiast renderen, maar heeft wel een aantal forse nadelen; het neemt in ons geval erg veel JS in beslag (184 KB), doet bovendien veel DOM manipulatie en er is een duidelijk zichtbare FOUC.

Niet alleen het aantal requests bepaalt uiteindelijk de laadtijd van je pagina, maar ook de tijd die een pagina nodig heeft om te renderen (opbouwen). Aangezien een browser niet gelijktijdig kan renderen en JS interpreteren werkt de 184 KB aan JavaScript die Cufon nodig heeft zeker als een vertragende factor. 

Aangezien er tegenwoordig veel betere ondersteuning is voor @font-face nemen we dat extra HTTP request graag voor lief. Je tekst is weer selecteerbaar, vertaalbaar en je hebt geen onnodige HTML elementen meer. 

Splitting payload / betere sprites

Uiteraard maakten we al gebruik van sprites, maar aan een site als Sport1 wordt gaandeweg veel aangepast. Extra elementen op de site, uitbreidingen, logo's die er bij of af moeten etc. Dit resulteerde in "vervuilde" sprites met ongebruikte afbeeldingen en grote gaten. Daarnaast werden er ook meer afbeeldingen ingeladen dan nodig op de homepage.

Door het "splitten van je payload" breek je je CSS, JS of sprites op in stukken waardoor de initieele download weer minder wordt. Je downloadt alleen wat je nodig hebt. Ik ben er vanuit gegaan dat een groot gedeelte van de bezoekers zijn 'sessie' start op de homepage. Om te zorgen dat deze nog sneller werd heb ik één hoofd sprite gemaakt, waar precies alle afbeeldingen in staan die nodig zijn om de homepage goed weer te geven. Andere gedeeltes van de site krijgen hun eigen sprite, waardoor er geen overbodige afbeeldingen in je hoofdsprite zitten.

Binnen E-sites draaien we al enige tijd een pilot met Glue. Glue is, kortgezegd, een tool die automatisch van een map met plaatjes 1 grote sprite maakt en daarbij zelf de beste positie voor een plaatje kiest. Ik heb alle oude sprites nagelopen en hier nieuwe van gemaakt.

Na het genereren van die (32-bits) sprite kijk ik met ImageAlpha of ik er -zonder zichtbaar kwaliteitsverlies- een 8-bits sprite met maximaal 256 kleuren van kan maken. Dit scheelt in mijn geval de helft (18KB). Vervolgens haal ik alle bestanden door ImageOptim. Dit is een tool die vrij agressief je JPG's & PNG's lossless crusht.

Uiteindelijk leverde dit voor de homepage 4 HTTP requests en ~36 KB besparing op. Over de hele site gezien levert dit, zeker qua HTTP requests nog meer op.

Upgraden Minify

Voor het samenvoegen en minify'en van alle CSS & JS bestanden gebruiken we momenteel Minify. Hoewel we achter de schermen bezig zijn om deze belangrijke stap te integeren in ons build proces gebeurt dit momenteel nog op de server door deze library.

De voorlaatste stabiele versie is 2.1.3, inmiddels 2 jaar oud, maar recentelijk is v2.1.5 uitgekomen. Tijd voor een upgrade dus!

Voor de JavaScript maken we gebruik van de Google Closure Compiler API, met als fallback het normale compressen op de server. Dit resulteert in ontzettend snelle responsetijd  (< 2 sec.) van je geminify'de bestanden. Dit, terwijl JS minification in het algemeen en GCC in het bijzonder erom bekend staat dat het een ontzettend intensief proces is. Tegen Google's enorme serverfarm kunnen wij uiteraard niet op. Mocht de API om wat voor reden down zijn of traag reageren dan gaat Minify zelf aan de slag.

De geminify'de CSS & JS slaan we op in Memcache, in plaats van het standaard file-based cachen wat Minify doet. Hiermee reduceren we het lezen / schrijven naar de harddisk en hebben we een bijna 'onwerkelijk' snelle responsetijd van ~15ms.

Compressen images

WebPageTest geeft een "grade F" op het compressen van de images. Dit is veruit de laagste score de we behalen op alle fronten. Tijd voor optimalisatie zou je zeggen, echter kan je bij de opgegeven manier van meten / waarderen vraagtekens zetten.

Alle JPG's die ruwweg een quality van meer dan 60% hebben scoren slecht, en juist dan gaat het hard. Per geschatte KB die je hier op verliest gaat de score hard achteruit. 

Nu is het zo dat de thumbnails van de afbeeldingen bewust een hoge kwaliteit hebben (van 95%) omdat ze een hoop mooie foto's gebruiken en dit duidelijk onderdeel is van het imago wat Sport 1 uit straalt. Voorlopig willen we hier dus bewust niets mee doen. Wellicht dat in een later stadium de gegenereerde thumbnails een iets lagere kwaliteit (~88%) krijgen en/of deze nog eens door compressie tools gehaald kunnen worden, maar testen wezen uit dat het voor nu niet echt efficient is om dit aan te passen.

PNG's groter dan 8-bit scoren slecht. Dit kan je prima oplossen door de sprites op te slaan met ImageAlpha en dat is uiteraard ook gedaan.

Resultaten

Toon resultaten in tabel

 

Ik ben erg tevreden over de resultaten. Hoewel de besparing in milliseconden op het oog niet heel veel lijkt geeft dit enigszins een vertekend beeld omdat ik van de meest ideale situatie uit ben gegaan (Amsterdam -> Amsterdam over een 20 Mbit lijn). Ik heb de lat bewust hoog gelegd. De gemiddelde bezoeker woont waarschijnlijk verder weg en heeft misschien een minder goede internetverbinding. 

Al met al wordt er een hoop minder ingeladen, zodat er een stuk meer threads beschikbaar zijn om de volgende bezoeker te bedienen. Pure winst dus!