Wróć do strony głównej
Angular

Jak Zoptymalizować Bundle Size Angulara

Bundle size twojej aplikacji znacząco wpływa na doświadczenia użytkownika. Dzisiaj, z tak wieloma bibliotekami dostępnymi w npm, jest łatwiej niż kiedykolwiek zrujnować wydajność aplikacji poprzez zwiększenie rozmiaru bundle. W tym artykule poruszę: znaczenie rozmiaru paczki, jak prawidłowo go zmierzyć oraz popularne strategie optymalizacji.

Czym jest Bundle Size?

Bundle size odnosi się do całkowitego skompilowanego rozmiaru kodu twojej aplikacji. Komponenty, style, biblioteki, zależności — wszystkie te elementy mają wpływ na twoją aplikację. Przy dodawaniu kolejnych funkcjonalności do aplikacji, bundle size będzie odpowiednio wzrastał. Po osiągnięciu pewnego punktu, będzie wymagać optymalizacji.

Dlaczego Bundle Size jest Ważny?

Zmniejszenie rozmiaru paczki twojej aplikacji może znacząco zmniejszyć czas jej ładowania. Mniejsza aplikacja oznacza szybsze prędkości ładowania. Jest to kluczowe szczególnie dla stron typu landing page, stron e-commerce i w wielu innych przypadkach. Dodatkowo bundle size, ma to wpływ na SEO twojej strony internetowej.

Wpływ na biznes również jest ogromny. Case study Google’a wskazuje na 32% wzrost prawdopodobieństwa opuszczenia strony, gdy czas ładowania strony wzrasta z 1 sekundy do 3 sekund. Podobnie, BBC zgłosiło 10% wzrost prawdopodobieństwa odejścia użytkownika za każdą dodatkową sekundę czasu ładowania.

Wniosek jest oczywisty: minimalny rozmiar bundle jest kluczowy w zatrzymaniu nowych użytkowników, zwłaszcza na wolniejszych sieciach.

Jak Analizować Rozmiar Bundle?

Rozmiar paczki można analizować za pomocą analizatorów rozmiaru paczki — narzędzi, które generują wykresy lub raporty szczegółowo opisujące wpływ różnych plików na twoją aplikację.

Oto kilka popularnych analizatorów:

Lighthouse Treemap (development)

To narzędzie jest zintegrowane z przeglądarkami opartymi na Chromium. Lighthouse Treemap jest przyjaznym dla użytkownika, nie wymaga zewnętrznych zależności w twojej aplikacji narzędziem. Wyróżniającą cechą tego narzędzia jest jego zdolność do wyświetlania ilości JavaScriptu, który pozostaje niewykorzystany, co czyni go idealnym narzędziem do analizy początkowego rozmiaru paczki na serwerze developerskim.

Webpack-bundle-analyzer (production)

To biblioteka npm wizualizująca rozmiar twojego bundle webpacka na interaktywnej, powiększalnej treemapie. Zamieściłem te narzędzie w artykule, ponieważ nie jest to sugerowane narzędzie do analizy rozmiaru bundle w aplikacjach angularowych. Webpack-bundle-analyzer, choć użyteczny i niezmiernie popularny, ma kilka wad: może zwiększyć czas budowania aplikacji, może być skomplikowany w konfiguracji i nie jest perfekcyjnie dokładny. Dla głębszego zrozumienia, polecam sprawdzić tweeta Minko, tech lead’a Angulara, lub przeczytać te issue na Githubie.

Source-map-explorer (production)

Biblioteka npm, source-map-explorer analizuje rozmiar paczki za pomocą map źródłowych, mapując bajty w zminifikowanym kodzie z powrotem do ich plików źródłowych. Jest to, moim zdaniem, najlepszy wybór dla Angulara.

Live-Coding

W tej sesji kodowania stworzymy przykładową aplikację wykorzystującą bibliotekę ikon Angular Font Awesome i będziemy analizować jej rozmiar bundle za pomocą source-map-explorer. Chociaż może się to wydawać proste, z czasem stanie się bardziej skomplikowane.

Konfiguracja Projektu:

Wygeneruj nową aplikację za pomocą Angular CLI 17.2.0.

ng new Playground --standalone --style=scss --ssr=false --routing=false

Otwórz projekt w twoim IDE i usuń zawartość pliku app.component.html. Następnie usuń property title w app.component.ts.

Dodawanie source-map-explorer

W tym projekcie będziemy używać source-map-explorer — narzędzia do analizy buildów produkcyjnych. Operowanie na buildzie aplikacji wymaga od nas dodania niestandardowej konfiguracji do angular.json.

