Znajdowanie powolnych interakcji w terenie

Dowiedz się, jak znaleźć powolne interakcje w danych pól witryny, aby znaleźć możliwości ulepszenia interakcji do kolejnego wyrenderowania.

Dane pól to dane, które informują, jak użytkownicy odbierają Twoją witrynę. Przedstawia problemy, których nie można znaleźć tylko w danych laboratoryjnych. Jeśli chodzi o interakcję z następnym wyrenderowaniem (INP), dane z pola są niezbędne do identyfikowania powolnych interakcji i dostarczają ważne wskazówki, które pomagają w ich naprawieniu.

Z tego przewodnika dowiesz się, jak szybko ocenić wartość INP witryny, korzystając z danych z poszczególnych pól z Raportu na temat użytkowania Chrome (CrUX), aby sprawdzić, czy w Twojej witrynie występują problemy z INP. Później dowiesz się, jak korzystać z kompilacji atrybucji z biblioteki Web Vitals w języku JavaScript oraz z nowych statystyk z interfejsu Long Animation Frames API (LoAF) – do zbierania i interpretowania danych z pola w przypadku powolnych interakcji w witrynie.

Zacznij od CrUX, aby ocenić wartość INP swojej witryny

Jeśli nie zbierasz danych z poszczególnych użytkowników swojej witryny, dobrym punktem wyjścia może być raport raportu CrUX. Raport CrUX zbiera dane z pól od prawdziwych użytkowników Chrome, którzy zgodzili się na wysyłanie danych telemetrycznych.

Dane raportu na temat użytkowania Chrome wyświetlają się w wielu różnych obszarach i zależą od zakresu informacji, których szukasz. Raport CrUX może dostarczyć dane o INP i innych podstawowych wskaźnikach internetowych w przypadku:

  • Pojedyncze strony i całe źródła za pomocą narzędzia PageSpeed Insights.
  • Typy stron. Na przykład wiele witryn e-commerce korzysta z typów strony szczegółów produktu i strony z listą produktów. W Search Console możesz uzyskać dane raportu na temat użytkowania Chrome dotyczące unikalnych typów stron.

Na początek wpisz w narzędziu PageSpeed Insights adres URL swojej witryny. Gdy wpiszesz adres URL, dane z jego pola (jeśli są dostępne) będą wyświetlać się dla wielu wskaźników, w tym INP. Możesz też użyć przełączników, aby sprawdzić wartości INP dla wymiarów dotyczących urządzeń mobilnych i komputerów.

Dane pól wyświetlane przez CrUX w Statystykach PageSpeed, pokazujące LCP, INP i CLS w przypadku 3 podstawowych wskaźników internetowych, a także TTFB, FCP jako dane diagnostyczne i FID jako wycofany wskaźnik podstawowych wskaźników internetowych.
Odczyt danych raportu CrUX na podstawie statystyk PageSpeed. W tym przykładzie wartość INP danej strony internetowej wymaga ulepszenia.

Te dane są przydatne, ponieważ informują o problemach. Jednak raport CrUX nie potrafi wskazać, co jest przyczyną problemów. Dostępnych jest wiele rozwiązań do monitorowania użytkowników rzeczywistego (RUM), które pomogą Ci zbierać własne dane z poszczególnych pól użytkowników Twojej witryny. Jedną z opcji jest samodzielne zbieranie tych danych za pomocą biblioteki JavaScriptu Web Vitals.

Zbieraj dane pól za pomocą biblioteki JavaScript web-vitals

Biblioteka JavaScript web-vitals to skrypt, który możesz wczytać w swojej witrynie, by zbierać dane od jej użytkowników. Możesz go używać do rejestrowania różnych wskaźników, w tym INP, w przeglądarkach, które go obsługują.

Obsługa przeglądarek

  • 96
  • 96
  • x
  • x

Źródło

Aby uzyskać podstawowe dane INP od użytkowników z danej dziedziny, można użyć standardowej kompilacji biblioteki Web Vitals:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  console.log(name);    // 'INP'
  console.log(value);   // 512
  console.log(rating);  // 'poor'
});

Aby można było przeanalizować dane pól od użytkowników, warto wysłać te dane w taki sposób:

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  // Prepare JSON to be sent for collection. Note that
  // you can add anything else you'd want to collect here:
  const body = JSON.stringify({name, value, rating});

  // Use `sendBeacon` to send data to an analytics endpoint.
  // For Google Analytics, see https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics.
  navigator.sendBeacon('/analytics', body);
});

