Jak poprawiliśmy wydajność SmashingMag

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ W tym artykule przyjrzymy się bliżej niektórym zmianom, które wprowadziliśmy w tej witrynie — działającej na JAMStack z React — w celu optymalizacji wydajności sieci i poprawy wskaźników podstawowych wskaźników internetowych. Z niektórymi błędami, które popełniliśmy, i niektórymi nieoczekiwanymi zmianami, które pomogły poprawić wszystkie wskaźniki na całej planszy.

Każda historia wydajności sieci jest podobna, prawda? Zawsze zaczyna się od długo oczekiwanego remontu strony internetowej. Dzień, w którym projekt, w pełni dopracowany i starannie zoptymalizowany, zostaje uruchomiony, zajmując wysokie miejsce i szybując powyżej wyników wydajności w Lighthouse i WebPageTest. W powietrzu panuje uroczystość i poczucie spełnienia – pięknie odzwierciedlone w retweetach, komentarzach, biuletynach i wątkach Slack.

Jednak w miarę upływu czasu emocje powoli zanikają i pojawiają się pilne poprawki, bardzo potrzebne funkcje i nowe wymagania biznesowe. I nagle, zanim się zorientujesz, baza kodu staje się nieco przeciążona i rozdrobniona . skrypty muszą ładować się nieco wcześniej, a nowa, dynamiczna zawartość trafia do DOM przez tylne drzwi skryptów stron trzecich i ich nieproszonych gości.

Byliśmy tam również w Smashing. Niewiele osób o tym wie, ale jesteśmy bardzo małym zespołem składającym się z około 12 osób, z których wielu pracuje w niepełnym wymiarze godzin, a większość z nich zwykle nosi wiele różnych kapeluszy danego dnia. Chociaż wydajność jest naszym celem od prawie dekady, tak naprawdę nigdy nie mieliśmy oddanego zespołu ds. wydajności.

Po ostatnim przeprojektowaniu pod koniec 2017 r. Ilya Pukhalski po stronie JavaScript (w niepełnym wymiarze godzin), Michael Riethmueller po stronie CSS (kilka godzin tygodniowo) i naprawdę grając w gry umysłowe z krytycznym CSS i próbując żonglować kilkoma zbyt wieloma rzeczami.

Zrzut ekranu źródeł wydajności pokazujący wyniki Lighthouse między 40 a 60
Od tego zaczęliśmy. Ponieważ wyniki Lighthouse wynoszą od 40 do 60, postanowiliśmy zająć się (po raz kolejny) wydajnością. (Źródło obrazu: Lighthouse Metrics) (duży podgląd)

Tak się złożyło, że straciliśmy kontrolę nad wydajnością w krzątaninie codziennej rutyny. Projektowaliśmy i budowaliśmy rzeczy, konfigurowaliśmy nowe produkty, refaktoryzowaliśmy komponenty i publikowaliśmy artykuły. Tak więc pod koniec 2020 roku sprawy wymknęły się nieco spod kontroli, a żółtawo-czerwone wyniki Lighthouse powoli pojawiają się na całej tablicy. Musieliśmy to naprawić.

tam byliśmy

Niektórzy z was mogą wiedzieć, że działamy na JAMStack, a wszystkie artykuły i strony są przechowywane jako pliki Markdown, pliki Sass skompilowane do CSS, JavaScript podzielony na kawałki za pomocą Webpack, a Hugo tworzy statyczne strony, które następnie obsługujemy bezpośrednio z Edge CDN. W 2017 r. zbudowaliśmy całą witrynę za pomocą Preact, ale w 2019 r. przenieśliśmy się do React — i używaj jej wraz z kilkoma interfejsami API do wyszukiwania, komentarzy, uwierzytelniania i realizacji transakcji.

Cała strona jest zbudowana z myślą o progresywnym ulepszeniu, co oznacza, że ​​Ty, drogi Czytelniku, możesz przeczytać w całości każdy artykuł Smashing bez konieczności uruchamiania aplikacji. Nie jest to zbyt zaskakujące — ostatecznie opublikowany artykuł nie zmienia się zbytnio na przestrzeni lat, podczas gdy elementy dynamiczne, takie jak uwierzytelnianie członkostwa i realizacja transakcji, wymagają uruchomienia aplikacji.

Cała kompilacja do wdrożenia około 2500 artykułów na żywo zajmuje obecnie około 6 minut . Sam proces budowania stał się z biegiem czasu dość bestią, z krytycznymi wstrzyknięciami CSS, dzieleniem kodu Webpack, dynamicznymi wstawkami reklam i paneli funkcji, (re)generacją RSS i ewentualnymi testami A/B na krawędzi.

Na początku 2020 roku rozpoczęliśmy dużą refaktoryzację komponentów układu CSS. Nigdy nie używaliśmy CSS-in-JS lub styled-components, ale zamiast tego dobry, stary system modułów Sass, oparty na komponentach, który zostałby skompilowany do CSS. W 2017 roku cały układ został zbudowany za pomocą Flexbox i przebudowany za pomocą CSS Grid i CSS Custom Properties w połowie 2019 roku. Jednak niektóre strony wymagały specjalnego potraktowania ze względu na nowe spoty reklamowe i nowe panele produktowe. Więc kiedy układ działał, nie działał zbyt dobrze i był dość trudny w utrzymaniu.

