Cum am îmbunătățit performanța SmashingMag

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, vom arunca o privire atentă asupra unora dintre modificările pe care le-am făcut chiar pe acest site — care rulează pe JAMStack cu React — pentru a optimiza performanța web și a îmbunătăți valorile Core Web Vitals. Cu unele dintre greșelile pe care le-am făcut și unele dintre schimbările neașteptate care au ajutat la creșterea tuturor valorilor la nivel general.

Fiecare poveste de performanță web este similară, nu-i așa? Începe întotdeauna cu mult așteptata revizuire a site-ului. O zi în care un proiect, complet lustruit și optimizat cu atenție, este lansat, ocupându-se la un loc ridicat și depășind scorurile de performanță în Lighthouse și WebPageTest. În aer predomină o sărbătoare și un sentiment de realizare din toată inima - reflectat frumos în retweet-uri și comentarii și buletine informative și firele Slack.

Cu toate acestea, pe măsură ce timpul trece, entuziasmul dispare încet, iar ajustările urgente, funcțiile atât de necesare și noi cerințe de afaceri se strecoară. Și dintr-o dată, înainte să vă dați seama, baza de cod devine puțin supraponderală și fragmentată , terță parte. scripturile trebuie să se încarce puțin mai devreme, iar conținutul dinamic nou strălucitor își găsește drum în DOM prin ușile din spate ale scripturilor de la a patra parte și ale invitaților lor neinvitați.

Am fost acolo și la Smashing. Nu mulți oameni știu asta, dar suntem o echipă foarte mică de aproximativ 12 persoane, mulți dintre care lucrează cu jumătate de normă și majoritatea poartă de obicei multe pălării diferite într-o anumită zi. Deși performanța este obiectivul nostru de aproape un deceniu, nu am avut niciodată o echipă de performanță dedicată.

După ultima reproiectare de la sfârșitul anului 2017, a fost Ilya Pukhalski din partea JavaScript (part-time), Michael Riethmueller din partea CSS a lucrurilor (câteva ore pe săptămână) și cu adevărat al tău, jucând jocuri mintale cu CSS critic. și încercând să jongleze cu câteva prea multe lucruri.

Captură de ecran a surselor de performanță care arată scorurile Lighthouse între 40 și 60
Aici am început. Cu scorurile Lighthouse fiind undeva între 40 și 60, am decis să abordăm performanța (din nou) din cap. (Sursa imagine: Lighthouse Metrics) (Previzualizare mare)

După cum s-a întâmplat, am pierdut noțiunea performanței în aglomerația rutinei de zi cu zi. Proiectam și construiam lucruri, înființam produse noi, refactoram componentele și publicam articole. Așadar, până la sfârșitul anului 2020, lucrurile au scăpat puțin de sub control, scorurile Lighthouse gălbui-roșu arătând încet peste tot. A trebuit să reparăm asta.

Acolo eram noi

Unii dintre voi s-ar putea să știți că rulăm pe JAMStack, cu toate articolele și paginile stocate ca fișiere Markdown, fișierele Sass compilate în CSS, JavaScript împărțit în bucăți cu Webpack și Hugo construind pagini statice pe care apoi le servim direct dintr-un CDN Edge. În 2017, am construit întregul site cu Preact, dar apoi ne-am mutat la React în 2019 - și l-am folosit împreună cu câteva API-uri pentru căutare, comentarii, autentificare și finalizare.

Întregul site este construit având în vedere îmbunătățirea progresivă, ceea ce înseamnă că tu, dragă cititor, poți citi fiecare articol Smashing în întregime fără a fi nevoie să pornești deloc aplicația. Nici nu este foarte surprinzător – în cele din urmă, un articol publicat nu se schimbă prea mult de-a lungul anilor, în timp ce piese dinamice, cum ar fi autentificarea de membru și verificarea, au nevoie ca aplicația să ruleze.

Întreaga versiune pentru implementarea a aproximativ 2500 de articole live durează aproximativ 6 minute în acest moment. Procesul de construire singur a devenit o bestie de-a lungul timpului, cu injectări critice de CSS, divizarea codului Webpack, inserții dinamice de reclame și panouri de caracteristici, (re)generare RSS și eventuale teste A/B pe margine.

La începutul lui 2020, am început cu refactorizarea mare a componentelor de aspect CSS. Nu am folosit niciodată CSS-in-JS sau componente cu stil, ci în schimb un sistem vechi bazat pe componente de module Sass care ar fi compilat în CSS. În 2017, întregul aspect a fost construit cu Flexbox și reconstruit cu CSS Grid și CSS Custom Properties la jumătatea anului 2019. Cu toate acestea, unele pagini au avut nevoie de un tratament special datorită noilor spoturi publicitare și panourilor de produse noi. Deci, în timp ce aspectul funcționa, nu funcționa foarte bine și era destul de dificil de întreținut.

