Strona główna / Artykuły / Odpowiedzi na pytania z rozmów kwalifikacyjnych dotyczących React i JavaScript, które demonstrują prawdziwą głębię wiedzy.

Odpowiedzi na pytania z rozmów kwalifikacyjnych dotyczących React i JavaScript, które demonstrują prawdziwą głębię wiedzy.

Odkryj silniejsze, bardziej zróżnicowane odpowiedzi na typowe pytania z rozmów kwalifikacyjnych dotyczących React i JavaScript, od Virtual DOM po projektowanie systemów, które pokazują głębszą biegłość inżynierską.

3825 słów

Wprowadzenie: Dlaczego większość kandydatów brzmi tak samo

Jeśli wzięjesz udział w wystarczającej liczbie rozmów kwalifikacyjnych dotyczących Reacta, zauważysz pewien wzorzec: te same powtarzalne frazy pojawiają się raz za razem. „Virtual DOM jest szybszy”. „useEffect służy do efektów ubocznych”. „JavaScript działa na pojedynczej nitce”. Żadne z tych stwierdzeń nie jest fałszywe, ale są to rzeczy, które każdy może wyrecytować po przeczytaniu kilku postów na blogu, a one nie mówią rekruterowi nic o tym, jak naprawdę myślisz.

To, co odróżnia dobrego kandydata od tego, który otrzymuje uprzejmy e-mail z odmową, to nie to, czy zna definicję z podręcznika – lecz to, czy potrafi zagłębić się o jeden poziom głębiej. Czy potrafisz powiązać dany koncept związki z kompromisami, które za nim stoją? Czy potrafisz wyjaśnić, dlaczego podjęto określoną decyzję projektową, a nie tylko to, co ona robi? To właśnie tego rodzaju umiejętności rekruterzy faktycznie szukają podczas rozmów kwalifikacyjnych.

To przewodnik omawia pytania, które rzeczywiście pojawiają się podczas rozmów kwalifikacyjnych z osobami o średnim i wyższym poziomie doświadczenia w dziedzinie frontendu, wraz z odpowiedziami na tyle szczegółowymi, że sprawiają one, iż rozmówca przestaje przeglądać notatki i faktycznie słucha.

Część 1: Podstawowe koncepcje React

Pytanie 1: „Wyjaśnij, czym jest Virtual DOM. Jak on działa?”

Odpowiedź, którą łatwo zapomnieć: „Virtual DOM to lżejsza kopia rzeczywistego DOM-u. React porównuje je i aktualizuje tylko to, co się zmieniło, dzięki czemu jest szybszy.”

Lepsza odpowiedź: Virtual DOM to abstrakcja, ale określanie go wyłącznie mianem „szybszego” pomija istotę problemu. Rzeczywista korzyść pod względem wydajności nie pochodzi sama z Virtual DOM-u – pochodzi od logiki grupowania operacji i ich synchronizacji, która jest wokół niego zbudowana.

React przechowuje wewnętrznie dwa drzewa: to, które jest obecnie widoczne na ekranie, oraz drzewo w fazie opracowywania, reprezentujące to, co ma zostać wyświetlone. Gdy stan się zmienia, React nie spieszy się z natychmiastową modyfikacją rzeczywistego DOM-u. Zamiast tego tworzy nowe drzewo Virtual DOM, przeprowadza analizę różnic, aby określić najmniejszą możliwą liczbę koniecznych zmian, a następnie aplikuje wszystkie te zmiany w rzeczywistym DOM-ie w jednej transakcji. To właśnie ta procedura grupowania zmian zapobiega powtarzającym się obliczeniom układu, często nazywanym problemem „layout thrashing”.

Istnieje jeszcze jedna niuans, o którym warto wspomnieć: Virtual DOM nie jest rozwiązaniem bezkosztowym. Jego algorytm porównywania jest celowo utrzymywany na poziomie złożoności O(n) poprzez wykorzystanie heurystyk – na przykład założenie, że elementy różnych typów będą tworzyć zupełnie różne poddrzewa – zamiast wykonywać pełne porównanie drzew o złożoności O(n³). Ten kompromis zapewnia szybką pracę w typowych przypadkach, ale oznacza, że niektóre sytuacje, takie jak bardzo długa lista, w której zmienia się tylko jeden element, mogą nadal być kosztowne. Właśnie dlatego istnieją narzędzia takie jak React.memo, useMemo oraz biblioteki do wirtualizacji list.