Dodatkowo nagłówek z główną nawigacją musiał się zmienić, aby pomieścić więcej elementów, które chcieliśmy wyświetlać dynamicznie. Ponadto chcieliśmy zrefaktoryzować niektóre często używane komponenty używane w witrynie, a zastosowany tam CSS również wymagał pewnych zmian — najbardziej zauważalnym winowajcą jest skrzynka z biuletynem. Zaczęliśmy od refaktoryzacji niektórych komponentów za pomocą CSS zorientowanego na użyteczność, ale nigdy nie doszliśmy do punktu, w którym był on konsekwentnie używany w całej witrynie.

Większym problemem był duży pakiet JavaScript , który — nic dziwnego — blokował główny wątek na setki milisekund. Duży pakiet JavaScript może wydawać się nie na miejscu w magazynie, który publikuje jedynie artykuły, ale w rzeczywistości za kulisami dzieje się wiele skryptów.

Posiadamy różne stany komponentów dla klientów uwierzytelnionych i nieuwierzytelnionych. Gdy się zalogujesz, chcemy pokazać wszystkie produkty w ostatecznej cenie, a gdy dodasz książkę do koszyka, chcemy, aby koszyk był dostępny po naciśnięciu przycisku — bez względu na to, na której stronie jesteś. Reklamy muszą pojawiać się szybko, bez powodowania uciążliwych zmian w układzie . To samo dotyczy natywnych paneli produktów, które wyróżniają nasze produkty. Plus pracownik usług, który buforuje wszystkie statyczne zasoby i udostępnia je do powtórnych wyświetleń, wraz z buforowanymi wersjami artykułów, które czytelnik już odwiedził.

Tak więc całe to pisanie skryptów musiało nastąpić w pewnym momencie, a to wyczerpywało wrażenia z czytania, mimo że scenariusz pojawiał się dość późno. Szczerze mówiąc, żmudnie pracowaliśmy nad stroną i nowymi komponentami, nie bacznie przyglądając się wydajności (i mieliśmy kilka innych rzeczy, o których warto pamiętać na 2020 rok). Punkt zwrotny nadszedł niespodziewanie. Harry Roberts prowadził z nami swoje (doskonałe) Web Performance Masterclass jako warsztat online i przez cały czas używał Smashingu jako przykładu, podkreślając problemy, które mieliśmy i proponując rozwiązania tych problemów wraz z przydatnymi narzędziami i wskazówkami.

Przez cały warsztat pilnie robiłem notatki i przeglądałem bazę kodów. W czasie warsztatów nasze wyniki w Lighthouse wynosiły 60–68 na stronie głównej i około 40–60 na stronach artykułów — i oczywiście gorsze na urządzeniach mobilnych. Po warsztatach zabraliśmy się do pracy.

Identyfikacja wąskich gardeł

Często polegamy na konkretnych partyturach, aby zrozumieć, jak dobrze sobie radzimy, ale zbyt często pojedyncze partytury nie dają pełnego obrazu. Jak elokwentnie zauważył David East w swoim artykule, wydajność sieci nie jest pojedynczą wartością; to dystrybucja. Nawet jeśli środowisko internetowe jest mocno i dokładnie zoptymalizowane pod kątem wszechstronnej wydajności, nie może być po prostu szybkie. Może to być szybkie dla niektórych odwiedzających, ale ostatecznie będzie również wolniejsze (lub wolniejsze) dla niektórych innych.

Powodów tego jest wiele, ale najważniejszą z nich jest ogromna różnica w warunkach sieciowych i sprzęcie urządzeń na całym świecie. Najczęściej nie możemy tak naprawdę wpłynąć na te rzeczy, więc musimy upewnić się, że nasze doświadczenie je uwzględnia.

W istocie naszym zadaniem jest zatem zwiększenie proporcji zgryźliwych doświadczeń i zmniejszenie proporcji powolnych doświadczeń. Ale do tego musimy uzyskać właściwy obraz tego, czym właściwie jest dystrybucja. Teraz narzędzia analityczne i narzędzia do monitorowania wydajności dostarczą te dane w razie potrzeby, ale przyjrzeliśmy się konkretnie CrUX, raportowi z doświadczeń użytkowników Chrome. CrUX generuje przegląd rozkładu wydajności w czasie, z ruchem zbieranym od użytkowników Chrome. Wiele z tych danych jest związanych z Core Web Vitals, które Google ogłosił w 2020 roku i które również przyczyniają się i są ujawniane w Lighthouse.

Największe statystyki Contentful Paint (LCP) pokazujące ogromny spadek wydajności między majem a wrześniem 2020 r.
Rozkład wydajności dla największej zawartości treści w 2020 roku. Między majem a wrześniem wydajność znacznie spadła. Dane z CrUX. (duży podgląd)

Zauważyliśmy, że w całym roku nasze wyniki gwałtownie spadły w ciągu roku, ze szczególnymi spadkami w sierpniu i wrześniu. Kiedy zobaczyliśmy te wykresy, moglibyśmy spojrzeć wstecz na niektóre PR, które wtedy przesunęliśmy na żywo, aby zbadać, co się właściwie wydarzyło.