În plus, antetul cu navigarea principală a trebuit să se schimbe pentru a se potrivi cu mai multe articole pe care doream să le afișam dinamic. În plus, am vrut să refactorăm unele componente utilizate frecvent pe site, iar CSS-ul folosit acolo a avut nevoie și de unele revizuiri - căsuța de buletin informativ fiind cel mai notabil vinovat. Am început prin a refactoriza unele componente cu un CSS de utilitate, dar nu am ajuns niciodată la punctul în care a fost folosit în mod constant pe întregul site.

Problema mai mare a fost pachetul mare de JavaScript care - nu foarte surprinzător - a blocat firul principal pentru sute de milisecunde. Un pachet JavaScript mare ar putea părea deplasat într-o revistă care publică doar articole, dar, de fapt, există o mulțime de scenarii care se întâmplă în culise.

Avem diferite stări de componente pentru clienții autentificați și neautentificați. Odată ce v-ați conectat, vrem să arătăm toate produsele în prețul final și, pe măsură ce adăugați o carte în coș, vrem să păstrăm un coș accesibil printr-o atingere pe un buton - indiferent de pagina pe care vă aflați. Publicitatea trebuie să apară rapid, fără a provoca schimbări perturbatoare de aspect , și același lucru este valabil și pentru panourile de produse native care evidențiază produsele noastre. Plus un lucrător de servicii care memorează în cache toate elementele statice și le servește pentru vizualizări repetate, împreună cu versiuni stocate în cache ale articolelor pe care un cititor le-a vizitat deja.

Așa că toate aceste scenarii au trebuit să se întâmple la un moment dat și a fost drenant pentru experiența de citire, chiar dacă scenariul a venit destul de târziu. Sincer, lucram cu minuțiozitate la site și la componente noi, fără să urmărim îndeaproape performanța (și mai aveam câteva lucruri de reținut pentru 2020). Punctul de cotitură a venit pe neașteptate. Harry Roberts și-a desfășurat (excelentul) Web Performance Masterclass ca un atelier online cu noi și, pe parcursul întregului atelier, a folosit Smashing ca exemplu, evidențiind problemele pe care le-am avut și sugerând soluții la aceste probleme alături de instrumente și linii directoare utile.

Pe parcursul atelierului, am luat cu sârguință notițe și am revizuit baza de cod. La momentul atelierului, scorurile noastre Lighthouse erau 60-68 pe pagina de pornire și aproximativ 40-60 pe paginile articolelor - și, evident, mai rău pe mobil. Odată terminat atelierul, ne-am apucat de treabă.

Identificarea blocajelor

Adesea avem tendința de a ne baza pe anumite scoruri pentru a înțelege cât de bine performam, totuși, prea des, scorurile individuale nu oferă o imagine completă. După cum a remarcat elocvent David East în articolul său, performanța web nu este o singură valoare; este o distributie. Chiar dacă o experiență web este puternic și complet o performanță optimizată completă, nu poate fi doar rapidă. Ar putea fi rapid pentru unii vizitatori, dar în cele din urmă va fi și mai lent (sau lent) pentru alții.

Motivele pentru aceasta sunt numeroase, dar cel mai important este o diferență uriașă în condițiile rețelei și hardware-ul dispozitivului din întreaga lume. De cele mai multe ori nu putem influența cu adevărat aceste lucruri, așa că trebuie să ne asigurăm că experiența noastră le găzduiește în schimb.

În esență, treaba noastră este să creștem proporția de experiențe captivante și să scădem proporția de experiențe lente. Dar pentru asta, trebuie să ne facem o imagine corectă a distribuției. Acum, instrumentele de analiză și instrumentele de monitorizare a performanței vor furniza aceste date atunci când este necesar, dar ne-am uitat în mod special la CrUX, Raportul despre experiența utilizatorului Chrome. CrUX generează o imagine de ansamblu asupra distribuțiilor de performanță de-a lungul timpului, cu trafic colectat de la utilizatorii Chrome. O mare parte din aceste date se referă la Core Web Vitals pe care Google le-a anunțat încă din 2020 și care, de asemenea, contribuie și sunt expuse în Lighthouse.

Cele mai mari statistici Contentful Paint (LCP) care arată o scădere masivă a performanței între mai și septembrie în 2020
Distribuția de performanță pentru cea mai mare vopsea plină de conținut în 2020. Între mai și septembrie, performanța a scăzut masiv. Date de la CrUX. (Previzualizare mare)

Am observat că în general, performanța noastră a regresat dramatic pe parcursul anului, cu scăderi deosebite în jurul lunii august și septembrie. Odată ce am văzut aceste grafice, ne-am putea uita înapoi la unele dintre PR-urile pe care le-am împins în direct atunci pentru a studia ce s-a întâmplat de fapt.

Nu a durat mult să ne dăm seama că tocmai în aceste vremuri am lansat o nouă bară de navigare live. Acea bară de navigare – folosită pe toate paginile – se baza pe JavaScript pentru a afișa elementele de navigare într-un meniu la atingere sau la clic, dar partea JavaScript a fost de fapt inclusă în pachetul app.js. Pentru a îmbunătăți Time To Interactive, am decis să extragem scriptul de navigare din pachet și să-l difuzăm inline.