Dane te nie mówią jednak wiele więcej niż te, które zostałyby dostarczone przez zespół raportu na temat użytkowania Chrome. Dlatego do akcji wkracza tu mechanizm atrybucji z biblioteki Web Vitals.

Pójdź o krok dalej dzięki tworzeniu atrybucji w bibliotece Web Vitals

Funkcja atrybucji z biblioteki Web Vitals wyświetla dodatkowe dane, które możesz uzyskać od użytkowników w tej dziedzinie, aby pomóc Ci w rozwiązywaniu problemów z interakcjami, które wpływają na wartość INP Twojej witryny. Te dane są dostępne przez obiekt attribution w metodzie onINP() biblioteki:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, rating, attribution}) => {
  console.log(name);         // 'INP'
  console.log(value);        // 56
  console.log(rating);       // 'good'
  console.log(attribution);  // Attribution data object
});
Jak wyglądają dzienniki konsoli z biblioteki Web Vitals. Konsola w tym przykładzie pokazuje nazwę danych (INP), wartość INP (56), gdzie ta wartość mieści się w progach INP (dobra wartość), oraz różne informacje wyświetlane w obiekcie atrybucji, w tym wpisy z interfejsu Long Animation Frame API.
Jak dane z biblioteki Web Vitals są wyświetlane w konsoli.

Oprócz wartości INP strony raport o atrybucji zapewnia też wiele danych, które pomagają zrozumieć przyczyny powolnych interakcji, np. na której części interakcji należy się skupić. Dzięki temu możesz uzyskać odpowiedzi na ważne pytania, takie jak:

  • „Czy użytkownik wchodził w interakcję ze stroną podczas jej wczytywania?”
  • „Czy moduły obsługi zdarzeń interakcji działały przez długi czas?”
  • „Czy kod modułu obsługi zdarzeń interakcji był opóźniony w uruchomieniu? Jeśli tak, to co jeszcze dzieje się w wątku głównym w tamtym czasie?”.
  • „Czy interakcja spowodowała dużo prac związanych z renderowaniem, które opóźniły wymalowanie następnej klatki?”

W tabeli poniżej znajdziesz niektóre podstawowe dane atrybucji, które możesz uzyskać z biblioteki. Mogą one pomóc Ci w znalezieniu ogólnych przyczyn powolnych interakcji w witrynie:

Klucz obiektu attribution Dane
interactionTarget Selektor arkusza CSS wskazujący element, który wygenerował wartość INP strony, np. button#save.
interactionType Typ interakcji – kliknięcia, dotknięcia lub dane wpisane na klawiaturze.
inputDelay* Opóźnienie wejściowe interakcji.
processingDuration* Czas od uruchomienia pierwszego detektora zdarzeń w odpowiedzi na interakcję użytkownika do zakończenia przetwarzania wszystkich danych przez detektor zdarzeń.
presentationDelay* Opóźnienie prezentacji interakcji, które trwa od zakończenia obsługi zdarzeń przez moduły obsługi zdarzeń do czasu wyrenderowania następnej klatki.
longAnimationFrameEntries* Wpisy z LoAF powiązane z interakcją. Więcej informacji znajdziesz w dalszej części tego artykułu.
*Nowość w wersji 4

Począwszy od wersji 4 biblioteki Web Vitals, możesz uzyskać jeszcze lepszy wgląd w problematyczne interakcje dzięki udostępnionym przez nią danym, takim jak podział fazowy INP (opóźnienie wejściowe, czas przetwarzania i opóźnienie prezentacji) oraz interfejs Long Animation Frame API (LoAF).

Interfejs Long Animation Frame API (LoAF)

Obsługa przeglądarek

  • 123
  • 123
  • x
  • x

Źródło

Debugowanie interakcji na podstawie danych z terenu nie jest łatwym zadaniem. Jednak dzięki danym z LoAF można teraz uzyskać lepszy wgląd w przyczyny powolnych interakcji, ponieważ udostępnia ono szereg szczegółowych kodów czasowych i innych danych, które pozwalają zidentyfikować precyzyjne przyczyny, a co ważniejsze – umiejscowienie źródła problemu w kodzie witryny.

Kompilacja atrybucji z biblioteki Web Vitals udostępnia tablicę wpisów LoAF w kluczu longAnimationFrameEntries obiektu attribution. Poniższa tabela zawiera kilka kluczowych informacji, które można znaleźć w każdej pozycji LoAF:

Klucz obiektu wpisu LoAF Dane
duration Czas trwania długiej klatki animacji do momentu ukończenia układu, z wyłączeniem malowania i komponowania.
blockingDuration Łączny czas w klatce, w którym przeglądarka nie mogła szybko odpowiedzieć z powodu długich zadań. Ten czas blokowania może obejmować długie zadania z włączonym JavaScriptem, a także wszelkie kolejne długie zadania renderowania w ramce.
firstUIEventTimestamp Sygnatura czasowa momentu, w którym zdarzenie zostało umieszczone w kolejce w ramach klatki. Ta opcja jest przydatna do ustalania początku opóźnienia danych wejściowych interakcji.
startTime Sygnatura czasowa rozpoczęcia ramki.
renderStart Kiedy rozpoczęło się renderowanie klatki. Obejmuje to wszystkie wywołania zwrotne requestAnimationFrame (i ResizeObserver wywołania zwrotne w odpowiednich przypadkach), ale potencjalnie przed rozpoczęciem pracy nad stylem lub układem.
styleAndLayoutStart Kiedy w klatce trwają prace nad stylem lub układem. Może być przydatne, gdy chcesz sprawdzić długość pracy związanej ze stylem lub układem podczas sprawdzania innych dostępnych sygnatur czasowych.
scripts Tablica elementów zawierających informacje o atrybucji skryptów przyczyniające się do wartości INP strony.
Wizualizacja długiej klatki animacji zgodnie z modelem LoAF.
Diagram czasu wyświetlania długiej klatki animacji według interfejsu LoAF API (minus blockingDuration).

Wszystkie te informacje wiele mówią o tym, co spowalnia interakcję, ale szczególnie interesuje Cię tablica scripts, którą wyświetlają wpisy LoAF:

Klucz obiektu atrybucji skryptu Dane
invoker Wywołujący. Może się ona różnić w zależności od typu wywołującego opisanego w następnym wierszu. Przykładami elementów wywołujących mogą być wartości takie jak 'IMG#id.onload', 'Window.requestAnimationFrame' czy 'Response.json.then'.
invokerType Typ wywołującego. Mogą to być 'user-callback', 'event-listener', 'resolve-promise', 'reject-promise', 'classic-script' lub 'module-script'.
sourceURL Adres URL skryptu, z którego pochodzi długa klatka animacji.
sourceCharPosition Pozycja znaku w skrypcie określony przez sourceURL.
sourceFunctionName Nazwa funkcji we zidentyfikowanym skrypcie.

Każdy wpis w tej tablicy zawiera dane widoczne w tej tabeli. Informacje te zawierają informacje o skrypcie, który był odpowiedzialny za powolną interakcję – oraz o sposobie jego udziału.

Zmierz i zidentyfikuj najczęstsze przyczyny powolnych interakcji

Z tego przewodnika dowiesz się, jak możesz wykorzystać te informacje. Z tego przewodnika dowiesz się, jak korzystać z danych LoAF wyświetlanych w bibliotece web-vitals, aby znaleźć niektóre przyczyny powolnych interakcji.

Długi czas przetwarzania

Czas przetwarzania interakcji to czas potrzebny na wykonanie wywołań zwrotnych zarejestrowanych zdarzeń modułu obsługi interakcji i wszystkie inne, które mogą wystąpić pomiędzy nimi. Długi czas przetwarzania wyświetla się w bibliotece Web Vitals:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5
});

To naturalne, że główną przyczyną powolnych interakcji jest to, że uruchomienie kodu modułu obsługi zdarzeń trwało zbyt długo, ale nie zawsze tak jest. Gdy potwierdzisz, że na tym polega problem, możesz skorzystać z bardziej szczegółowych danych dotyczących LoAF:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5

  // Get the longest script from LoAF covering `processingDuration`:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Get attribution for the long-running event handler:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

Jak widać we wcześniejszym fragmencie kodu, za pomocą danych LoAF możesz znaleźć dokładną przyczynę interakcji z wysokimi wartościami czasu przetwarzania, w tym:

  • Element i jego zarejestrowany detektor zdarzeń.
  • Plik skryptu i jego pozycję znaku w nim zawierający długotrwały kod modułu obsługi zdarzeń.
  • Nazwa funkcji.

Dane tego typu są bezcenne. Nie musisz już sprawdzać, która interakcja (lub które moduły obsługi zdarzeń) odpowiadała za długie wartości czasu przetwarzania danych. Poza tym, ponieważ skrypty innych firm często rejestrują własne moduły obsługi zdarzeń, możesz ustalić, czy to Twój kod, czy nie. W przypadku kodu, nad którym masz kontrolę, warto zastanowić się nad optymalizacją długich zadań.