Nie zajęło nam trochę czasu, zanim zorientowaliśmy się, że właśnie w tym czasie uruchomiliśmy nowy pasek nawigacyjny na żywo. Ten pasek nawigacyjny — używany na wszystkich stronach — opierał się na JavaScript, aby wyświetlać elementy nawigacji w menu po dotknięciu lub kliknięciu, ale jego fragment JavaScript był w rzeczywistości dołączony do pakietu app.js. Aby ulepszyć Time To Interactive, postanowiliśmy wyodrębnić skrypt nawigacyjny z pakietu i udostępnić go w trybie inline.

Mniej więcej w tym samym czasie przeszliśmy z (nieaktualnego) ręcznie tworzonego krytycznego pliku CSS na zautomatyzowany system, który generował krytyczny CSS dla każdego szablonu — strony głównej, artykułu, strony produktu, wydarzenia, tablicy ogłoszeń itd. — oraz wbudowanego krytycznego CSS podczas czas kompilacji. Jednak tak naprawdę nie zdawaliśmy sobie sprawy, o ile cięższy jest automatycznie generowany krytyczny CSS. Musieliśmy to zbadać bardziej szczegółowo.

Mniej więcej w tym samym czasie dostosowywaliśmy ładowanie czcionek internetowych , próbując bardziej agresywnie wprowadzać czcionki internetowe za pomocą wskazówek dotyczących zasobów, takich jak wstępne ładowanie. Wydaje się jednak, że jest to sprzeczne z naszymi wysiłkami na rzecz wydajności, ponieważ czcionki internetowe opóźniały renderowanie treści, mając nadrzędne priorytety w stosunku do pełnego pliku CSS.

Obecnie jedną z najczęstszych przyczyn regresji jest wysoki koszt JavaScript, więc przyjrzeliśmy się również analizatorowi pakietów Webpack Bundle i mapie żądań Simona Hearne'a, aby uzyskać wizualny obraz naszych zależności JavaScript. Na początku wyglądał całkiem zdrowo.

Wizualna mapa myśli zależności JavaScript
Nic przełomowego: początkowo mapa żądań nie wydawała się przesadna. (duży podgląd)

Kilka próśb napłynęło do CDN, usługi Cookiebot, usługi wyrażania zgody na pliki cookie, Google Analytics oraz naszych wewnętrznych usług do obsługi paneli produktów i niestandardowych reklam. Nie wydawało się, że istnieje wiele wąskich gardeł — dopóki nie przyjrzeliśmy się nieco bliżej.

W pracy związanej z wydajnością często przygląda się wydajności niektórych krytycznych stron — najprawdopodobniej strony głównej i najprawdopodobniej kilku stron z artykułami/produktami. Jednak chociaż istnieje tylko jedna strona główna, może być wiele różnych stron produktów, więc musimy wybrać te, które są reprezentatywne dla naszych odbiorców.

W rzeczywistości, ponieważ publikujemy na SmashingMag sporo artykułów z dużą ilością kodu i projektów, przez lata zgromadziliśmy dosłownie tysiące artykułów, które zawierały ciężkie GIF-y, fragmenty kodu z wyróżnieniem składni, osadzania CodePen, wideo/audio osadzone i zagnieżdżone wątki niekończących się komentarzy.

Zebrane razem, wiele z nich powodowało jedynie eksplozję rozmiaru DOM wraz z nadmierną pracą głównych wątków — spowalniając działanie na tysiącach stron. Nie wspominając już o tym, że po wprowadzeniu reklam niektóre elementy DOM zostały wstrzyknięte na późnym etapie cyklu życia strony, powodując kaskadę przeliczania stylów i odmalowań — również kosztownych zadań, które mogą generować długie zadania.

Wszystko to nie pojawiło się na mapie, którą wygenerowaliśmy dla dość lekkiej strony artykułu na powyższym wykresie. Wybraliśmy więc najcięższe strony, jakie mieliśmy — wszechmocną stronę główną, najdłuższą, z wieloma osadzonymi filmami wideo i tę z wieloma osadzonymi elementami CodePen — i postanowiliśmy zoptymalizować je tak bardzo, jak tylko mogliśmy. W końcu, jeśli są szybkie, to strony z jednym osadzonym CodePenem również powinny być szybsze.

Mając na uwadze te strony, mapa wyglądała trochę inaczej. Zwróć uwagę na ogromną grubą linię prowadzącą do odtwarzacza Vimeo i Vimeo CDN, z 78 żądaniami pochodzącymi z artykułu Smashing.

Wizualna mapa myśli pokazująca problemy z wydajnością, szczególnie w artykułach, w których wykorzystano wiele filmów wideo i/lub osadzonych filmów
Na niektórych stronach artykułów wykres wyglądał inaczej. Zwłaszcza w przypadku dużej liczby osadzonych kodów lub filmów wydajność znacznie spadła. Niestety wiele naszych artykułów je zawiera. (duży podgląd)