În același timp, am trecut de la un fișier CSS critic (învechit) creat manual la un sistem automat care genera CSS critic pentru fiecare șablon - pagină de pornire, articol, pagină de produs, eveniment, panou de locuri de muncă și așa mai departe - și CSS critic inline în timpul timpul de construire. Cu toate acestea, nu ne-am dat seama cât de greu era CSS-ul critic generat automat. A trebuit să o explorăm mai detaliat.

Și, de asemenea, cam în același timp, am ajustat încărcarea fonturilor web , încercând să împingem fonturile web mai agresiv cu sugestii de resurse, cum ar fi preîncărcarea. Totuși, acest lucru pare să afecteze eforturile noastre de performanță, deoarece fonturile web întârziau redarea conținutului, fiind supra-prioritizate lângă fișierul CSS complet.

Acum, unul dintre motivele obișnuite ale regresiei este costul ridicat al JavaScript, așa că ne-am uitat și la Webpack Bundle Analyzer și la harta solicitărilor lui Simon Hearne pentru a obține o imagine vizuală a dependențelor noastre JavaScript. Arăta destul de sănătos la început.

O hartă vizuală a dependențelor JavaScript
Nimic inovator cu adevărat: harta cererilor nu părea excesivă la început. (Previzualizare mare)

Câteva solicitări veneau către CDN, un serviciu de consimțământ pentru cookie-uri Cookiebot, Google Analytics, plus serviciile noastre interne pentru difuzarea panourilor de produse și publicitate personalizată. Nu părea că există multe blocaje - până când ne-am uitat puțin mai atent.

În munca de performanță, este obișnuit să se uite la performanța unor pagini critice - cel mai probabil pagina de pornire și cel mai probabil câteva pagini de articole/produs. Cu toate acestea, deși există o singură pagină de pornire, ar putea exista o mulțime de pagini de produse diferite, așa că trebuie să alegem unele care să fie reprezentative pentru publicul nostru.

De fapt, deoarece publicăm destul de multe articole de cod și design pe SmashingMag, de-a lungul anilor am acumulat literalmente mii de articole care conțineau GIF-uri grele, fragmente de cod evidențiate de sintaxă, încorporare CodePen, video/audio încorporați și fire imbricate de comentarii fără sfârșit.

Când au fost reuniți, mulți dintre ei au provocat nimic altceva decât o explozie a dimensiunii DOM, împreună cu munca excesivă în firul principal - încetinind experiența pe mii de pagini. Ca să nu mai vorbim de faptul că, odată cu publicitatea, unele elemente DOM au fost injectate târziu în ciclul de viață al paginii, provocând o cascadă de recalculări și revopsiri de stil - de asemenea, sarcini costisitoare care pot produce sarcini lungi.

Toate acestea nu apăreau în harta pe care am generat-o pentru o pagină de articol destul de ușoară din graficul de mai sus. Așa că am ales cele mai grele pagini pe care le aveam - pagina de pornire atotputernică, cea mai lungă, cea cu multe încorporare video și cea cu multe încorporare CodePen - și am decis să le optimizăm cât de mult am putut. La urma urmei, dacă sunt rapide, atunci paginile cu o singură încorporare CodePen ar trebui să fie și mai rapide.

Având în vedere aceste pagini, harta arăta puțin diferit. Rețineți linia groasă uriașă care se îndreaptă către playerul Vimeo și Vimeo CDN, cu 78 de solicitări provenind dintr-un articol Smashing.

O hartă mentală vizuală care arată probleme de performanță, în special în articolele care au folosit o mulțime de încorporare video și/sau video
Pe unele pagini de articole, graficul arăta diferit. Mai ales cu o mulțime de încorporare de cod sau video, performanța a scăzut destul de semnificativ. Din păcate, multe dintre articolele noastre le au. (Previzualizare mare)

Pentru a studia impactul asupra firului principal, am analizat în profunzime panoul Performanță din DevTools. Mai precis, căutam sarcini care durează mai mult de 50 ms (evidențiate cu un dreptunghi roșu în colțul din dreapta sus) și sarcini care conțin stiluri de recalculare (bară violetă). Primul ar indica execuția JavaScript costisitoare, în timp ce cel din urmă ar expune invalidările de stil cauzate de injecții dinamice de conținut în DOM și CSS suboptimal. Acest lucru ne-a oferit câteva indicații utile de unde să începem. De exemplu, am descoperit rapid că încărcarea fonturilor noastre web a avut un cost semnificativ de revopsire, în timp ce fragmentele JavaScript erau încă suficient de grele pentru a bloca firul principal.

O captură de ecran a panoului de performanță din DevTools care arată fragmente JavaScript care erau încă suficient de grele pentru a bloca firul principal
Studierea panoului de performanță în DevTools. Au fost câteva sarcini lungi, care durau mai mult de 50 ms și blocau firul principal. (Previzualizare mare)