Należy również przyznać, że Virtual DOM traci na znaczeniu jako unikalna zaleta produktu. Kompilatory takie jak Svelte całkowicie pomijają krok z Virtual DOM, a sam React zmierza w stronę funkcji renderowania równoległego, które zmieniają sposób działania procesu synchronizacji w tle. Virtual DOM rozwiązał konkretny problem około 2013 roku. Zrozumienie powodu jego wprowadzenia jest ważniejsze niż umiejętność opisania jego mechanizmów.

Taka odpowiedź jest skuteczna, ponieważ pokazuje świadomość historyczną, szczere uznanie konieczności kompromisów oraz znajomość ewolucji szerszego otoczenia frontendu — nie tylko odpowiadasz na dosłowne pytanie, ale także demonstrujesz, że rozumiesz, jak ta koncepcja pasuje do szerszego kontekstu.

Pytanie 2: „Jaka jest różnica między useEffect, useLayoutEffect i w jakich sytuacjach należy używać każdego z nich?”

Odpowiedź, którą łatwo zapomnieć: „useEffect wykonywany jest po renderowaniu. useLayoutEffect wykonywany jest przed narysowaniem ekranu przez przeglądarkę. Używaj useLayoutEffect, gdy musisz zmierzyć strukturę DOM.”

Lepsza odpowiedź: rozróżnienie czasowe jest powszechnie znane, ale to wyjaśnienie, dlaczego ten moment ma znaczenie, odróżnia dobrą odpowiedź od słabej. useEffect uruchamia się asynchronicznie, po tym jak przeglądarka już narysowała ekran. useLayoutEffect natomiast uruchamia się synchronicznie, zaraz po tym, jak React zakończy obliczanie zmian w DOM, ale przed tym, jak przeglądarka zdąży narysować ekran.

To rozróżnienie oznacza, że useLayoutEffect faktycznie blokuje aktualizację wizualną. Jeśli umieścisz w nim kosztowne obliczenia, użytkownik zauważy zamarły ekran. Właśnie dlatego dokumentacja React zaleca używanie domyślnie useEffect – niepotrzebne blokowanie procesu rysowania to częsta pułapka dotycząca wydajności.

Mimo to istnieją uzasadnione powody, by korzystać z useLayoutEffect poza prostym „mierzeniem DOM-u”. Dobrym przykładem zastosowania jest zapobieganie widocznym migotaniom. Wyobraźmy sobie renderowanie podpowiedzi, której pozycja zależy od wymiarów elementu docelowego – wykonywanie takich obliczeń pozycjonowania wewnątrz useEffect powoduje widoczny błysk, przy którym podpowiedź na chwilę pojawia się w niewłaściwym miejscu, zanim trafia we właściwe miejsce. Wykonywanie tych samych obliczeń wewnątrz useLayoutEffect całkowicie unika tego błysku, ponieważ odbywa się to przed tym, jak przeglądarka narysuje cokolwiek.

Istnieje jeszcze trzeci hook w tej rodzinie, który wielu programistów przeocza: useInsertionEffect. Jego zadaniem jest umożliwienie narzędziom typu CSS-in-JS wstawianie reguł stylów do dokumentu przed momentem, w którym efekty układu inaczej odczytałyby przestarzałe informacje stylowe z DOM. Większość programistów nigdy nie będzie go potrzebować bezpośrednio, ale sama świadomość, że jest on częścią cyklu życia efektów w React, wskazuje na głębszą znajomość sposobu, w jaki React 18 radzi sobie z efektami ogólnie.

Przezroczysty może dopytać, co się stanie, jeśli użyjesz useLayoutEffect podczas renderowania po stronie serwera. Odpowiedź brzmi: React wyda ci ostrzeżenie, ponieważ na serwerze nie ma dostępnego DOM do pomiarów. Hook ten po prostu nie jest wykonywany podczas SSR, więc wszelka logika zależna od DOM musi mieć mechanizm ochronny dostępny tylko na stronie klienta albo zostać przeniesiona do useEffect.