Aby zbadać wpływ na główny wątek, zagłębiliśmy się w panel Wydajność w DevTools. Dokładniej, szukaliśmy zadań, które trwają dłużej niż 50 ms (podświetlonych czerwonym prostokątem w prawym górnym rogu) oraz zadań, które zawierają style przeliczania (fioletowy pasek). Pierwsza wskazywałaby na drogie wykonanie JavaScriptu, podczas gdy druga ujawniłaby unieważnienia stylów spowodowane dynamicznym wstrzykiwaniem treści w DOM i nieoptymalnym CSS. To dało nam kilka praktycznych wskazówek, od czego zacząć. Na przykład szybko odkryliśmy, że ładowanie naszych czcionek internetowych wiązało się ze znacznym kosztem odświeżenia, podczas gdy fragmenty JavaScript były nadal wystarczająco ciężkie, aby zablokować główny wątek.

Zrzut ekranu panelu wydajności w DevTools pokazujący fragmenty JavaScript, które wciąż były wystarczająco ciężkie, aby zablokować główny wątek
Badanie panelu Wydajność w DevTools. Było kilka długich zadań, trwających ponad 50 ms i blokujących główny wątek. (duży podgląd)

Jako punkt odniesienia przyjrzeliśmy się bardzo uważnie Core Web Vitals, starając się zapewnić dobre wyniki we wszystkich z nich. Zdecydowaliśmy się skupić w szczególności na wolnych urządzeniach mobilnych — z wolnym 3G, 400 ms RTT i prędkością transferu 400 kb/s, po prostu po to, by być po pesymistycznej stronie rzeczy. Nic więc dziwnego, że Lighthouse również nie był zadowolony z naszej strony, dostarczając w pełni solidne czerwone oceny dla najcięższych artykułów i niestrudzenie narzekając na nieużywany JavaScript, CSS, obrazy pozaekranowe i ich rozmiary.

Zrzut ekranu danych Lighthouse pokazujących możliwości i szacunkowe oszczędności
Lighthouse też nie był szczególnie zadowolony z wydajności niektórych stron. To ten z dużą ilością osadzonych filmów. (duży podgląd)

Gdy mieliśmy już przed sobą trochę danych, mogliśmy skupić się na optymalizacji trzech najcięższych stron z artykułami, skupiając się na krytycznym (i niekrytycznym) CSS, pakiecie JavaScript, długich zadaniach, ładowaniu czcionek internetowych, zmianach układu i innych -osadza. Później zmienimy również bazę kodu, aby usunąć starszy kod i korzystać z nowych nowoczesnych funkcji przeglądarki. Wydawało się, że czeka nas dużo pracy i rzeczywiście przez nadchodzące miesiące byliśmy bardzo zajęci.

Poprawa kolejności aktywów w <head>

Jak na ironię, pierwsza rzecz, której się przyjrzeliśmy, nie była nawet ściśle związana ze wszystkimi zadaniami, które zidentyfikowaliśmy powyżej. Podczas warsztatów wydajnościowych Harry spędził znaczną ilość czasu wyjaśniając kolejność zasobów w <head> każdej strony, zwracając uwagę, że szybkie dostarczenie krytycznej treści oznacza bycie bardzo strategicznym i uważnym na to, jak zasoby są uporządkowane w kodzie źródłowym .

Teraz nie powinno być wielkim odkryciem, że krytyczny CSS jest korzystny dla wydajności sieci. Jednak trochę zaskoczyło , jak bardzo różni się kolejność wszystkich innych zasobów — wskazówek dotyczących zasobów, wstępnego ładowania czcionek internetowych, skryptów synchronicznych i asynchronicznych, pełnego CSS i metadanych.

Odwróciliśmy cały <head> do góry nogami, umieszczając krytyczny kod CSS przed wszystkimi asynchronicznymi skryptami i wszystkimi wstępnie załadowanymi zasobami, takimi jak czcionki, obrazy itp. Podzieliliśmy zasoby, z którymi będziemy się wstępnie łączyć lub wstępnie ładować według szablonu i typ pliku, dzięki czemu krytyczne obrazy, podświetlanie składni i umieszczanie filmów wideo będą wymagane wcześnie tylko w przypadku określonego rodzaju artykułów i stron.

Ogólnie rzecz biorąc, starannie zaaranżowaliśmy kolejność w <head> , zmniejszyliśmy liczbę wstępnie załadowanych zasobów, które rywalizowały o przepustowość, i skupiliśmy się na poprawnym uzyskaniu krytycznego kodu CSS. Jeśli chcesz głębiej zagłębić się w niektóre krytyczne kwestie związane z zamówieniem <head> , Harry podkreśla je w artykule na temat CSS i wydajności sieci. Już sama ta zmiana przyniosła nam około 3-4 punktów punktowych w latarni morskiej.

Przejście od zautomatyzowanego krytycznego CSS z powrotem do ręcznego krytycznego CSS

Przesuwanie tagów <head> było jednak prostą częścią całej historii. Trudniejszym było generowanie i zarządzanie krytycznymi plikami CSS. W 2017 r. ręcznie stworzyliśmy krytyczny kod CSS dla każdego szablonu, zbierając wszystkie style wymagane do renderowania pierwszych 1000 pikseli wysokości na wszystkich szerokościach ekranu. Było to oczywiście kłopotliwe i nieco mało inspirujące zadanie, nie wspominając o problemach z konserwacją całej rodziny krytycznych plików CSS i pełnego pliku CSS.