Ca punct de referință, ne-am uitat foarte îndeaproape la Core Web Vitals, încercând să ne asigurăm că obținem un punctaj bun pentru toate acestea. Am ales să ne concentrăm în mod special pe dispozitivele mobile lente - cu 3G lentă, 400 ms RTT și viteză de transfer de 400 kbps, doar pentru a fi pe latura pesimistă a lucrurilor. Nu este de mirare că nici Lighthouse nu a fost foarte mulțumit de site-ul nostru, oferind scoruri roșii complet solide pentru articolele cele mai grele și plângându-se neobosit de JavaScript, CSS, imaginile offscreen și dimensiunile acestora nefolosite.

O captură de ecran a datelor Lighthouse care arată oportunitățile și economiile estimate
Nici Lighthouse nu a fost deosebit de mulțumit de performanța unor pagini. Acesta este cel cu o mulțime de încorporare video. (Previzualizare mare)

Odată ce am avut câteva date în fața noastră, ne-am putea concentra pe optimizarea celor mai grele trei pagini de articole, cu accent pe CSS critic (și non-critice), pachetul JavaScript, sarcini lungi, încărcarea fonturilor web, schimbările de aspect și terțe părți. -inglobeaza. Mai târziu, vom revizui, de asemenea, baza de cod pentru a elimina codul vechi și a folosi noi funcții moderne de browser. Părea că aveam multă muncă înainte și, într-adevăr, eram destul de ocupați pentru lunile următoare.

Îmbunătățirea ordinii activelor în <head>

În mod ironic, primul lucru la care ne-am uitat nici măcar nu era strâns legat de toate sarcinile pe care le-am identificat mai sus. În cadrul atelierului de performanță, Harry a petrecut o perioadă considerabilă de timp explicând ordinea elementelor în <head> a fiecărei pagini, subliniind că a furniza rapid conținut critic înseamnă a fi foarte strategic și atent la modul în care sunt ordonate activele în codul sursă. .

Acum nu ar trebui să fie o mare revelație că CSS critic este benefic pentru performanța web. Cu toate acestea, a fost puțin surprinzător cât de multă diferență are ordinea tuturor celorlalte active - indicii de resurse, preîncărcare a fonturilor web, scripturi sincrone și asincrone, CSS complet și metadate.

Am întors întregul <head> cu susul în jos, plasând CSS critic înaintea tuturor scripturilor asincrone și a tuturor activelor preîncărcate, cum ar fi fonturile, imaginile etc. Am defalcat elementele la care ne vom preconecta sau le vom preîncărca după șablon și tip de fișier, astfel încât imaginile critice, evidențierea de sintaxă și încorporarea video vor fi solicitate din timp doar pentru un anumit tip de articole și pagini.

În general, am orchestrat cu atenție ordinea în <head> , am redus numărul de active preîncărcate care concurau pentru lățimea de bandă și ne-am concentrat pe obținerea corectă a CSS-ului critic. Dacă doriți să vă aprofundați câteva dintre considerentele critice cu ordinea <head> , Harry le evidențiază în articolul despre CSS și performanța rețelei. Numai această schimbare ne-a adus în jur de 3–4 puncte Lighthouse în totalitate.

Trecerea de la CSS critic automat automat Înapoi la CSS critic manual

Totuși, mutarea etichetelor <head> a fost o parte simplă a poveștii. Una mai dificilă a fost generarea și gestionarea fișierelor CSS critice. În 2017, am creat manual CSS critic pentru fiecare șablon, prin colectarea tuturor stilurilor necesare pentru a reda primii 1000 de pixeli în înălțime pe toate lățimile ecranului. Desigur, aceasta a fost o sarcină greoaie și ușor neinspirată, ca să nu mai vorbim de problemele de întreținere pentru îmblânzirea unei întregi familii de fișiere CSS critice și a unui fișier CSS complet.

Așa că am analizat opțiunile de automatizare a acestui proces ca parte a rutinei de construire. Nu era cu adevărat o lipsă de instrumente disponibile, așa că am testat câteva și am decis să rulăm câteva teste. Am reușit să le punem în funcțiune și să funcționăm destul de repede. Ieșirea părea să fie suficient de bună pentru un proces automat, așa că, după câteva modificări de configurare, l-am conectat și l-am împins în producție. Acest lucru s-a întâmplat în jurul lunii iulie-august anul trecut, ceea ce este bine vizualizat în creșterea și scăderea performanței în datele CrUX de mai sus. Am continuat să mergem înainte și înapoi cu configurația, având adesea probleme cu lucruri simple, cum ar fi adăugarea unor anumite stiluri sau eliminarea altora. De exemplu, stiluri de prompt pentru consimțământul cookie-urilor care nu sunt cu adevărat incluse pe o pagină decât dacă scriptul cookie a fost inițializat.

În octombrie, am introdus câteva modificări majore de aspect pe site și, când ne uităm la CSS-ul critic, ne-am confruntat din nou cu exact aceleași probleme - rezultatul generat a fost destul de pronunțat și nu a fost chiar ceea ce ne-am dorit. . Așadar, ca experiment la sfârșitul lunii octombrie, ne-am reunit cu toții punctele forte pentru a revizui abordarea noastră critică CSS și a studia cât de mai mic ar fi un CSS critic realizat manual . Am respirat adânc și am petrecut zile întregi în jurul instrumentului de acoperire a codurilor din paginile cheie. Am grupat manual regulile CSS și am eliminat duplicatele și codul vechi în ambele locuri — CSS-ul critic și CSS-ul principal. A fost într-adevăr o curățare foarte necesară, deoarece multe stiluri care au fost scrise în 2017-2018 au devenit învechite de-a lungul anilor.