Source-map-explorer wymaga następujących opcji, aby wygenerować dokładny tree-map rozmiaru bundle:

  • Source Maps — generuje mapy źródłowe dla skryptów i stylów, zapewniając, że każdy plik JavaScript ma odpowiadający mu plik .js.map. Na przykład, main.js będzie miał odpowiadający mu plik main.js.map. Więcej informacji na temat konfiguracji znajdziesz w dokumentacji Angulara.
  • Output Hashing — definiuje hashowanie nazw plików bundle w celu skorzystania z cache-busting. Z tym ustawieniem na “none”, pliki w bundlu nie będą miały hashowego przyrostka – a więc main.js nie zmieni swojej nazwy na main-5R55YYTO.js w końcowym bundlu.
  • Named Chunks — używa nazw plików dla lazy loaded chunków. Gdy ustawimy te property na true, nasz lazy loaded plik będzie nazwany feature-file-3DXD2T2D.js zamiast chunk-Q6M2GG4B.js. Ta opcja jest przydatna przy analizie bundle lazy-loaded ficzerów.

Teraz skonfigurujemy naszą aplikację, korzystając z opcji, które właśnie omówiliśmy.

angular.json

"configurations": {
 "production": { ... },
 "analyze-bundle": {
   "sourceMap": true,
   "outputHashing": "none",
   "namedChunks": true
 },
 "development": { ... }
},

Skonfigurowaliśmy już proces budowania Angulara. Teraz dodamy bibliotekę source-map-explorer do naszego projektu z flagą development dependency.

npm i -D source-map-explorer

Teraz możesz analizować rozmiar bundla swojej aplikacji za pomocą polecenia `source-map-explorer`. Więcej o tym poleceniu dowiesz się z dokumentacji.

Polecam dodanie skryptu do twojej konfiguracji npm, gdy pracujesz z zewnętrznymi komendami z kilku powodów:

  • Inni deweloperzy od razu zobaczą komendę po otwarciu package.json.
  • Nie musisz zapamiętywać komendy oraz jej parametrów.
  • Pozwala to uruchomić skrypt bezpośrednio przez GUI lub CLI twojego IDE bez określania dodatkowych argumentów.

Dodajmy polecenie `analyze-bundle` do naszej konfiguracji npm `package.json`:

package.json

"scripts": {
 "analyze-bundle": "ng build --configuration=analyze-bundle && source-map-explorer dist/**/*.js --no-border-checks"
},

Teraz możesz uruchomić skrypt za pomocą polecenia:

npm run analyze-bundle

Ignoruj ostrzeżenia Unable to map X/X bytes (X%) jeśli nie przekraczają więcej niż 5% rozmiaru twojego pliku. Po wykonaniu polecenia w twojej przeglądarce powinna otworzyć się nowa zakładka, zawierająca wizualizację treemap twojej aplikacji.

Jak Używać i Czytać Treemapy Rozmiaru Bundle

Korzystanie z treemap jest bardzo proste. W lewym górnym rogu możesz wybrać plik do wyświetlenia. Tutaj mamy 2 opcje do wyboru:

  • main.js — plik zawierający początkowy bundle twojej aplikacji
  • polifills.js — plik z kodem, który zapewnia, że twoja aplikacja internetowa działa spójnie niezależnie od tego z jakiej przeglądarki korzysta użytkownik.

Gdy zaczniesz korzystać z lazy-loadingu lub @defer bloków w kodzie twojej aplikacji, więcej plików pojawi się w select’cie. Teraz przejdźmy do prostokątów z czarnym obramowaniem na treemapie. Zawierają one dwie informacje:

  • Nazwa pliku/folderu (np. node_modules, main.js lub core.mjs).
  • Rozmiar bundle pliku/folderu w KB oraz jako procent wagi wybranego pliku lub łącznego rozmiaru bundle.

Kliknięcie na prostokąt skupia uwagę na jego dzieciach. Jest to przydatne do analizowania zależności w dużych aplikacjach. Warto zauważyć także zależność – im większy jest prostokąt, tym większy wpływ ma na rozmiar twojego bundle.

Wystarczy już tej teorii. Spójrzmy na początkowy rozmiar bundla naszej aplikacji, 77,43KB. Zapamiętaj tę wartość do późniejszego porównania.

Modyfikacja Projektu Dodanie Zewnętrznej Zależności