Dlatego przyjrzeliśmy się opcjom automatyzacji tego procesu w ramach procedury kompilacji. Tak naprawdę nie brakowało dostępnych narzędzi, więc przetestowaliśmy kilka i postanowiliśmy przeprowadzić kilka testów. Udało nam się je dość szybko skonfigurować i uruchomić. Wynik wydawał się być wystarczająco dobry do zautomatyzowanego procesu, więc po kilku poprawkach konfiguracji podłączyliśmy go i wprowadziliśmy do produkcji. Stało się to w okolicach lipca-sierpnia zeszłego roku, co jest ładnie zobrazowane w gwałtownym wzroście i spadku wydajności w powyższych danych CrUX. Ciągle krążyliśmy z konfiguracją, często mając problemy z prostymi rzeczami, takimi jak dodawanie określonych stylów lub usuwanie innych. Np. style monitów o zgodę na pliki cookie, które tak naprawdę nie są zawarte na stronie, chyba że skrypt cookie został zainicjowany.

W październiku wprowadziliśmy kilka poważnych zmian w układzie strony, a przyglądając się krytycznym CSS, ponownie napotkaliśmy dokładnie te same problemy — wygenerowany wynik był dość szczegółowy i nie do końca był tym, czego chcieliśmy . W ramach eksperymentu pod koniec października wszyscy połączyliśmy nasze mocne strony, aby ponownie przyjrzeć się naszemu krytycznemu podejściu do CSS i zbadać, o ile mniejszy byłby ręcznie wykonany krytyczny CSS . Wzięliśmy głęboki oddech i spędziliśmy kilka dni wokół narzędzia do pokrycia kodu na kluczowych stronach. Pogrupowaliśmy ręcznie reguły CSS i usunęliśmy duplikaty i starszy kod w obu miejscach — krytyczny CSS i główny CSS. Było to rzeczywiście bardzo potrzebne porządki, ponieważ wiele stylów, które zostały napisane w latach 2017–2018, z biegiem lat stało się przestarzałych.

W rezultacie otrzymaliśmy trzy ręcznie wykonane krytyczne pliki CSS i trzy kolejne pliki, nad którymi obecnie trwają prace:

  • krytyczna-strona-domowa-manual.css (8,2 KB, Brotlified)
  • krytyczny-artykuł-manual.css (8 KB, Brotlified)
  • krytyczne-artykuly-manual.css (6 KB, Brotlified)
  • krytyczne-booki-manual.css ( praca do wykonania )
  • krytyczne-zdarzenia-manual.css ( praca do wykonania )
  • krytyczne-job-board-manual.css ( praca do wykonania )

Pliki są umieszczone w nagłówku każdego szablonu, a obecnie są zduplikowane w monolitycznym pakiecie CSS, który zawiera wszystko, co kiedykolwiek było używane (lub nie jest już używane) w witrynie. W tej chwili zastanawiamy się nad rozbiciem pełnego pakietu CSS na kilka pakietów CSS, tak aby czytelnik magazynu nie pobierał stylów z tablicy ogłoszeń lub stron książki, ale po dotarciu do tych stron otrzyma szybkie renderowanie z krytycznym CSS i pobierz resztę CSS dla tej strony asynchronicznie — tylko na tej stronie.

Trzeba przyznać, że ręcznie wykonane krytyczne pliki CSS nie były dużo mniejsze: zmniejszyliśmy rozmiar krytycznych plików CSS o około 14% . Zawierały jednak wszystko, czego potrzebowaliśmy, we właściwej kolejności, od góry do końca, bez duplikatów i nadrzędnych stylów. Wydawało się, że jest to krok we właściwym kierunku i dało nam doładowanie Lighthouse o kolejne 3-4 punkty. Robiliśmy postępy.

Zmiana ładowania czcionek internetowych

Z font-display na wyciągnięcie ręki, ładowanie czcionek wydaje się być problemem w przeszłości. Niestety w naszym przypadku tak nie jest. Wy, drodzy czytelnicy, odwiedzacie wiele artykułów w Smashing Magazine. Często wracasz również na stronę, aby przeczytać kolejny artykuł — być może kilka godzin lub dni później, a może tydzień później. Jednym z problemów, które mieliśmy z font-display w witrynie, było to, że dla czytelników, którzy często przechodzili między artykułami, zauważyliśmy wiele błysków między czcionką zastępczą a czcionką internetową (co normalnie nie powinno mieć miejsca, ponieważ czcionki byłyby prawidłowo buforowane).

To nie było przyzwoite wrażenia użytkownika, więc przyjrzeliśmy się opcjom. W Smashing używamy dwóch głównych krojów pisma — Mija do nagłówków i Elena do kopiowania treści. Mija występuje w dwóch wagach (zwykłej i pogrubionej), podczas gdy Elena występuje w trzech wagach (zwykłej, kursywy i pogrubionej). Odrzuciliśmy Bold Italic Eleny lata temu podczas przebudowy, tylko dlatego, że użyliśmy go na zaledwie kilku stronach. Inne czcionki są podzbiorów, usuwając nieużywane znaki i zakresy Unicode.