Ca rezultat, am ajuns să avem trei fișiere CSS critice realizate manual și cu alte trei fișiere care sunt în curs de desfășurare:

  • critical-homepage-manual.css (8,2 KB, Brotlified)
  • critic-article-manual.css (8 KB, Brotlified)
  • critic-articles-manual.css (6 KB, Brotlified)
  • critic-books-manual.css ( lucru de făcut )
  • critic-events-manual.css ( lucru de făcut )
  • critic-job-board-manual.css ( lucru de făcut )

Fișierele sunt aliniate în capul fiecărui șablon, iar momentan sunt duplicate în pachetul CSS monolitic care conține tot ce a fost folosit vreodată (sau nu a mai folosit cu adevărat) pe site. Momentan, ne uităm să împărțim pachetul CSS complet în câteva pachete CSS, astfel încât un cititor al revistei nu ar descărca stiluri de pe panoul de locuri de muncă sau din paginile cărții, dar atunci când ajunge la acele pagini, ar obține o randare rapidă. cu CSS critic și obțineți restul CSS-ului pentru pagina respectivă în mod asincron - numai pe pagina respectivă.

Desigur, fișierele CSS critice realizate manual nu aveau o dimensiune mult mai mică: am redus dimensiunea fișierelor CSS critice cu aproximativ 14% . Cu toate acestea, au inclus tot ceea ce aveam nevoie în ordinea corectă, de la început până la sfârșit, fără duplicate și stiluri supraevaluate. Acesta părea a fi un pas în direcția corectă și ne-a oferit un impuls Lighthouse de încă 3-4 puncte. Făceam progrese.

Modificarea încărcării fontului web

Cu font-display la îndemână, încărcarea fonturilor pare să fie o problemă în trecut. Din păcate, în cazul nostru nu este deloc corect. Voi, dragi cititori, se pare că vizitați o serie de articole pe Smashing Magazine. De asemenea, reveniți frecvent pe site pentru a citi încă un articol - poate câteva ore sau zile mai târziu, sau poate o săptămână mai târziu. Una dintre problemele pe care le-am avut cu font-display folosit pe site a fost că, pentru cititorii care s-au mutat mult între articole, am observat o mulțime de flash-uri între fontul alternativ și fontul web (ceea ce nu ar trebui să se întâmple în mod normal, deoarece fonturile ar fi stocate corect în cache).

Nu s-a părut o experiență decentă de utilizator, așa că am căutat opțiuni. Pe Smashing, folosim două fonturi principale - Mija pentru titluri și Elena pentru text. Mija vine în două greutăți (Regular și Bold), în timp ce Elena vine în trei greutăți (Regular, Italic, Bold). Am renunțat la Elena's Bold Italic cu ani în urmă, în timpul reproiectării, doar pentru că l-am folosit pe doar câteva pagini. Subsetăm celelalte fonturi eliminând caracterele neutilizate și intervalele Unicode.

Articolele noastre sunt în mare parte plasate în text, așa că am descoperit că de cele mai multe ori pe site, cea mai mare vopsea de conținut este fie primul paragraf de text dintr-un articol, fie fotografia autorului. Aceasta înseamnă că trebuie să avem o grijă deosebită să ne asigurăm că primul paragraf apare rapid într-un font alternativ, în timp ce trecem cu grație la fontul web cu reflow minim.

Aruncă o privire atentă la experiența inițială de încărcare a primei pagini (încetinită de trei ori):

Am avut patru obiective principale atunci când găsim o soluție:

  1. La prima vizită, redați textul imediat cu un font alternativ;
  2. Potriviți valorile fonturilor ale fonturilor alternative și ale fonturilor web pentru a minimiza schimbările de aspect;
  3. Încărcați toate fonturile web asincron și aplicați-le pe toate simultan (max. 1 reflow);
  4. La vizitele ulterioare, redați tot textul direct în fonturi web (fără clipire sau reflow).

Inițial, am încercat să folosim font-display: swap on font-face . Aceasta părea a fi cea mai simplă opțiune, totuși, așa cum am menționat mai sus, unii cititori vor vizita o serie de pagini, așa că am ajuns să pâlpâiem mult cu cele șase fonturi pe care le redam pe tot site-ul. De asemenea, doar cu font-display , nu am putut grupa solicitări sau revopsi.

O altă idee a fost să redați totul în font de rezervă la vizita inițială , apoi să solicitați și să puneți în cache toate fonturile în mod asincron, iar numai la vizitele ulterioare să livrați fonturi web direct din cache. Problema cu această abordare a fost că un număr de cititori provin de la motoarele de căutare și cel puțin unii dintre ei vor vedea doar acea pagină - și nu am vrut să redăm un articol doar într-un font de sistem.