Pytanie 3: „Wyjaśnij zachowanie renderowania w React. Kiedy komponent jest ponownie renderowany?”

Odpowiedź, którą łatwo zapomnieć: „Komponent jest ponownie renderowany za każdym razem, gdy zmienia się jego stan lub propsy.”

Mocniejsza odpowiedź: to jest tylko powierzchowność. Bardziej interesujące pytanie brzmi, co właściwie stanowi „zmianę” i co robi React po jej wykryciu.

Komponent jest ponownie renderowany w trzech sytuacjach:

  1. Zmienia się jego własny lokalny stan, zwykle za pośrednictwem funkcji ustawiającej stan
  2. Jego rodzic jest ponownie renderowany, niezależnie od tego, czy przekazane propsy rzeczywiście się zmieniły
  3. Zmienia się wartość kontekstu, którą komponent wykorzystuje

Kluczowym spostrzeżeniem jest drugi punkt: React nie porównuje właściwości przed podjęciem decyzji o ponownym renderowaniu komponentu potomnego. Jeśli komponent nadrzędny się renderuje, to zgodnie z projektem renderują się również jego komponenty potomne. Porównywanie właściwości wiąże się z kosztami obliczeniowymi, a w większości rzeczywistych przypadków komponent potomny i tak musi zostać zaktualizowany, więc pominięcie tego porównania jako standardu stanowi rozsądny kompromis.

Dokładnie w takich sytuacjach programiści sięgają po React.memo, często błędnie. Sama mechanizm memoizacji ma swoje koszty – React nadal musi przeprowadzić porównanie właściwości podczas każdego renderowania. Jeśli te właściwości to złożone obiekty lub jeśli komponent, który jest otaczany, i tak jest tani w renderowaniu, to jego otoczenie funkcją React.memo może w rzeczywistości pogorszyć wydajność zamiast jej poprawić.

Prawdziwa umiejętność polega na wiedzy, kiedy optymalizacja jest rzeczywiście konieczna. Praktyczną zasadą jest unikanie stosowania mechanizmów memoizacji, dopóki nie zmierzymy konkretnego problemu. Najpierw użyj narzędzia React DevTools Profiler, aby znaleźć prawdziwe wąskie gardła, a dopiero potem zastosuj React.memo, useMemo lub useCallback w sposób celowy. Przedwczesna optymalizacja w React zazwyczaj oznacza pracę przeciwko projektowi frameworka, a nie z nim.

Konkurencyjne renderowanie w React 18 dodaje do tego kolejny element. Renderowanie może teraz być przerywane, uprzywilejowywane lub nawet odrzucane w trakcie wykonywania. Zrozumienie, że renderowanie nie zawsze przekłada się bezpośrednio na synchroniczną aktualizację DOM, jest kluczowe przy pisaniu kodu, który poprawnie funkcjonuje w warunkach konkurencyjnego renderowania.

Pytanie 4: Jak należy zarządzać stanem w dużej aplikacji React?

Słaba odpowiedź traktuje to jako kwestię narzędzi: używaj Redux do wszystkiego, co ma charakter globalny, a useState do elementów lokalnych.

Lepsza odpowiedź zaczyna się od zastanowienia, jaki rodzaj stanu jest zaangażowany i kto faktycznie od niego zależy, zanim wybierze się jakąkolwiek bibliotekę.