Nasze artykuły są w większości osadzone w tekście, więc odkryliśmy, że przez większość czasu w witrynie największą treścią jest albo pierwszy akapit tekstu w artykule, albo zdjęcie autora. Oznacza to, że musimy zadbać o to, aby pierwszy akapit pojawił się szybko w czcionce zastępczej, jednocześnie z wdziękiem przechodząc na czcionkę internetową z minimalnymi przepływami.

Przyjrzyj się bliżej początkowemu ładowaniu strony głównej (zwolnione trzy razy):

Przy opracowywaniu rozwiązania mieliśmy cztery główne cele:

  1. Przy pierwszej wizycie natychmiast wyrenderuj tekst czcionką zastępczą;
  2. Dopasuj metryki czcionek zastępczych i internetowych, aby zminimalizować przesunięcia układu;
  3. Załaduj wszystkie czcionki internetowe asynchronicznie i zastosuj je wszystkie naraz (maks. 1 ponowne wlanie);
  4. Podczas kolejnych wizyt wyrenderuj cały tekst bezpośrednio czcionkami internetowymi (bez flashowania lub ponownego wlewania tekstu).

Początkowo próbowaliśmy użyć wyświetlania czcionek: zamiana na font-face . Wydawało się to najprostszą opcją, jednak, jak wspomniano powyżej, niektórzy czytelnicy odwiedzą wiele stron, więc skończyło się na migotaniu sześciu czcionek, które renderowaliśmy w całej witrynie. Ponadto przy samym wyświetlaniu czcionek nie mogliśmy grupować żądań ani odmalowań.

Innym pomysłem było wyrenderowanie wszystkiego w czcionce rezerwowej podczas pierwszej wizyty , a następnie asynchroniczne żądanie i buforowanie wszystkich czcionek, a dopiero przy kolejnych wizytach dostarczanie czcionek internetowych bezpośrednio z pamięci podręcznej. Problem z tym podejściem polegał na tym, że wielu czytelników pochodzi z wyszukiwarek i przynajmniej niektórzy z nich zobaczą tylko tę jedną stronę — a nie chcieliśmy renderować artykułu samą czcionką systemową.

Więc co jest wtedy?

Od 2017 roku używamy podejścia Two-Stage-Render do ładowania czcionek internetowych, które zasadniczo opisuje dwa etapy renderowania: jeden z minimalnym podzbiorem czcionek internetowych, a drugi z pełną rodziną grubości czcionek. Kiedyś stworzyliśmy minimalne podzbiory Miji Bold i Eleny Regular, które były najczęściej używanymi ciężarkami na stronie. Oba podzbiory zawierają tylko znaki łacińskie, interpunkcję, cyfry i kilka znaków specjalnych. Te czcionki ( ElenaInitial.woff2 i MijaInitial.woff2 ) były bardzo małe — często miały około 10–15 KB. Obsługujemy je w pierwszym etapie renderowania czcionek, wyświetlając w tych dwóch czcionkach całą stronę.

CLS spowodowany migotaniem czcionek internetowych
CLS spowodowany migotaniem czcionek internetowych (cienie pod obrazami autora poruszają się z powodu zmiany czcionki). Wygenerowano za pomocą generatora GIF z przesunięciem układu. (duży podgląd)

Robimy to za pomocą interfejsu API ładowania czcionek, który dostarcza nam informacji o tym, które czcionki zostały pomyślnie załadowane, a które jeszcze nie. Za kulisami dzieje się to poprzez dodanie klasy .wf-loaded-stage1 do body , ze stylami renderującymi treść w tych czcionkach:

 .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; }

Ponieważ pliki czcionek są dość małe, miejmy nadzieję, że dość szybko przejdą przez sieć. Następnie, gdy czytelnik może zacząć czytać artykuł, ładujemy asynchronicznie pełną wagę czcionek i dodajemy .wf-loaded-stage2 do body :

 .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; }