Deci ce este atunci?

Din 2017, folosim abordarea de redare în două etape pentru încărcarea fonturilor web, care descrie practic două etape de randare: una cu un subset minim de fonturi web și cealaltă cu o familie completă de greutăți de font. Pe vremuri, am creat subseturi minime de Mija Bold și Elena Regular, care erau greutățile cele mai frecvent utilizate pe site. Ambele subseturi includ doar caractere latine, semne de punctuație, numere și câteva caractere speciale. Aceste fonturi ( ElenaInitial.woff2 și MijaInitial.woff2 ) aveau dimensiuni foarte mici - adesea doar aproximativ 10–15 KB. Le servim în prima etapă a redării fonturilor, afișând întreaga pagină în aceste două fonturi.

CLS cauzat de pâlpâirea fonturilor web
CLS cauzat de pâlpâirea fonturilor web (umbrele de sub imaginile autorului se mișcă din cauza schimbării fontului). Generat cu Layout Shift GIF Generator. (Previzualizare mare)

Facem acest lucru cu un API de încărcare a fonturilor care ne oferă informații despre fonturile care s-au încărcat cu succes și care nu s-au încărcat încă. În culise, se întâmplă prin adăugarea unei clase .wf-loaded-stage1 la body , cu stiluri care redă conținutul în acele fonturi:

 .wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }

Deoarece fișierele cu fonturi sunt destul de mici, sperăm că trec prin rețea destul de repede. Apoi, deoarece cititorul poate începe de fapt să citească un articol, încărcăm greutățile complete ale fonturilor în mod asincron și adăugăm .wf-loaded-stage2 în corp :

 .wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: Mija,sans-serif; }