W chwili pisania tego artykułu, biblioteka @fortawesome/angular-fontawesome służy jako świetny przykład, dlaczego powinieneś analizować rozmiar bundla swojej aplikacji po pobraniu każdej zewnętrznej zależności. Ta biblioteka jest popularnym wyborem wśród programistów — ma ponad ~200k tygodniowych pobrań.

Wróćmy do naszego IDE i dodajmy fortawesome do naszego projektu.

ng add @fortawesome/[email protected]

Podczas instalacji wybierz następujące zestawy ikon:

  • Font Awesome 6
  • Free Solid Icons

Jesteśmy gotowi do użycia komponentu <fa-icon/> w naszym projekcie. Zacznijmy od deklaracji FaIconComponent w tablicy importów komponentu. Następnie zaimportuj i przypisz ikonę faClose do property w AppComponent. Zwróć uwagę, że możesz importować ikony z dwóch ścieżek:

  • @fortawesome/free-solid-svg-icons
  • @fortawesome/free-solid-svg-icons/faClose — użyj tego importu

app.component.ts

import { Component } from '@angular/core';
import { faClose } from "@fortawesome/free-solid-svg-icons/faClose";
import { FaIconComponent } from "./fa-icon/fa-icon.component";

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [FaIconComponent],
 templateUrl: './app.component.html',
 styleUrl: './app.component.scss'
})
export class AppComponent {
 protected readonly faClose = faClose;
}

Jako ostatni krok użyj tagu <fa-icon/> w template komponentu.

app.component.html

<fa-icon [icon]="faClose" />

Analiza Bundle Po Dodaniu Zewnętrznej Zależności

Nadszedł czas, aby przejrzeć rozmiar bundle aplikacji. Uruchommy skrypt analyze-bundle.

npm run analyze-bundle

Powinieneś zobaczyć poniższą wizualizację treemap, ale bez kolorów (wybierz plik main.js).

Od ostatniego sprawdzenia rozmiar bundla prawie się podwoił. Dodatkowe elementy bundle oznaczyłem na różowo i fioletowo dla zobrazowania zmian. Wyraźnie widać coś jest nie tak z rozmiarem naszego bundle.

Ten przykład, choć trywialny, pokazuje potencjalne trudności w zarządzaniu rozsądnym rozmiarem bundle. W prawdziwym przypadku będziesz miał do czynienia z dziesiątkami zależności, ale zasada pozostaje taka sama: znajdź największe prostokąty i analizuj je.

Podczas pracy nad analizą rozmiaru bundla warto dokumentować swoje ustalenia. Pozwoli to stworzyć action pointy z różnymi priorytetami optymalizacji. A propos…

Jak Zmniejszyć Rozmiar Bundle?

Istnieje kilka strategii optymalizacji rozmiaru bundle:

  • Korzystaj z lazy-loadingu do ładowania modułów i komponentów, które nie są wymagane przy początkowym ładowaniu aplikacji.
  • Użyj nowej składni @defer w szablonach komponentów. (Pamiętaj, że ma to wpływ na SEO).
  • Wykorzystaj dynamiczne wyrażenia importu w TypeScript do lazy-loadowania kodu na żądanie. Ten wzorzec jest często stosowany w przypadku okien dialogowych.
  • Zastąp zewnętrzne biblioteki własnymi, lżejszymi implementacjami.
  • Usuń lub przenieś style z globalnych stylów (styles.scss) do stylów komponentu.
  • Użyj komponentów standalone lub architektury SCAM (dla wersji Angulara poniżej 14.0.0), aby skorzystać z tree-shaking’u i usunąć nieużywane części kodu z bundle.
  • Usuń cały martwy kod z aplikacji, w tym nieużywane serwisy, dyrektywy, pipe’y, moduły i tak dalej. Dodatkowo usuń nieużywane zależności i biblioteki z bundle.

Zmniejszanie Bundle w Naszej Aplikacji

Przeanalizujmy jeszcze raz rozmiar pliku naszej aplikacji. Dodanie biblioteki Font Awesome spowodowało dodanie 82.95KB do naszego początkowego rozmiaru bundla. Obecnie nasza aplikacja waży łącznie 160.38KB — prawie tyle samo, co początkowy bundle Angulara!

Zacznijmy optymalizację od prostego rozwiązania. Jak widać na fioletowych prostokątach na mapie drzewa, importowanie ikony `faClose` spowodowało dodanie do naszego bundla ikony `faXMark`, mimo że jej nigdzie nie używamy. Możemy zaktualizować import z:

import { faClose } from "@fortawesome/free-solid-svg-icons/faClose";

na:

import { faClose } from "@fortawesome/free-solid-svg-icons";