Tak więc podczas ładowania strony czytelnicy najpierw szybko otrzymają mały podzbiór czcionki internetowej, a następnie przełączymy się na pełną rodzinę czcionek. Teraz domyślnie przełączanie się między czcionkami rezerwowymi a czcionkami internetowymi odbywa się losowo, w zależności od tego, co najpierw pojawi się w sieci. To może wydawać się dość uciążliwe, gdy zacząłeś czytać artykuł. Dlatego zamiast pozostawiać przeglądarce decyzję, kiedy zmienić czcionkę, grupujemy odświeżania , zmniejszając do minimum wpływ ponownego wlewania tekstu.

 /* 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}`); }); }

Co jednak, jeśli pierwszy mały podzbiór czcionek nie przechodzi szybko przez sieć? Zauważyliśmy, że wydaje się, że dzieje się to częściej, niż byśmy chcieli. W takim przypadku, po upływie limitu czasu 3 s, nowoczesne przeglądarki wracają do czcionki systemowej (w naszym stosie czcionek byłaby to Arial), a następnie przełączają się na ElenaInitial lub MijaInitial , aby później przejść odpowiednio na pełną Elenę lub Mija . To spowodowało, że podczas naszej degustacji było trochę za dużo błysków. Początkowo myśleliśmy o usunięciu renderowania pierwszego etapu tylko dla wolnych sieci (poprzez Network Information API), ale potem zdecydowaliśmy się całkowicie go usunąć.

Tak więc w październiku całkowicie usunęliśmy podzbiory wraz z etapem pośrednim. Za każdym razem, gdy wszystkie grubości czcionek Elena i Mija zostaną pomyślnie pobrane przez klienta i gotowe do zastosowania, inicjujemy etap 2 i odmalowujemy wszystko na raz. Aby zmiany przepływu były jeszcze mniej zauważalne, poświęciliśmy trochę czasu na dopasowywanie czcionek zastępczych i czcionek internetowych . Oznaczało to głównie zastosowanie nieco innych rozmiarów czcionek i wysokości linii dla elementów namalowanych w pierwszej widocznej części strony.

W tym celu użyliśmy font-style-matcher i (ahem, ahem) kilku magicznych liczb. Jest to również powód, dla którego początkowo wybraliśmy -apple-system i Arial jako globalne czcionki zastępcze; San Francisco (wyrenderowane przez -apple-system ) wydawało się nieco ładniejsze niż Arial, ale jeśli nie jest dostępne, zdecydowaliśmy się użyć Arial tylko dlatego, że jest szeroko rozpowszechniony w większości systemów operacyjnych.

W CSS wyglądałoby to tak:

 .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 */ }

Działało to całkiem dobrze. Wyświetlamy tekst natychmiast, a czcionki internetowe pojawiają się na ekranie zgrupowane, co idealnie powoduje dokładnie jeden przepływ w pierwszym widoku i całkowity brak zmian w kolejnych widokach.

Po pobraniu czcionek przechowujemy je w pamięci podręcznej Service Workera. Przy kolejnych wizytach najpierw sprawdzamy, czy czcionki są już w pamięci podręcznej. Jeśli tak, pobieramy je z pamięci podręcznej service workera i natychmiast stosujemy. A jeśli nie, zaczynamy od początku z fallback-web-font-switcheroo .

To rozwiązanie ograniczyło liczbę ponownych przepływów do minimum (jednego) na stosunkowo szybkich połączeniach, jednocześnie utrzymując czcionki na stałe i niezawodnie w pamięci podręcznej. Mamy szczerą nadzieję, że w przyszłości magiczne liczby zastąpimy f-modami. Może Zach Leatherman byłby dumny.

Identyfikacja i rozbicie monolitycznego JS

Kiedy studiowaliśmy główny wątek w panelu Wydajność DevTools, dokładnie wiedzieliśmy, co musimy zrobić. Było osiem długich zadań, które trwały od 70 do 580 ms, blokując interfejs i uniemożliwiając jego reakcję. Generalnie były to skrypty kosztujące najwięcej:

  • uc.js , skrypt podpowiedzi do plików cookie (70 ms)
  • przeliczenia stylów spowodowane przychodzącym plikiem full.css (176ms) (krytyczny CSS nie zawiera stylów poniżej 1000px we wszystkich oknach ekranu)
  • skrypty reklamowe uruchamiane na zdarzeniu load do zarządzania panelami, koszykiem itp. + przeliczenia stylów (276ms)
  • przełączanie czcionek internetowych, przeliczanie stylu (290ms)
  • ocena app.js (580 ms)

Skupiliśmy się na tych, które były najbardziej szkodliwe w pierwszej kolejności — niejako najdłuższych Długich Zadaniach.

Zrzut ekranu zaczerpnięty z DevTools, pokazujący walidacje stylów na pierwszej stronie rozbijającego czasopisma
Na dole Devtools pokazuje unieważnienia stylów — zmiana czcionki wpłynęła na 549 elementów, które trzeba było odmalować. Nie wspominając o zmianach w układzie, które to powodowało. (duży podgląd)

Pierwszy z nich miał miejsce z powodu kosztownych przeliczeń layoutu spowodowanych zmianą fontów (z zastępczej na web font), powodując ponad 290ms dodatkowej pracy (na szybkim laptopie i szybkim połączeniu). Usuwając pierwszy etap z samego ładowania czcionek, byliśmy w stanie zyskać około 80 ms z powrotem. Nie było to jednak wystarczająco dobre, ponieważ były znacznie poza budżetem 50 ms. Więc zaczęliśmy kopać głębiej.

Głównym powodem, dla którego dokonano ponownych obliczeń, były jedynie ogromne różnice między czcionkami zastępczymi a czcionkami internetowymi. Dopasowując wysokość i rozmiary linii dla czcionek zastępczych i czcionek internetowych , byliśmy w stanie uniknąć wielu sytuacji, w których wiersz tekstu zawijał się w nowym wierszu w czcionce zastępczej, ale potem był nieco mniejszy i mieścił się w poprzednim wierszu. powodując duże zmiany w geometrii całej strony, a co za tym idzie masowe przesunięcia układu. Bawiliśmy się również letter-spacing word-spacing , ale nie przyniosło to dobrych rezultatów.