Duże opóźnienia danych wejściowych

Długotrwałe moduły obsługi zdarzeń są powszechne, ale warto też wziąć pod uwagę inne aspekty interakcji. Jedna część ma miejsce przed czasem przetwarzania, który jest nazywany opóźnieniem danych wejściowych. Jest to czas od zainicjowania przez użytkownika interakcji do momentu rozpoczęcia wywoływania jego wywołań zwrotnych modułu obsługi zdarzeń i ma miejsce, gdy wątek główny przetwarza już inne zadanie. Kompilacja atrybucji w bibliotece Web Vitals może określać długość opóźnienia danych wejściowych z interakcją:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536
});

Jeśli zauważysz, że niektóre interakcje mają duże opóźnienia danych wejściowych, musisz sprawdzić, co dzieje się na stronie w momencie interakcji, co spowodowało długie opóźnienie w danych wejściowych. Często sprowadza się to od tego, czy interakcja miała miejsce podczas wczytywania strony, czy po jej zakończeniu.

Czy było to podczas wczytywania strony?

Wątek główny jest często najbardziej obciążony, gdy ładuje się strona. W tym czasie wszystkie rodzaje zadań są umieszczane w kolejce i przetwarzane, a jeśli użytkownik próbuje wejść w interakcję ze stroną w trakcie wykonywania wszystkich czynności, może to opóźnić interakcję. Strony, które wczytują JavaScript, mogą rozpocząć pracę nad kompilacją i oceną skryptów oraz wykonywaniem funkcji przygotowujących stronę do interakcji z użytkownikiem. Może to przeszkodzić w interakcji użytkownika podczas danej aktywności. Możesz sprawdzić, czy w przypadku użytkowników Twojej witryny tak jest:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Invoker types can describe if script eval blocked the main thread:
    const {invokerType} = script;    // 'classic-script' | 'module-script'
    const {sourceLocation} = script; // 'https://example.com/app.js'
  }
});

Jeśli rejestrujesz te dane w tym polu i zauważasz duże opóźnienia danych wejściowych i typy wywołujących typu 'classic-script' lub 'module-script', oznacza to, że możesz stwierdzić, że skrypty w Twojej witrynie oceniają za długo i blokują główny wątek na tyle długo, że opóźniają interakcje. Aby skrócić czas blokowania, możesz podzielić skrypty na mniejsze pakiety, opóźnić początkowe wczytanie nieużywanego kodu i sprawdzić witrynę pod kątem nieużywanego kodu, który można całkowicie usunąć.

Czy po wczytaniu strony?

Opóźnienia danych wejściowych często występują podczas wczytywania strony, jednak czasami mogą wystąpić po załadowaniu strony z zupełnie innej przyczyny. Częstą przyczyną opóźnień danych wejściowych po wczytaniu strony mogą być okresowe uruchamianie kodu w związku z wcześniejszym wywołaniem setInterval, a nawet wywołania zwrotne zdarzeń, które zostały umieszczone w kolejce wcześniej i nadal są przetwarzane.

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    const {invokerType} = script;        // 'user-callback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

Tak jak w przypadku rozwiązywania problemów z długimi czasami przetwarzania, duże opóźnienia wprowadzania danych z wymienionych wcześniej przyczyn pozwalają uzyskać szczegółowe dane o atrybucji skryptu. Różnica polega jednak na tym, że typ wywołującego zmieni się w zależności od charakteru dzieła, które opóźniło interakcję:

  • 'user-callback' oznacza, że zadanie blokowania pochodzi z setInterval, setTimeout, a nawet requestAnimationFrame.
  • 'event-listener' oznacza, że zadanie blokowania pochodzi z wcześniejszych danych wejściowych, które znajdują się w kolejce i nadal są przetwarzane.
  • 'resolve-promise' i 'reject-promise' oznaczają, że zadanie blokowania pochodziło z pracy asynchronicznej, która została uruchomiona wcześniej i została zakończona lub odrzucona w momencie, gdy użytkownik próbował wejść w interakcję ze stroną, co opóźniało interakcję.

W każdym przypadku dane o atrybucji skryptu dadzą Ci wskazówkę, gdzie zacząć szukać i czy opóźnienie sygnału wejściowego było spowodowane przez Twój kod czy skrypt innej firmy.