Teraz uruchom ponownie skrypt `analyze-bundle`.

Poprawiliśmy rozmiar naszego bundla o 1,12KB – zmniejszyliśmy rozmiar pliku main.js z 160,38KB do 159,26KB. To zdecydowanie poprawa — ale wciąż nie jest to znacząca optymalizacja.

Zagłębmy się głębiej w problem i skupmy się na prostokącie `fontawesome-svg-core/index.mjs`. To znaczący element naszego bundla, ważący 60,03KB! Moje początkowe przypuszczenie jest takie, że komponent Font Awesome zawiera funkcje, których nie używamy, a które powodują dodanie dodatkowych KB do naszego bundla.

Po zbadaniu repozytorium na GitHub’ie i dokumentacji jest jasne, że jest tam więcej funkcjonalności niż tylko te, które używamy w naszej aplikacji. Istnieją dwie możliwe rozwiązania: albo czekamy, aż wtyczka Font Awesome stanie się bardziej zoptymalizowana i bardziej tree-shake-owalna, albo skorzystamy z innych technik optymalizacji.

Po przeglądzie sekcji `Jak zmniejszyć rozmiar bundle?`, wydaje się, że mamy dwie opcje, aby zmniejszyć początkowy rozmiar bundla naszej aplikacji:

  • Użycie bloku @defer i lazy-ładowanie naszych komponentów. Nie zamierzamy korzystać z tego podejścia, ponieważ może to wpłynąć na SEO naszej strony i nie rozwiązuje problemu u jego źródła — ostatecznie komponent i tak musi zostać załadowany.
  • Napisanie własnej implementacji komponentu fa-icon na bazie API biblioteki font-awesome. To rozwiązanie wydaje się lepsze — ale jest znacznie trudniejsze. Mimo to zdecydowałem się spróbować.

Pisanie własnej implementacji Font Awesome

Chociaż przepisywanie komponentów oferuje długoterminowe korzyści w zakresie elastyczności i wydajności, jest pełne wyzwań. Jednym z nich jest accessibility — nawigacja za pomocą klawiatury, atrybuty ARIA, tłumaczenia, implementowanie harnessów oraz innych API. W trakcie pisania customowej implementacji często będziesz przeszukiwać dokumentację oryginalnej biblioteki, kod źródłowy, issues oraz pull requesty. W konsekwencji, tworzenie customowej wersji komponentu zorientowanego pod projekt może być czasochłonne.

Z tego powodu nie będę szczegółowo dokumentować mojego researchu nad ikonami Font Awesome. Zamiast tego, moim celem jest pokazanie, co jest możliwe, gdy jesteś w pełni zaangażowany w optymalizację rozmiaru swojego bundle.

Zacznijmy naszą własną implementację, od wygenerowania komponentu

ng g faIcon --inline-style --inline-template --skip-tests

Następnie zmodyfikuj `fa-icon.component.ts` w następujący sposób:

fa-icon.component.ts

import { Component, computed, inject, input, ViewEncapsulation } from '@angular/core';
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";

/**
* Code based on https://github.com/FortAwesome/react-fontawesome/issues/232#issuecomment-1158654385
*/
@Component({
 selector: 'app-fa-icon',
 standalone: true,
 template: ``,
 host: {
   '[innerHTML]': 'iconHtml()',
 },
 styles: `
   .svg-inline--fa {
     display: inline-block;
     height: 1em;
     overflow: visible;
     vertical-align: -0.125em;
   }
 `,
 encapsulation: ViewEncapsulation.None
})
export class FaIconComponent {
 icon = input.required<IconDefinition>()
 iconHtml = computed(() => this._createIconHtml(this.icon()))

 private readonly _sanitizer = inject(DomSanitizer);

 private _createIconHtml(faIcon: IconDefinition): SafeHtml {
   const [width, height, , , svgPathData] = faIcon.icon;
   const iconHtml = `
  <svg aria-hidden="true" focusable="false" class="svg-inline--fa" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img">
     <path fill="currentColor" d="${svgPathData}" />
   </svg>`

   return this._sanitizer.bypassSecurityTrustHtml(iconHtml);
 }
}

Oto wyjaśnienie powyższego kodu:

  • Wymagany input `icon` jest punktem wejściowym do naszego komponentu. Używa typu `IconDefinition` pochodzącego z biblioteki font-awesome.
  • Na podstawie tego sygnału tworzymy `iconHtml`, który przekształca wartość wejściową w ikonę svg za pomocą metody `_createIconHtml`.
  • Na koniec wyświetlamy sanitizowany html wewnątrz właściwości `[innerHTML]` w hoście komponentu. Dołączamy również style dla naszych ikon.