Stan w dużej aplikacji zazwyczaj dzieli się na cztery grupy:

  • Lokalny stan interfejsu: wartości formularzy, przełączniki, informacja o otwartym modalu. W tym przypadku wystarczy useState.
  • Stan serwera: dane pobrane z backendu. Do tego celu stworzono React Query lub SWR, ponieważ już rozwiązują one problemy z buforowaniem, eliminacją duplikatów, ponownym pobieraniem danych w tle oraz aktualizacjami optymistycznymi — funkcjonalności, których Redux nigdy nie miał na celu zapewnienia.
  • Wspólny stan klienta: takie elementy jak preferencje użytkownika, status autoryzacji czy flagi funkcjonalne. Kontekst jest wystarczający, gdy aktualizacje są rzadkie; Zustand lub Jotai nadają się lepiej, gdy stan zmienia się często lub składa się z większej liczby elementów.
  • Stan URL: filtry, paginacja i parametry wyszukiwania. Powinien znajdować się w URL, a nie w magazynie stanu, ponieważ umożliwia tworzenie linków do udostępniania oraz prawidłową nawigację za pomocą przycisku „wstecz” bez konieczności dodawania dodatkowego kodu.
  • Częstym błędem jest od samego początku projektu wybieranie Reduxa jako rozwiązania domyślnego. Redux doskonale sprawdza się w przypadku naprawdę złożonej logiki po stronie klienta z wieloma wzajemnie powiązanymi aktualizacjami, ale większość aplikacji tak naprawdę nie ma tego problemu – to, co wygląda jak stan po stronie klienta, często jest w rzeczywistości tylko zamaskowanym stanem serwera. Przechowywanie odpowiedzi API w Reduxie jest porównywalne z używaniem młota, by powiesić obraz: da się to zrobić, ale wymaga to znacznie więcej wysiłku, niż tego wymaga zadanie.

    Gdy Redux jest rzeczywiście konieczny, dobrym podejściem jest połączenie Redux Toolkit z RTK Query. RTK Query przejmuje obowiązki związane ze stanem serwera, pozwalając Reduxowi zarządzać jedynie tą logiką po stronie klienta, która jest faktycznie skomplikowana. Rozdzielenie tych zadań ułatwia zrozumienie całej architektury.

    Rozdział 2: Szczegółowe omówienie JavaScripta

    Pytanie 5: Wyjaśnij zamykania w JavaScriptie. Podaj praktyczny przykład.

    Odpowiedź na poziomie powierzchniowym opisuje zamykanie jako zwykłą funkcję, która przechowuje zmienne z otaczającego jej zakresu.

    Głębsza odpowiedź łączy to z zakresem leksykalnym: gdy tworzy się funkcję, ona przechwytuje referencje do zmiennych wokół niej w tym momencie i zachowuje dostęp do nich nawet po tym, jak wykonywanie przechodzi poza ten pierwotny zakres.

    To, co odróżnia dobrego kandydata, to umiejętność wyjaśnienia, dlaczego to zachowanie ma znaczenie właśnie w React. Rozważmy wzorzec, który często powoduje błędy:

    function Counter() {
      const [count, setCount] = useState(0);
    useEffect(() => {
        const timer = setInterval(() => {
          console.log(count); // Always logs 0
          setCount(count + 1); // Resets to 1 every time
        }, 1000);
      }, []); // Empty deps = closure over initial count
    }
    

    Zmienna count używana wewnątrz setInterval pozostaje nieruchoma i ma tę samą wartość, jaka istniała podczas pierwszego renderowania. Ponieważ efekt ten jest wykonywany tylko raz, dzięki pustej tablicy zależności, ta zamknięta funkcja nigdy nie jest aktualizowana nowymi wartościami. Proste dodanie count do listy zależności również nie jest właściwym rozwiązaniem, ponieważ spowodowałoby to rozbiór i ponowną konstrukcję interwału przy każdej aktualizacji. Prawidłowym rozwiązaniem jest użycie funkcji aktualizacyjnej typu funkcyjnego, setCount(c => c + 1), która całkowicie unika polegania na przestarzałej zamkniętej funkcji.

    Zamknięcia powodują również konflikty z słuchaczami zdarzeń wewnątrz niestandardowych hooków. Ilekroć słuchacz jest dodawany wewnątrz useEffect i odczytuje dane ze stanu, zaangażowane jest zamknięcie. Powszechną techniką dla hooka w stylu useEventListener jest przechowywanie funkcji obsługi w ref, dzięki czemu słuchacz może zawsze odczytywać najnowszą wersję bez konieczności ponownego dodawania go.

    To wszystko nie czyni z zamknieć czegoś, czego należy unikać — są to kluczowe mechanizmy wart opanowania. Wzory modułowe, zmienne prywatne, funkcje fabryczne oraz technika kurryingu wszystkie od nich zależą. Ważną umiejętnością jest rozpoznawanie dokładnego momentu tworzenia zamknięcia oraz upewnianie się, że przechowuje ono wartość, która rzeczywiście jest zamierzona.

    Pytanie 6: Co to jest pętla zdarzeń? Wyjaśnij mikrozadania i makrozadania.

    Powierzchowna odpowiedź stwierdza, że pętla zdarzeń zarządza pracą asynchroniczną, a mikrozadania są wykonywane przed makrozadaniami.

    Bardziej szczegółowa odpowiedź wyjaśnia, że JavaScript jest wykonywany na pojedynczej nitce, a przeglądarka symuluje równoczesność za pomocą pętli zdarzeń. Kod synchroniczny jest wykonywany na stosie wywołań; gdy napotka się operację asynchroniczną, jest ona przekazywana do Web API — setTimeout, fetch, zdarzeń DOM — a po zakończeniu tej pracy jej funkcja zwrotna trafia do kolejki.

    Należy zwrócić uwagę na to, że nie istnieje tylko jedna kolejka. Makrotaski — setTimeout, setInterval, operacje I/O — trafiają do jednej kolejki, natomiast mikrotaski — Promise.then, queueMicrotask, MutationObserver — do innej. Gdy stos wywołań zostanie opróżniony, pętla zdarzeń usuwa całą kolejkę mikrotasków, zanim prze procesuje choćby jeden makrotask.

    To stwarza poważne ryzyko: mikrotaski mogą „zahamować” resztę programu. Jeśli nowe mikrotaski są dodawane rekurencyjnie do kolejki, czekające na wykonanie funkcje powrotnicze setTimeout nigdy nie dostaną swojej szansy. Taka sytuacja może sparaliżować interfejs użytkownika, gdy Promises są łączone w pętli bez oddawania kontroli z powrotem do przeglądarki.

    To bezpośrednio wiąże się z tym, jak React grupuje aktualizacje stanu. W React 18 aktualizacje stanu są automatycznie grupowane, niezależnie od tego, skąd pochodzą – z wnętrza setTimeout, z wnętrza Promise lub z wnętrza natywnego obsługiwanego wydarzenia. Wcześniej, przed React 18, aktualizacje uruchamiane wewnątrz setTimeout były stosowane pojedynczo, zamiast być grupowane. Zrozumienie pętli wydarzeń wyjaśnia, dlaczego automatyczne grupowanie w React 18 jest tak istotne: pozwala ono na połączenie się z kolejką mikrozadań, dzięki czemu wszystkie oczekujące aktualizacje są przetwarzane jednocześnie przed następnym renderowaniem.

    Możliwe jest również, że poproszą cię o przewidzenie wyniku krótkiego fragmentu kodu takiego jak ten:

    console.log('1');
    setTimeout(() => console.log('2'), 0);
    Promise.resolve().then(() => console.log('3'));
    console.log('4');
    

    Oczekiwana odpowiedź to: 1, 4, 3, 2 – najpierw wykonywane są instrukcje synchroniczne, następnie mikrozadania takie jak callback Promise, a dopiero potem uruchamia się makrozadanie zapisane w setTimeout.

    Pytanie 7: „Wyjaśnij this w JavaScript. W czym różni się od innych języków?”

    Słaba odpowiedź: „this wskazuje na obiekt, który wywołał funkcję.”

    Mocna odpowiedź: „this w JavaScript opiera się na dynamicznym zasięgu dostępu, a nie na leksykalnym. W większości języków wartość self lub this jest ustalana w momencie definiowania funkcji. JavaScript natomiast określa ją w momencie wywołania, na podstawie sposobu jej uruchomienia, a nie miejsca, w którym znajduje się w kodzie źródłowym.”

    „Istnieje kolejność priorytetów czterech reguł wiązania:

    1. Nowe wiązanie: new Foo() ustawia this na świeżo utworzoną instancję
    2. Jawnie określone wiązanie: foo.call(obj), foo.apply(obj) lub foo.bind(obj) zmuszają this do odnoszenia się do przekazanego obiektu
  • Związek domyślny: wywołanie obj.foo() sprawia, że this staje się równe obj
  • Związek standardowy: proste wywołanie foo() pozostawia this jako undefined w trybie strict, w przeciwnym razie używany jest globalThis"
  • "Funkcje strzałkowe celowo łamią ten wzorzec — dziedziczą this leksykalnie od kontekstu, który je otacza. Właśnie dlatego deweloperzy używali funkcji strzałkowych w komponentach React opartych na klasach, zanim pojawiły się hooki: unikali w ten sposób konieczności wywoływania .bind(this) w konstruktorze."

    "Kod w React oparty na hookach rzadko dotyka bezpośrednio this, ponieważ komponenty nie są już klasami. Mimo to koncepcja ta pojawia się ponownie, gdy konserwujesz starsze komponenty klasowe, integrujesz biblioteki zewnętrzne lub masz do czynienia z rozmówcą, który chce sprawdzić twoją znajomość podstaw JavaScriptu. Powszechną pułapką w praktyce jest przekazywanie metody obiektu jako funkcji zwrotnej — na przykład przekazywanie obj.handleClick do słuchacza zdarzeń — co usuwa jej ukryte powiązanie i sprawia, że this wskazuje w nieoczekiwanym miejscu."

    Pytanie 8: "Czym są obietnice w JavaScript? Wyjaśnij async/await."

    Słaba odpowiedź: "Obietnice zarządzają pracą asynchroniczną, a async/await to po prostu cukier składniowy nad nimi."

    Decydująca odpowiedź: „Obietnica reprezentuje wartość, która jeszcze nie istnieje, ale w końcu zostanie ustanowiona. Zastępuje skomplikowane łańcuchy funkcji zwrotnych interfejsem umożliwiającym łączenie elementów oraz spójnym sposobem na określenie sukcesu lub porażki.”

    „To, co sprawia, że obietnice są naprawdę przydatne, to nie składnia, lecz gwarancje stojące za nimi. Gdy obietnica zostanie zrealizowana – czy to pomyślnie, czy nie – ten wynik zostaje zamknięty i nie może się już zmienić. To właśnie ta niezmienność sprawia, że można je łączyć i łatwiej o nich myśleć.”

    Mówienie, że async/await to „tylko ozdoba”, nie oddaje jego prawdziwego znaczenia – ono fundamentalnie zmienia sposób pisania logiki asynchronicznej, pozwalając jej wyglądać jak kod synchroniczny i znacznie ułatwiając jej zrozumienie. Niemniej jednak wprowadza kilka pułapek, na które warto zwrócić uwagę.”

    // This runs sequentially - 6 seconds total
    async function sequential() {
      const a = await fetch('/a'); // 3s
      const b = await fetch('/b'); // 3s
    }
    // This runs in parallel - 3 seconds total
    async function parallel() {
      const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
    }
    

    "Częstym błędem wśród mniej doświadczonych programistów jest umieszczanie await wewnątrz pętli, co nieumyślnie zmusza operacje do wykonywania się jedna po drugiej, zamiast równolegle. Rozwiązaniem jest użycie Promise.all, gdy operacje nie są od siebie zależne, a pętlę for...of z await należy zachować w przypadkach, gdy wymagana jest ścisła sekwencja wykonywania operacji."

    ">Kierowanie błędami to kolejna kwestia, w której mogą wystąpić problemy. Otoczenie wywołania await w blokach try/catch pozwala przechwycić jego odrzucenie, natomiast pominięcie tego otoczenia może spowodować, że nieprzechwycony błąd doprowadzi do awarii procesu Node.js. W środowisku frontendowym otaczanie wywołań asynchronicznych obszarami obsługi błędów lub korzystanie z biblioteki takiej jak React Query, która deklaratywnie zarządza stanami błędów, pozwala uniknąć takiego scenariusza awarii."

    Rozdział 3: Projekt systemu i architektura

    Pytanie 9: „Zaprojektuj edytor dokumentów w czasie rzeczywistym do współpracy, podobny do Google Docs.”

    To pytanie całkowicie zmienia kierunek rozmowy. Pytający nie bada już twojej wiedzy o React – chce zobaczyć, jak rozumiesz architekturę systemów.

    Solidne podejście wygląda tak:

    „Zanim napiszę choć jedną linię kodu, określę wymagania:

    • Ile osób będzie edytować jednocześnie? Projekt dla 10 użytkowników nie przypomina projektu dla 10 000.
    • Jaka latencja jest do przyjęcia – prawdziwy czas rzeczywisty, czy coś bliższego niemal czasowi rzeczywistemu?
    • Czy aplikacja musi działać offline?
    • Jaką strategię rozwiązywania konfliktów wybieramy?”

    „Z perspektywy frontendu:

    • Zarządzanie stanem: każdy klient przechowuje własną lokalną kopię dokumentu. Edycje są najpierw stosowane optymistycznie na poziomie klienta, a następnie wysyłane do serwera, który przekazuje je wszystkim pozostałym połączonym klientom.
    • Transformacja operacyjna lub CRDT: to mechanizm służący do rozwiązywania konfliktów pomiędzy edycjami. OT była pierwotną techniką Google, ale wymaga centralnego serwera do ustalania kolejności działań. CRDT (Conflict-free Replicated Data Types) mogą natomiast funkcjonować w modelu peer-to-peer, a narzędzia takie jak Yjs sprawiły, że stały się coraz powszechniejsze.
  • Integracja z React: komponent edytora renderuje się na podstawie wspólnego stanu dokumentu. Przysłane zdalne operacje docierają przez połączenie WebSocket i przechodzą przez etap transformacji, zanim zaktualizują lokalny stan. Przechowywanie instancji edytora w ref zamiast w stanie React zapobiega ponownemu renderowaniu przy każdym naciśnięciu klawisza — stan React jest przeznaczony wyłącznie do elementów widocznych dla użytkownika, takich jak pozycja kursora, awatary współpracowników i wskaźniki obecności.
  • Wydajność: wirtualizacja renderowania dla dużych dokumentów, grupowanie aktualizacji interfejsu za pomocą requestAnimationFrame oraz ograniczenie częstotliwości wysyłanych zapytań sieciowych, dzięki czemu nie wysyła się żadnego zapytania przy każdym naciśnięciu klawisza.
  • "Na warstwie synchronizacji:

    • WebSocket zajmuje się transmisją w czasie rzeczywistym
    • Server-Sent Events lub długie polling służą jako alternatywa, jeśli nie uda się nawiązać połączenia WebSocket
  • Dane dotyczące obecności — kto jest online i gdzie znajduje się ich kursor — są przekazywane przez własny, lekki kanał oddzielony od treści dokumentu"
  • "Prawdziwie trudna część tego problemu nie ma nic wspólnego z renderowaniem w React — chodzi o model spójności leżący u jego podstaw. Co powinno się stać, gdy dwie osoby wpisują tekst dokładnie w tej samej pozycji kursora w tym samym momencie? Odpowiedź zależy wyłącznie od tego, czy wybrano OT, czy CRDTs, a ta jedna decyzja wpływa na niemal wszystkie pozostałe wybory architektoniczne."

    Pytanie 10: "Jak zoptymalizować aplikację React, która wolno się ładuje i z której trudno korzystać?"

    Niezadowalająca odpowiedź: wymienienie szeregu technik — memoizacji, ładowania opóźnionego, dzielenia plików — bez żadnego kontekstu.

    Odpowiedź, która wyróżnia się: „Zacząłbym od pomiarów zamiast domysłów. Narzędzie React DevTools Profiler oraz panel Performance w Chrome DevTools pokazują, czy problemem jest czas ładowania, czas renderowania, czy oba — i nie ma sensu optymalizować ślepo.”

    Jeśli chodzi o ładowanie:

    • Dzielenie kodu: Podział według tras za pomocą React.lazy i Suspense to standard, ale nie powinno na tym się kończyć. Ciężkie komponenty, które nie są od razu widoczne — modale, treść poniżej linii widoku — również zasługują na osobne punkty podziału.
    • Przedładowywanie: Użyj <link rel="preload"> dla kluczowych zasobów i połącz React.lazy z funkcją prefetching dla tras, które użytkownik prawdopodobnie odwiedzi w następnej kolejności.
  • Analiza pliku: Uruchom webpack-bundle-analyzer, aby zidentyfikować zbędne elementy. Często zdarza się, że plik ma rozmiar 2 MB, z czego 1,5 MB pochodzi od pojedynczej biblioteki do tworzenia wykresów, której używa tylko jedna strona.
  • Tree shaking: Upewnij się, że importy nie powodują efektów ubocznych i są napisane jako moduły ES. Użycie import lodash from 'lodash' zamiast import debounce from 'lodash/debounce' może sprawić, że rozmiar końcowego pliku zmniejszy się o 100 KB.
  • Jeśli chodzi o interakcje:

    • Wirtualizacja: Gdy lista zawiera około 50 elementów, warto skorzystać z react-window lub react-virtualized. Wyświetlanie jednocześnie 10 000 węzłów DOM nigdy nie będzie szybkie, bez względu na efektywność reszty kodu.
    • Dyscyplinowana strategia memoizacji: Najpierw przeanalizuj, potem działaj. Otaczaj kosztowne obliczenia funkcją useMemo, drogie callbacki funkcją useCallback, a komponenty, które renderują się niepotrzebnie, funkcją React.memo. Memoizowanie wszystkiego domyślnie, bez oceny rzeczywistego wpływu, zazwyczaj powoduje dodatkowe obciążenie, zamiast je zmniejszyć.
    • Lokalizacja stanu: Trzymaj stan jak najbliżej komponentu, który go faktycznie używa. Przenoszenie stanu do wspólnego przodka tylko dlatego, że wydaje się to uporządkowane, powoduje dodatkowe renderowania za każdym razem, gdy stan się zmienia.
    • Dzielenie kontekstów: Gdy jeden kontekst łączy aktualizacje wysokiej częstotliwości — takie jak pozycja myszy — z aktualizacjami niskiej częstotliwości — takimi jak stan autoryzacji — podziel go na dwa. W przeciwnym razie każdy ruch myszy zmusza do ponownego renderowania wszystkich komponentów korzystających z tego kontekstu, nawet tych, które interesują się tylko autoryzacją.

    O odczuwanej wydajności:

    • Ekrany szkieletowe zamiast elementów spinujących sprawiają, że interfejs wydaje się szybszy, ponieważ treść pojawia się stopniowo, a nie naraz.
    • Stopniowe ładowanie za pomocą mechanizmu Suspense z React 18 umożliwia pierwsze załadowanie kluczowej treści, podczas gdy sekcje drugorzędne ładowane są później.
    • Warto śledzić wskaźnik Interaction to Next Paint, nowszy pomiar z serii Core Web Vital firmy Google. Zastępuje on First Input Delay, ponieważ mierzy responsywność w całym cyklu życia strony, a nie tylko podczas pierwszej interakcji. Celem jest zapewnienie, aby obsługa zdarzeń wykonywała się w czasie krótszym niż 200 milisekund.

    Literatura pokrewna

  • Dziesięć praktycznych workflowów Power Automate, które oszczędzają czas co tydzień — Poznaj dziesięć praktycznych schematów pracy Power Automate – od wydobywania danych z plików PDF po kierowanie zatwierdzeniami – które co tydzień eliminują godziny powtarzalnej pracy administracyjnej.
  • Jak dataLayer GA4 określa, czy twoje analizy są prawdziwe — Wyjaśnia, w jaki sposób dataLayer łączy strony internetowe i GTM z GA4, dlaczego surowe wydarzenia zakłócają raportowanie w e-commerce oraz jak prawidłowo strukturyzować i debugować dane przesyłane do systemu.
  • Projektowanie dla cache'u tylno-przódowego: Eligibilita, odnowa i stan — Dowiedz się, jak cache tylno-przódowy przeglądarki przywraca całe strony, co go w tle blokuje oraz jak obsługiwać przywrócony stan za pomocą funkcji pageshow i pagehide.
  • Sześć technik TypeScriptu, które przekształcają type'y w rzeczywistą ochronę przed błędami — Dowiedz się, w jaki sposób funkcje satisfies, zagnieżdżone unie, mechanizm never checks, typ unknown, typy pochodne oraz branded IDs pozwalają TypeScriptowi wykrywać prawdziwe błędy już na etapie kompilacji, a nie w środowisku produkcyjnym.
  • Sześć własnych cech HTML, które zastępują powszechne biblioteki JavaScript do interfejsu — Dowiedz się, w jaki sposób popover, exclusive details, dialog, Declarative Shadow DOM, fetchpriority i datalist zastępują własny kod JavaScript, a także jakie ograniczenia nadal mają każde z nich.