Først lige en væsentlig note for læsningen. Indlægget har ikke til hensigt at tale dårligt om nogle af de nævnte plugins, og ingen af de nævnte plugins er i sig selv dårlige, eller gør din WooCommerce webshop (nævneligt) langsommere. Der er tale om interaktionen mellem dem, og det faktum at webshoppen jeg anvender som eksempel, har over 70.000 produkter i kataloget.
Jeg har med vilje anonymiseret virksomheden og kunden, samt ting der kunne lede til at identificere websitet for ikke at risikere at udstille nogen unødigt.
For ganske nylig var der performance problemer på en webshop, som vi blev bedt om at kigge på. Serveren crashede flere gange om dagen, og løb simpelthen tør for ressourcer. Det udviklede sig til en ganske interessant case, som du måske kan anvende elementer fra.
Det følgende er ret detaljeret, og lidt mere nørdet 🤓 end hvad jeg ellers skriver om, men jeg synes det fortjente ord på digitalt papir, fordi det forhåbentlig kan hjælpe dig, hvis du selv slås med performance på en WooCommerce webshop.
Først af alt hosting
Før nogen optimering er relevant at påbegynde, er det første vigtige punkt, at se på hvilken hosting der anvendes. Hvis hostingen ikke er tilstrækkelig, så er det lige meget at optimere, da du ikke kan stoppe flere folk ind i restauranten, end der er fysisk plads til.
Når det er skrevet skal det samtidig siges at en god løsning sjældent er, at sætte raketmotorer på siden af din VW Golf med strips og gaffa. Nogle gange får du bare den bedste optimering ved at bede mekanikeren gennemgå din bil fra top til tå. Så finder han lige den dims der trænger til at blive skiftet ud, for at hele maskineriet igen køre brandgodt og stabilt. Raketmotorer sparet 🤑🤩
Hvis du kører en webshop med hundredetusinder af besøgende, med Google Ads der driver trafik til sitet, og du har adskillige titusinder af produkter, – ja, så skal både løsningen og hostingen være dimensioneret til at kunne håndtere det.
I dette tilfælde vidste vi at hostingen ikke var problemet, da webshoppen kører på enterprise-grade skalérbare servere, samme type som vi anvender til en anden WooCommerce webshop, der modtager en ordrer mellem hvert til hver tredje sekund.
Hvis en server som dette maxer ud og crasher, så er det med 99,99% sandsynlighed ikke hostingen der er problemet, men dét der køres på den.
Da vi ikke selv har bygget løsningen, men primært hoster og drifter den, var det på tide at kigge nærmere på hvad der foregik på serveren.
Overblik over kode, plugins og tema
Det første arbejde i at fejlfinde på en løsning for at optimere, – særligt på en løsning du ikke har bygget, er at se på hvordan det hele er stykket sammen.
Tema: Flatsome
Der anvendes temaet Flatsome, der er egentlig et højt rost tema, på grund af sin performance og brugervenlighed. Der er var intet child-theme installeret, så det betød, at der heller ingen ekstra kode (ja, jeg tjekkede 😉) der kørte via f.eks. functions.php.
Det betød at mistanken ikke umiddelbart faldt på temaet, til at starte med.
Plugins
Ved kontrol ser vi, at der kører intet mindre end 74 plugins på webshoppen. Før du hoster (morgen)kaffen op fordi den røg i den gale hals, så vil jeg indskyde, at 74 plugins i sig selv ikke er et problem.
Det er en almindelig misforståelse, at det er antallet af plugins, der udgør problemet på WooCommerce / WordPress installation. Jeg vil understrege: Det er det ikke.
Det er hvad de 74 plugins gør og hvordan de gør det, der er interessant. Jeg har set top-tunede WooCommerce installationer med over 90 plugins, der fløj derud af som Space-X raketter, fordi:
- Hvert plugin havde et formål.
- Hvert plugin var valgt og/eller udviklet med omhu.
- Hvert plugin var tunet til at performe.
Når du ser 74 plugins, skal det ikke give dig koldsved. Men det skal få dig til at stille spørgsmål, og kigge på hvert enkelt af dem, og deres funktionalitet.
Oprydning i plugins
Det første skridt var således givet: Kig på samtlige 74 plugins og kortlæg hvad de gør, hvad de anvendes til.
Og her jeg rendte på de første interessante fund. Der var på vores case shop installeret:
- 2 Facebook / Meta feed plugins.
- 3 Plugins til at eksportere / importere ordrer.
- 2 Plugins til at eksportere / importere produkter.
- Et Google Shopping feed plugin.
- Et MailChimp feed plugin.
- Et Klaviyo feed plugin.
- Et ProfitMetrics feed plugin.
- Et Clerk.io feed plugin.
- Og et plugin som stod for søgning og indeksering af produkter.
Det vi ser ovenfor, er ret mange plugins, der ved først øjekast potentielt gør det samme. Da jeg spurgte ind til historikken, viste det sig at flere personer havde været inde over og hjælpe med løsningen, igennem årene. Hver person havde installeret sin egen værktøjskasse og yndlingsplugins, uden at se på hvad der i forvejen var installeret på platformen. En håndfuld år senere har vi så dette sammensurium af installationer, som efterlader løsningen lidt som en lejebil, der har haft mange forskellige brugere, hvoraf ingen har fjernet deres gamle takeaway, parkeringsbøder og tomme colaflasker.
Ved nærmere undersøgelse kunne jeg konkludere:
- Der havde ikke været kørt nogle Facebook annoncering de seneste år.
- Google shopping feed blev ikke anvendt, fordi den data der skulle bruges hertil kom fra en anden kilde.
- MailChimp var der ingen der anvendte.
- De mange eksport plugins var der heller ingen der anvendte.
Hvad kører og hvornår
For at forstå konteksten for de mange plugins og for hvorfor serveren konstant blev overbelastet, var det værd at kigge på hvad der kører hvornår (og hvorfor). Et virkelig godt og meget nørdet 👩💻🤓 værktøj til dette er WP Crontrol.
WP Crontrol giver dig adgang til at se hvilke CRON-job der kører i WordPress. Oversat til almindelig dansk er et CRON-job et tilrettelagt og timet “job” der er sat til at køre på et bestemt tidspunkt. Du kan sammenligne det med at sætte alarmen på din telefon, hvor resultatet her er, at når det er tid, så udføres specifikke systemopgaver på det angivne tidspunkt.
WP Crontrol afslørede at der hver dag kører import jobs, og disse import jobs opdaterer priser på varer i systemet. Her taler vi ikke kun om nogle få hundrede varer, vi taler om over 70.000 varer 😮.
Sådan virker produktfeed plugins
Her tager vi en lille detour, for at snakke lidt om hvordan produkt feed plugins som Facebook, Google, MailChimp, Klaviyo og ProfitMetrics, grundlæggende fungerer.
Der er basalt set tre måder de kan fungere på:
1. Produktfeedet kører efter en fast tidsplan, og hver dag kl. et-eller-andet dannes feedet på ny.
2. Produktfeedet opdateres hver gang et produkt ændrer sig.
3. Der sendes et signal til et eksternt system, med dataene for det opdaterede produkt.
Har din webshop kun nogle få hundrede produkter og opdateres der produkter sporadisk her og der, så er det nærmest lige meget hvilken metode der anvendes, da belastningen vil være så lav, at det i praksis intet betyder for sidens performance.
Den første kilde til problemer
Hvis du som denne webshop, derimod har 70.000 produkter og kører opdateringer af priser hver dag, så kan du hurtigt løbe ind i problemer. De første problemer fandtes ved en uheldig konfiguration, som ingen formentlig havde tænkt over før det begyndte at drukne webshoppen.
- Google Shopping feedet var sat til at køre hver dag.
- Det ene Facebook feed plugin kørte hver dag.
- Det Facebook feed kørte og startede forfra hver gang er var en produkt opdatering.
- MailChimp fik produkt data hver gang et produkt blev opdateret.
- Klaviyo fik produkt data hver gang et produkt blev opdateret.
- Clerk.io kørte 2 gange i døgnet og hentede alle ordrer og produkter.
- ProfitMetrics kørte hver gang et produkt blev opdateret.
Hvor efterlader det webshoppen? Det efterlader en situation hvor følgende sker ved en produkt opdatering (og husk, vi opdaterer 70.000 produkter hver dag):
- Der er måske/måske ikke et Google Shopping feed i gang med at køre alt produktdata igennem (det er tidskrævende og tager resourcer – derfor gøres det typisk i mindre bidder).
- Der er måske/måske ikke et Facebook feed i gang med at køre alt produktdata igennem.
- Der er måske/måske ikke et Clerk.io feed i gang med at køre alt produkt- og ordredata igennem.
- Der er et Facebook feed der siger “hov, en produktopdatering – Jeg starter lige forfra med at danne feedet”. Det er i sig selv et problem, for det gør at feedet når at starte forfra intet mindre end 70.000 gange mens der opdateres produkter.
- Der er et ProfitMetrics feed der siger “hov, en produktopdatering – Jeg starter lige forfra med at danne feedet”.
Så for hver eneste produktopdatering startes to feeds forfra, samtidig med at intet mindre end 3 andre plugins måske er i gang med at køre alt produkt data igennem. Tilføj til dette, 3 eksterne systemer der får signal med produkt data hver gang et produkt opdateres.
Den nørdede og kvikke læser vil her sige: Jamen hov! Kan denne data ikke bare caches og så er den meget hurtigere at hente?
Svaret er nej: Fordi i det samme et produkt opdateres, vil alle caches der gemmer produktets data, som udgangspunkt blive slettet og dermed skal dataen hentes fra ny. Hvis ikke det er sat meget nøje op, så ville alle forespørgsler være uncached. Og fordi der er flere samtidige spørgere på dataen, står de alle og vente på den samme data, så vi ser ikke kun ét enkelt uncached kald, men mange, mange kald.
Et forenklet regnestykke ville være 8 kald, der potentielt står og vente på samme data.
Så taler vi pludselig om 70.000 x 8 = 560.000 forespørgsler til uncached data.
“Bla, bla, bla – Det lyder sygt nørdet Yan..”
– Ganske almindelig WordPress bruger
Du har totalt ret.
Derfor vil jeg vise dig et før og efter billede af hvad der sker, når 26 unødige og belastende plugins fjernes. Fordi nogen en gang sagde et billede siger mere end tusind ord.
Du kan selv få lov at gætte hvor på kurven de mange unødige feeds blev fjernet 😂.
But, I want my feeds..
Hvis du har samme nære forhold til dine shopping feeds som Gollum har til guldringe, så vil jeg kraftigt råde dig til at konsolidere disse. I webshoppens tilfælde her, har vi samlet alle disse Shopping feeds i ét feed, således at data samles ét sted og dermed skal der hentes data til feedet én gang og ikke 8 gange 😅.
Så langt så godt. Nu har vi ryddet ud i plugins, som bogstaveligt talt stod på nakken af hinanden, for at få lov at lave belastende calls til systemet, – konstant. Så skulle man tro at den hellige grav var velforvaret. Men det var den ikke.
Når alt (andet) fejler – grav dybere
Nå, du troede nok at historien var slut her..? 😂 Det troede jeg også. Men webshoppen blev ved med at med at crashe. Langt sjældnere end tidligere, men dog alligevel. Da jeg efterhånden havde ryddet alt ud der ikke var nødvendigt, optimeret og konsolideret feeds, stod der et mysterie tilbage i form af hvorfor webshoppen stadig crashede.
Det besynderlige var, at der absolut ingen fejl at finde i fejl logs. Det betød at alt som udgangspunkt fungerede som det skulle, men noget skabte de flaskehalse der fik serveren til at crashe.
Derfor var det på tide at hente et af de helt nørdede værktøjer frem: New Relic. (Dam-dam-dahh!)
New Relic
Lad mig starte med at skrive: New Relic ikke er for almindelige brugere. Det er et værktøj til udviklere, hosting folk og lignende professioner. Og selvfølgelig til dig, der bare ikke kan få nok nørderi.
New Relic anvendes til at få dybere indsigt i hvad der kører af processer og transaktioner på systemer. På WordPress installationer kan det særligt give indsigt i hvad der tager tid, og hvor. Det er særlig brugbart når alt tilsyneladende virker og så alligevel ikke virker.
Vi tager lige den med et billede og tusind ord igen..
Det du ser ovenfor, er de langsomste indlæsningstider (load tider) i ms for produktvisninger. Den øverste er 204.029 ms. Det er 204 sekunder for at vise ét produkt. Nærmest alt andet så rimelig fint ud, men produktvisningerne (og dem er der alligevel 70.000 af) tog af en eller anden årsag utrolig lang tid.
Da jeg gravede i dataene for hvad en produktvisning krævede, så viste det at for hver produktvisning blev der hentet mellem 80-100 andre produkters data. På en webshop i denne størrelsesorden der trækker mange, mange besøgende ind betyder det at hvert produkt ikke kun henter ét andet produkt, men i stedet henter 80-100 produkter, ved en enkelt visning.
Så hvordan i alverden kan den slags forekomme? Preloading.
Preloading
Preloading er noget som en velmenende sjæl har opfundet, men som bare ikke er særlig godt i praksis, især ikke på sites med massive mængder trafik. Lidt forenklet betyder preloading, at når en visning indlæses, så hentes (preloades) samtlige links som brugeren enten hovere over med musen, eller bare samtlige links, helt automatisk.
Gæt hvad der var opsat i dette tilfælde af de to 😉 Du får lige et før og efter billede til preloading blev slået fra:
Og så et før og efter billede på indlæsningstiden for de langsomste produktvisninger som er nede på ca. 7-10 sekunder. Det er alligevel en forbedring fra 204 sekunder.
Det er bestemt ikke en formel-1 bil med 7-10 sekunders produktvisninger, men vi taler her om de langsomste – de hurtigste er i omegnen af 1-2 sekunder.
7-10 sekunder er langsomt
For en moderne webshop er 7-10 sekunder for langsomt. Google sætter grænsen ved ca. 2 sekunder for hvornår en given side opfattes som langsom, så det var stadig et godt stykke derfra.
Det gav anledning til at grave videre i, hvorfor de lange loadtider stadig forekom til trods for optimeringerne der allerede var lavet. Ved at dykke videre ned i dataene fandt jeg følgende:
For den knap så nørdede læser er det ovenstående viser, at der i forbindelse med indlæsningen af en produktvisning, indlæses en Widget med et “XforWC_Product_Filters_Frontend” der samtidig henter “wc_get_products“.
Det indikerer på almindeligt hverdags sprog, at der hentes en eller anden form for produkt filtrering (altså sådan noget hvor du f.eks. kan vælge farve, størrelse, etc.) og det efterfølgende henter produkter til denne filtrering.
Og her indtraf mystikken så 🕵️♀️ For på selve produktvisningen var der absolut ingen produktfiltre at finde.
Det gav anledning til at grave i koden der indlæses ifm. en produktvisning, her fandt jeg følgende:
Det som der vises her er, at på selve produktvisningen lå der en Widget skjult. Den var skjult så ingen kunne se den og derfor havde den aldrig fanget nogens opmærksomhed. Det den gjorde var dog katastrofalt for sitets performance.
For at forstå konsekvensen er det vigtigt at forstå hvad et produkt filter “gør”. Et produktfilter er typisk i en kontekst, f.eks. “T-shirt” (kategori) og “Farver” (Egenskab) og henter således alle T-shirts i en given farve. Denne kontekst kommer oftest fra en kategori-side (altså en gruppering af produkter) og typisk hentes der “maksimalt X antal produkter” (f.eks. 50 ad gangen).
I dette tilfælde var den Widget som blev anvendt, fra et plugin, der ikke havde taget stilling til hvad der ville ske hvis den anvendtes uden for kontekst. Når den anvendes uden for kontekst skete følgende (let forenklet):
- Kan jeg finde en kategori? Nej – Så hent alle produkter
- Kan jeg finde en begrænsning på antal der skal vises ad gangen? Nej – Så hent alt på én gang
Det var katastrofalt. For det betyder at fordi den var indsat på en produktside hvor ingen af delene kunne findes, så hentede hver eneste produktvisning over 70.000 produkter. I praksis betyder det at hvert enkelt besøg til en side, henter hele webshoppens katalog – Og set på den måde var det da egentlig lidt imponerende at det “kun” tog 7-10 sekunder for de langsommeste visninger 😅
Pluginnet skulle egentlig have været lavet på en måde, således at hvis konteksten ikke fandtes, så skulle den aldrig forsøge at hente noget 🤓
Med denne widget ryddet af vejen så det pludselig helt anderledes ud. Nu var vi nede på at de langsomste produktvisninger var på omkring 800ms (eller 0,8 sekunder) – en forbedring på over 90%
Til sidst: Caching
Når der skal laves produktopdateringer i størrelsesordnen 70.000 produkter og der samtidig skal genereres feeds, er det værd at tænker over hvilken caching strategi du benytter. Almindeligvis er de fleste caches opsat således, at et produkts cache cleares ved hver produkt opdatering. Ofte er det hele cachen, der cleares.
Laves der opdateringer på 70.000 produkter giver det rigtig meget cache der skal dannes på ny. Og det koster også ressourcer på potentielt unødig processering. En af de ting jeg derfor gjorde for webshoppen her var at sætte “timings” op således at cachen ikke cleares unødigt, men kun på de rette tidspunkter. Således at der ikke konstant bruges ressourcer på at genopbygge en cache der få sekunder efter, cleares igen.
Vores case shop kører stabilt og spinner som en mis i forhold til tidligere. Performance er forbedret med 1200% på nuværende tidspunkt. Det var en svømmetur helt nede i den dybe ende, men resultatet var også derefter.
Har selv en WooCommerce performance historie at dele?
Så giv lyd i kommentarerne 😊