Dzięki tym zmianom byliśmy w stanie skrócić o kolejne 50-80 ms, ale nie byliśmy w stanie zredukować go poniżej 120 ms bez wyświetlania zawartości czcionką zastępczą, a następnie wyświetlania zawartości czcionką internetową. Oczywiście powinno to mieć ogromny wpływ tylko na odwiedzających po raz pierwszy, ponieważ kolejne odsłony byłyby renderowane z czcionkami pobranymi bezpośrednio z pamięci podręcznej Service Worker, bez kosztownych zmian przepływu spowodowanych przełączaniem czcionek.

Nawiasem mówiąc, dość ważne jest, aby zauważyć, że w naszym przypadku zauważyliśmy, że większość długich zadań nie była spowodowana masowym JavaScriptem, ale przeliczaniem układu i parsowaniem CSS, co oznaczało, że musieliśmy zrobić trochę 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. (duży podgląd)

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. (duży podgląd)

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. (duży podgląd)

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. (duży podgląd)

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. (duży podgląd)

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.

Pobawiliśmy się trochę z link rel="prefetch" , a nawet link rel="prerender" (NoPush prefetch) niektórych części strony, które z dużym prawdopodobieństwem będą używane do dalszej nawigacji — na przykład do wstępnego pobierania zasobów dla pierwszego artykuły na pierwszej stronie (wciąż dyskutowane).

Wstępnie ładujemy również obrazy autora, aby zmniejszyć największą zawartość treści, a także niektóre kluczowe zasoby używane na każdej stronie, takie jak obrazy tańczącego kota (do nawigacji) i cień używany we wszystkich obrazach autora. Jednak wszystkie z nich są wstępnie ładowane tylko wtedy, gdy czytnik znajduje się na większym ekranie (>800px), chociaż zamiast tego szukamy użycia interfejsu Network Information API, aby być bardziej dokładnym.

Zmniejszyliśmy również rozmiar pełnego CSS i wszystkich krytycznych plików CSS, usuwając stary kod, refaktoryzując szereg komponentów i usuwając sztuczkę z cieniami tekstu , której używaliśmy do uzyskania doskonałych podkreśleń z kombinacją text-decoration-skip -grubość -atramentu i -dekoracji-tekstu (nareszcie!).

Praca do skończenia

Spędziliśmy dość dużo czasu pracując nad wszystkimi drobnymi i większymi zmianami na stronie. Zauważyliśmy dość znaczną poprawę na komputerach stacjonarnych i dość zauważalny wzrost na urządzeniach mobilnych. W chwili pisania tego artykułu nasze artykuły uzyskują średnio od 90 do 100 punktów Lighthouse na komputerach i około 65-80 na urządzeniach mobilnych .

Wynik Lighthouse na komputerach pokazuje od 90 do 100
Wynik wydajności na komputerze. Strona główna jest już mocno zoptymalizowana. (duży podgląd)
Wynik latarni morskiej w programach mobilnych między 65 a 80
Na urządzeniach mobilnych prawie nigdy nie osiągamy wyniku Lighthouse powyżej 85. Głównymi problemami są nadal czas na interaktywność i całkowity czas blokowania. (duży podgląd)

Powodem słabego wyniku na urządzeniach mobilnych jest wyraźnie słaby czas na interaktywność i słaby całkowity czas blokowania ze względu na uruchamianie aplikacji i rozmiar pełnego pliku CSS. Więc jest jeszcze trochę pracy do zrobienia.

Jeśli chodzi o kolejne kroki, obecnie szukamy dalszego zmniejszenia rozmiaru CSS , a konkretnie rozbicia go na moduły, podobnie jak JavaScript, ładując niektóre części CSS (np. kasa lub tablica ogłoszeń lub książki/eBooki) tylko wtedy, gdy potrzebne.

Badamy również opcje dalszych eksperymentów w zakresie tworzenia pakietów na urządzeniach mobilnych, aby zmniejszyć wpływ app.js na wydajność, chociaż w tej chwili wydaje się to nietrywialne. Na koniec przyjrzymy się alternatywom dla naszego rozwiązania monitującego o ciasteczka, przebudowując nasze kontenery za pomocą CSS clamp() , zastępując technikę współczynnika dopełniania dolnego współczynnikiem aspect-ratio i starając się wyświetlać jak najwięcej obrazów w AVIF.

To wszystko, ludzie!

Miejmy nadzieję, że to małe studium przypadku będzie dla Ciebie przydatne i być może istnieje jedna lub dwie techniki, które możesz od razu zastosować w swoim projekcie. Ostatecznie wydajność to suma wszystkich drobnych szczegółów, które po zsumowaniu tworzą lub psują wrażenia klienta.

Chociaż jesteśmy bardzo zaangażowani w poprawę wydajności, pracujemy również nad poprawą dostępności i zawartości witryny. Więc jeśli zauważysz coś, co nie jest do końca właściwe lub cokolwiek, co moglibyśmy zrobić, aby jeszcze bardziej ulepszyć Smashing Magazine, daj nam znać w komentarzach do tego artykułu.

Na koniec, jeśli chcesz być na bieżąco z artykułami takimi jak ten, zasubskrybuj nasz biuletyn e-mailowy, aby uzyskać przyjazne wskazówki internetowe, gadżety, narzędzia i artykuły oraz sezonową selekcję kotów Smashing.