Jak widać, kod odpowiedzialny za renderowanie ikony jest prosty. A co z użyciem naszej własnej implementacji w innych komponentach? Spróbujmy!

app.component.ts

import { Component } from '@angular/core';
import { faClose } from "@fortawesome/free-solid-svg-icons/faClose";
import { FaIconComponent } from "./fa-icon/fa-icon.component";

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [FaIconComponent],
 templateUrl: './app.component.html',
 styleUrl: './app.component.scss'
})
export class AppComponent {
 protected readonly faClose = faClose;
}

app.component.html

<app-fa-icon [icon]="faClose" />

Jeśli chodzi o użytkowanie, prawie nie ma różnicy między naszą własną implementacją a oficjalną biblioteką! Dla wygody moglibyśmy nawet zaktualizować selektor komponentu na `fa-icon`.

Czy ta własna implementacja jest idealna dla Ciebie? Oczywiście to zależy, ale jeśli nie używasz żadnych dodatkowych funkcji komponentu z biblioteki fa-icon (osobiście nigdy ich nie używałem) ani nawet klas pomocniczych, to rozwiązanie wydaje się perfekcyjne.

A co z rozmiarem bundla? Czy osiągnęliśmy nasz cel optymalizacji? Uruchommy skrypt `analyze-bundle`. (wybierz plik `main.js`)

Rzeczywiście, zmniejszyliśmy rozmiar bundla z 158,27KB do 94,64KB. To znaczący zysk pod względem rozmiaru bundla (nasze postępy można zobaczyć na wykresie w finalnej analizie poniżej). Chociaż ten proces może wydawać się szybki, osiągnięcie podobnych wyników w rzeczywistym świecie może wymagać dni, a nawet tygodnie researchu i implementacji.

Ponadto zauważ, że nasz własny komponent “fa-icon” nie jest widoczny na treemapie, ponieważ nie ma wystarczającej wagi, by być wyświetlony. W przypadkach obejmujących większe komponenty zobaczysz je w wewnątrz prostokąta /src.

Końcowa Analiza Rozmiaru Bundle

Jako zwieńczenie tego artykułu, przeanalizujmy rozmiar bundle pliku `main.js`. Przygotowałem wykres szczegółowo przedstawiający każdy krok, który podjęliśmy w naszej aplikacji:

  • (1) Initial Project — Pomiar rozmiaru bundla wykonany zaraz po utworzeniu świeżego projektu Angular.
  • (2) Adding Icons — Pomiar rozmiaru bundla zaraz po dodaniu i wykorzystaniu biblioteki @fortawesome/angular-fontawesome w naszym projekcie. Zauważalnie, nasz rozmiar bundle gwałtownie wzrósł.
  • (3) Import Fix — Pomiar rozmiaru bundla po korekcie importu ikon. Chociaż zauważyliśmy pewną poprawę, nie była to znacząca zmiana, która wpłynęłaby na doświadczenia użytkownika.
  • (4) Rewriting Component — Pomiar rozmiaru bundla po zaimplementowaniu naszego własnego komponentu fa-icon. Jak widać, zrobiliśmy duże postępy — `@fortawesome` jest prawie niewidoczne na wykresie, ponieważ waga kodu ikony svg jest prawie symboliczna.

Wnioski

Początkowy rozmiar bundle jest kluczowym wskaźnikiem wydajności dla twoich aplikacji. Analizatory rozmiaru bundle mogą pomóc w mierzeniu rozmiaru bundle zarówno dla buildów developerskich, jak i produkcyjnych. Dobrą praktyką jest mierzenie bundle regularne i co jeszcze ważniejsze, przeglądanie go po dodaniu nowych bibliotek do projektu. Praktyka ta może zapobiegać niepotrzebnym wysiłkom przeznaczonym na optymalizację w dłuższej perspektywie. Możesz też rozważyć ustanowienie budżetu dla początkowego rozmiaru bundle i stylów komponentów, aby skutecznie utrzymać standardy wydajności.

 

O autorze

Dawid Kostka

Angular Developer w House of Angular. Software development dla mnie to coś więcej niż tylko klepanie kodu. Kocham eksplorować i bawić się wysokiej jakości kodem. Uwielbiam adrenalinę i pozytywne nastawienie. W wolnych chwilach jeżdzę na rolkach agresywnych.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

Leave a Reply

Your email address will not be published. Required fields are marked *