Deci, atunci când încarcă o pagină, cititorii vor primi rapid mai întâi un mic font web subset, apoi trecem la familia completă de fonturi. Acum, în mod implicit, aceste comutări între fonturile alternative și fonturile web au loc aleatoriu, în funcție de ceea ce vine mai întâi prin rețea. Acest lucru s-ar putea simți destul de perturbator, deoarece ați început să citiți un articol. Deci, în loc să lăsăm browserului să decidă când să schimbe fonturile, grupăm revopsele , reducând impactul refluxului la minimum.

 /* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }

Totuși, ce se întâmplă dacă primul subset mic de fonturi nu ajunge rapid prin rețea? Am observat că acest lucru pare să se întâmple mai des decât ne-am dori. În acest caz, după expirarea unui timeout de 3 secunde, browserele moderne revin la un font de sistem (în stiva noastră de fonturi ar fi Arial), apoi trec la ElenaInitial sau MijaInitial , doar pentru a fi trecute la Elena sau Mija complet mai târziu. . Asta a produs un pic prea mult fulger la degustarea noastră. Ne-am gândit inițial să eliminăm prima etapă de randare doar pentru rețelele lente (prin Network Information API), dar apoi am decis să o eliminăm cu totul.

Deci, în octombrie, am eliminat subseturile cu totul, împreună cu etapa intermediară. Ori de câte ori toate greutățile ambelor fonturi Elena și Mija sunt descărcate cu succes de client și gata pentru a fi aplicate, inițiem etapa 2 și revopsim totul deodată. Și pentru a face reflow-urile și mai puțin vizibile, am petrecut puțin timp potrivirea fonturilor alternative și a fonturilor web . Aceasta a însemnat în mare parte aplicarea unor dimensiuni de font și înălțimi de linii ușor diferite pentru elementele pictate în prima porțiune vizibilă a paginii.

Pentru asta, am folosit font-style-matcher și (ahem, ahem) câteva numere magice. Acesta este și motivul pentru care inițial am folosit -apple-system și Arial ca fonturi de rezervă globale; San Francisco (redat prin -apple-system ) părea a fi puțin mai frumos decât Arial, dar dacă nu este disponibil, am ales să folosim Arial doar pentru că este larg răspândit în majoritatea sistemelor de operare.

În CSS, ar arăta astfel:

 .article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }

Acest lucru a funcționat destul de bine. Afișăm text imediat, iar fonturile web apar pe ecran grupate, în mod ideal provocând exact o redistribuire în prima vizualizare și nicio redistribuire în totalitate în vizualizările ulterioare.

Odată ce fonturile au fost descărcate, le stocăm în memoria cache a unui lucrător de service. La vizitele ulterioare, verificăm mai întâi dacă fonturile sunt deja în cache. Dacă sunt, le preluăm din memoria cache a lucrătorului de service și le aplicăm imediat. Și dacă nu, începem de la capăt cu fallback-web-font-switcheroo .

Această soluție a redus numărul de refluxuri la minim (una) pe conexiuni relativ rapide, păstrând totodată fonturile persistent și fiabil în cache. În viitor, sperăm sincer să înlocuim numerele magice cu f-mod-uri. Poate că Zach Leatherman ar fi mândru.

Identificarea și defalcarea JS monolitic

Când am studiat firul principal din panoul de performanță al DevTools, am știut exact ce trebuie să facem. Au fost opt ​​sarcini lungi care durau între 70ms și 580ms, blocând interfața și făcând-o să nu răspundă. În general, acestea au fost scripturile care au costat cel mai mult:

  • uc.js , un script de prompt pentru cookie (70 ms)
  • recalculări de stil cauzate de fișierul full.css primit (176ms) (CSS-ul critic nu conține stiluri sub înălțimea de 1000 px în toate ferestrele de vizualizare)
  • scripturi de publicitate care rulează la evenimentul de încărcare pentru a gestiona panouri, coș de cumpărături etc. + recalculări de stil (276 ms)
  • comutator font web, recalculări de stil (290 ms)
  • evaluare app.js (580 ms)

Ne-am concentrat mai întâi pe cele care au fost cele mai dăunătoare – să zicem așa cele mai lungi sarcini lungi.

O captură de ecran luată de la DevTools care arată validările de stil pentru prima pagină a revistei zdrobitoare
În partea de jos, Devtools arată invalidările de stil - un comutator de font a afectat 549 de elemente care au trebuit să fie repetate. Ca să nu mai vorbim de schimbările de aspect pe care le provoca. (Previzualizare mare)

Prima a avut loc din cauza recalculărilor costisitoare de aspect cauzate de schimbarea fonturilor (de la fontul alternativ la font web), provocând peste 290 ms de muncă suplimentară (pe un laptop rapid și o conexiune rapidă). Prin eliminarea primei etape din încărcarea fontului, am reușit să câștigăm aproximativ 80 ms înapoi. Totuși, nu a fost suficient de bun pentru că era mult peste bugetul de 50 ms. Așa că am început să săpăm mai adânc.

Principalul motiv pentru care au avut loc recalculările a fost doar din cauza diferențelor uriașe dintre fonturile alternative și fonturile web. Prin potrivirea înălțimii liniilor și a dimensiunilor pentru fonturile alternative și fonturile web , am reușit să evităm multe situații în care o linie de text s-ar încadra pe o nouă linie în fontul alternativ, dar apoi se micșorează puțin și se potrivește pe linia anterioară, provocând schimbări majore în geometria întregii pagini și, în consecință, schimbări masive de aspect. Ne-am jucat și cu letter-spacing și word-spacing , dar nu a dat rezultate bune.

Cu aceste modificări, am reușit să reducem încă 50-80 ms, dar nu am putut să o reducem sub 120 ms fără a afișa conținutul într-un font alternativ și apoi să afișăm conținutul în fontul web. Evident, ar trebui să afecteze în mod masiv doar vizitatorii pentru prima dată, deoarece vizualizările ulterioare ale paginii ar fi redate cu fonturile preluate direct din memoria cache a lucrătorului de servicii, fără refluxuri costisitoare din cauza comutatorului de font.

Apropo, este destul de important să observăm că, în cazul nostru, am observat că majoritatea sarcinilor lungi nu au fost cauzate de JavaScript masiv, ci de recalculări ale aspectului și analiza CSS, ceea ce însemna că trebuie să facem puțin CSS. cleaning, especially watching out for situations when styles are overwritten. In some way, it was good news because we didn't have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren't way above the magical 50ms threshold.

The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.

With Webpack, we've split up the monolithic bundle into smaller chunks with code-splitting , about 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.

We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we've added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).

A screenshot showing JavaScript chunks affecting performance with each running no longer than 40ms on the main thread
JavaScript chunks in action, with each running no longer than 40ms on the main thread. (Previzualizare mare)

Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.

We removed the script from the bundle and extracted it as a separate script . Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.

Performance stats for the smashing magazine front page showing the function call for nav.js that happened right after a monolithic app.js bundle had been executed
Notice that the function call for nav.js is happening after a monolithic app.js bundle is executed. That's not quite right. (Previzualizare mare)

However, what we didn't notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn't really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.

A screenshot of the the Long task reduced by almost 200ms
By prioritizing the nav.js script, we were able to reduce the Long task by almost 200ms. (Previzualizare mare)

We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have three major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.

It's worth mentioning that these results are really the best-scenario- results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts and customer's browser extensions that would require a separate conversation.

Dealing With 3rd-Parties

Fortunately, our third-party scripts footprint (and the impact of their friends' fourth-party-scripts) wasn't huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scripts , but also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.

Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.

The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width and height for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.

We've re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with <img loading=lazy> ), while the ones on the top are prioritized ( <img loading=eager> ). The same goes for all third-party embeds.

We replaced some dynamic parts with their static counterparts — eg while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker's cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.

As of the moment of writing, we're preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async attribute, so the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.

A screenshot of the main front page of smashing magazine being highlighted by the Diagnostics CSS tool for each image that does not have a width/height attribute
Diagnostics CSS in use: highlighting images that don't have width/height attributes, or are served in legacy formats. (Previzualizare mare)

To ensure that our images always include width and height attributes, we've also modified Harry Roberts' snippet and Tim Kadlec's diagnostics CSS to highlight whenever an image isn't served properly. It's used in development and editing but obviously not in production.

One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading .

First, we've added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.

* { outline: 3px solid red }
* { outline: 3px solid red } 
A screenshot of an article published on smashing magazine with red lines on the layout to help check the stability and rendering on the page
A quick trick to check the stability of the layout, by adding * { outline: 3px red } and observing the boxes as the browser is rendering the page. (Previzualizare mare)

Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.

Here's the recording of a page being loaded on a fast connection:

Recording for the loading of the page with an outline applied, to observe layout shifts.

And here's the recording of a recording being played to study what happens with the layout:

Auditing the layout shifts by rewatching a recording of the site loading in slow motion, watching out for height and width of content blocks, and layout shifts.

By auditing the layout shifts this way, we were able to quickly notice what's not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height and font-size on headings might go a long way to avoid large shifts.

With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest article, and gain a few points for code embeds.

Enhancing The Experience

We've tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we've done our best to use some of the new technologies that have become available last year.

We are planning on using AVIF by default to serve images on SmashingMag, but we aren't quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don't really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.

We're lazy rendering some of the offset components of the page with content-visibility: auto . For example, the footer, the comments section, as well as the panels way below the first 1000px height threshold, are all rendered later after the visible portion of each page has been rendered.

Ne-am jucat puțin cu link rel="prefetch" și chiar link rel="prerender" (NoPush prefetch) unele părți ale paginii care este foarte probabil să fie folosite pentru navigare ulterioară - de exemplu, pentru a prelua în prealabil elementele pentru prima articole pe prima pagină (încă în discuție).

De asemenea, preîncărcăm imaginile autorului pentru a reduce cea mai mare vopsea de conținut și unele elemente cheie care sunt utilizate pe fiecare pagină, cum ar fi imaginile cu pisici dansante (pentru navigare) și umbra utilizate pentru toate imaginile de autor. Cu toate acestea, toate sunt preîncărcate numai dacă un cititor se întâmplă să fie pe un ecran mai mare (>800 px), deși încercăm să folosim Network Information API pentru a fi mai precis.

De asemenea, am redus dimensiunea CSS-ului complet și a tuturor fișierelor CSS esențiale prin eliminarea codului moștenit, refactorizarea unui număr de componente și eliminarea trucului de umbre a textului pe care îl folosim pentru a obține subliniere perfectă cu o combinație de text-decorare-sărire. -cerneală și text-decor-grosime (în sfârșit!).

Lucru De Terminat

Am petrecut o cantitate destul de importantă de timp lucrând la toate modificările minore și majore de pe site. Am observat îmbunătățiri destul de semnificative pe desktop și o creștere destul de vizibilă pe mobil. În momentul scrierii, articolele noastre au un scor mediu între 90 și 100 Lighthouse pe desktop și aproximativ 65-80 pe mobil .

Scorul Lighthouse pe desktop arată între 90 și 100
Scor de performanță pe desktop. Pagina de pornire este deja puternic optimizată. (Previzualizare mare)
Scorul Lighthouse la emisiunile mobile între 65 și 80
Pe mobil, nu ajungem aproape niciodată la un scor Lighthouse peste 85. Principalele probleme sunt încă Time to Interactive și Total Blocking Time. (Previzualizare mare)

Motivul pentru scorul slab pe dispozitivul mobil este în mod clar sărac pentru Interactive și sărac de blocare totală din cauza pornirii aplicației și a dimensiunii fișierului CSS complet. Deci mai este ceva de făcut acolo.

În ceea ce privește următorii pași, în prezent ne uităm la reducerea în continuare a dimensiunii CSS și, în mod specific, să-l descompunem în module, în mod similar cu JavaScript, încărcând unele părți ale CSS (de exemplu, checkout sau job board sau cărți/eBooks) numai atunci când Necesar.

De asemenea, explorăm opțiuni de experimente suplimentare de grupare pe mobil pentru a reduce impactul asupra performanței aplicației.js , deși pare să nu fie banal în acest moment. În cele din urmă, vom căuta alternative la soluția noastră promptă pentru cookie-uri, reconstruind containerele noastre cu CSS clamp() , înlocuind tehnica padding-bottom ratio cu aspect-ratio și analizând difuzarea a cât mai multe imagini posibil în AVIF.

Asta e, oameni buni!

Sperăm că acest mic studiu de caz vă va fi util și poate că există una sau două tehnici pe care ați putea să le aplicați imediat proiectului dvs. În cele din urmă, performanța se referă la o sumă a tuturor detaliilor fine care, atunci când sunt adunate, fac sau distrug experiența clientului tău.

Deși suntem foarte hotărâți să îmbunătățim performanța, lucrăm și la îmbunătățirea accesibilității și a conținutului site-ului. Așadar, dacă observați ceva care nu este în regulă sau orice am putea face pentru a îmbunătăți în continuare Smashing Magazine, vă rugăm să ne spuneți în comentariile acestui articol.

În cele din urmă, dacă doriți să fiți la curent cu articole precum acesta, vă rugăm să abonați-vă la buletinul nostru informativ prin e-mail pentru sfaturi web prietenoase, bunătăți, instrumente și articole și o selecție sezonieră de Smashing cats.