długie opóźnienia prezentacji;

Opóźnienia prezentacji to ostatni kilometr interakcji i zaczynają się po zakończeniu działania modułów obsługi zdarzeń interakcji aż do momentu wyrenderowania następnej klatki. Pojawiają się, gdy działanie modułu obsługi zdarzeń w wyniku interakcji zmienia wygląd interfejsu. Podobnie jak w przypadku czasu przetwarzania i opóźnień danych wejściowych, biblioteka Web Vitals podaje informacje o opóźnieniu prezentacji danych z interakcji:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691
});

Jeśli rejestrujesz te dane i zauważysz duże opóźnienia prezentacji w przypadku interakcji przyczyniających się do wartości INP witryny, przyczyny mogą być różne. Warto jednak przyjrzeć się kilku przyczynom.

Wymagana praca nad stylem i układem

Długie opóźnienia prezentacji mogą być kosztowne podczas obliczania stylów i układu. Wynika to z wielu przyczyn, takich jak złożone selektory arkusza CSS czy duże rozmiary DOM. Możesz mierzyć czas trwania tej pracy za pomocą kodów czasowych LoAF wyświetlanych w bibliotece Web Vitals:

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  // Get necessary timings:
  const {startTime} = loaf; // 2120.5
  const {duration} = loaf;  // 1002

  // Figure out the ending timestamp of the frame (approximate):
  const endTime = startTime + duration; // 3122.5

  // Get the start timestamp of the frame's style/layout work:
  const {styleAndLayoutStart} = loaf; // 3011.17692309

  // Calculate the total style/layout duration:
  const styleLayoutDuration = endTime - styleAndLayoutStart; // 111.32307691

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running style and layout operation:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

LoAF nie poinformuje Cię, jak długo trwa styl i układ danej klatki, ale dowiesz się, kiedy się ona rozpoczęła. Dzięki tej sygnaturze czasowej możesz wykorzystać inne dane z LoAF, aby obliczyć dokładny czas trwania tej pracy, określając czas zakończenia klatki i odejmując od niej sygnaturę czasową rozpoczęcia stylu i układu.

Długo trwające wywołania zwrotne requestAnimationFrame

Jedną z potencjalnych przyczyn długich opóźnień prezentacji jest nadmierna ilość pracy podczas wywołania zwrotnego requestAnimationFrame. Zawartość tego wywołania zwrotnego jest wykonywana po zakończeniu działania modułów obsługi zdarzeń, ale tuż przed ponownym obliczeniem stylu i pracą układu.

Realizacja tych wywołań zwrotnych może zająć dużo czasu, jeśli są związane z nimi skomplikowane zadania. Jeśli podejrzewasz, że duże opóźnienia prezentacji są wynikiem Twojej pracy z narzędziem requestAnimationFrame, możesz użyć danych LoAF pochodzących z biblioteki Web Vitals, aby zidentyfikować te scenariusze:

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 543.1999999880791

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  // Get the render start time and when style and layout began:
  const {renderStart} = loaf;         // 2489
  const {styleAndLayoutStart} = loaf; // 2989.5999999940395

  // Calculate the `requestAnimationFrame` callback's duration:
  const rafDuration = styleAndLayoutStart - renderStart; // 500.59999999403954

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running requestAnimationFrame callback:
    const {invokerType} = script;        // 'user-callback'
    const {invoker} = script;            // 'FrameRequestCallback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

Jeśli zauważysz, że znaczna część czasu opóźnienia prezentacji jest wykonywana w wywołaniu zwrotnym requestAnimationFrame, upewnij się, że Twoja praca w tych wywołaniach jest ograniczona do czynności, które powodują aktualizację interfejsu użytkownika. Wszelkie inne prace, które nie dotyczą DOM ani nie wymagają aktualizacji stylów, niepotrzebnie opóźnią wyrenderowanie następnej klatki, więc zachowaj ostrożność.

Podsumowanie

Dane terenowe to najlepsze źródło informacji, na podstawie których możesz dowiedzieć się, które interakcje stanowią problem dla rzeczywistych użytkowników w terenie. Korzystając z narzędzi do zbierania danych w terenie, takich jak biblioteka JavaScript Web Vitals (lub dostawca RUM), możesz mieć pewność, które interakcje są najbardziej problematyczne, a następnie przejść do odtwarzania problematycznych interakcji w module, a potem zająć się ich wyeliminowaniem.

Baner powitalny z filmu Unsplash, autorstwa Federico Respini.