SolidJS i Qwik w akcji: lekkie frameworki frontendowe dla wymagających projektów

0
83
Rate this post

Nawigacja:

Gdzie kończy się React, a zaczyna problem – kontekst dla SolidJS i Qwik

Typowe bóle dużych projektów na React

React sprawdza się świetnie w średnich projektach, ale przy naprawdę wymagających aplikacjach frontendowych zaczynają wychodzić na wierzch powtarzalne problemy. Najczęściej pojawiają się cztery zjawiska, które uderzają w Core Web Vitals i ogólne odczucie szybkości:

  • zimny start (cold start) – pierwsze wejście na stronę kończy się długim czekaniem na JS, który musi się pobrać, sparsować, wykonać, a następnie zhydratować aplikację;
  • kosztowna hydratacja – serwer wyrenderował HTML, ale zanim UI stanie się interaktywny, React musi „przejąć” każde zdarzenie i zbudować wirtualne drzewo komponentów w przeglądarce;
  • rozrastające się bundlery – każdy kolejny feature dorzuca kod JS, który ląduje w głównym bundlu lub w zbyt dużych chunkach;
  • lagujące UI przy większej ilości stanu – globalne store’y, duże formularze, listy z setkami elementów powodują mikro‑lagi przy każdej aktualizacji.

Jedna z iluzji typowych dla Reacta polega na tym, że dopóki pracuje się lokalnie na mocnym laptopie, wszystko wygląda dobrze. Problemy widać dopiero na słabszych urządzeniach, przy kiepskim łączu i źle zoptymalizowanym cache. Wtedy każdy dodatkowy kilobajt JS, każda warstwa abstrakcji, każdy niepotrzebny re-render zaczyna być odczuwalny. SolidJS i Qwik powstawały w dużej mierze jako odpowiedź właśnie na te zjawiska, a nie jako kolejne „frameworki dla sportu”.

Mit, który warto tu rozbroić: „React jest wystarczająco szybki, skoro używa go X i Y, wystarczy używać lazy loadingu”. Rzeczywistość jest mniej wygodna – w pewnej skali lazy loading tylko opóźnia ból. Jeśli architektura opiera się na ciężkich komponentach, nieoptymalnym przepływie danych i globalnym stanie, to nawet agresywny code splitting przestaje ratować UX.

Co naprawdę spowalnia nowoczesny frontend

Winą za wolne aplikacje obarcza się często sam framework, ale to zwykle skrót myślowy. W praktyce na wydajność mają wpływ głównie cztery elementy:

  • sieć – rozmiar i liczba requestów, brak lub zła konfiguracja cache, brak kompresji, brak HTTP/2/3;
  • czas parsowania i wykonywania JS – im większy bundle, tym dłuższy czas TTI (Time to Interactive);
  • koszt hydratacji – odtworzenie struktury komponentów po stronie klienta na podstawie serwerowego HTML;
  • przeciążenie głównego wątku – blokujące operacje, niepotrzebne re‑rendery, złożone obliczenia w renderze.

SolidJS i Qwik „uderzają” w różne miejsca tego łańcucha. Solid redukuje koszt aktualizacji UI dzięki fine‑grained reactivity i braku virtual DOM. Qwik ekstremalnie ogranicza koszt hydratacji dzięki resumability i agresywnemu lazy loadingowi, który działa na poziomie frameworka, a nie tylko na poziomie pojedynczych komponentów.

Ciekawostka z praktyki: w kilku migrowanych projektach, które zaczynały jako klasyczne SPA na React, najwięcej zyskała nie tyle sama liczba FPS, ile odczucie „klikam i od razu działa” na tanich smartfonach. To właśnie efekt mniejszego JS na start i niższego kosztu inicjalizacji.

Kiedy lazy loading w React przestaje wystarczać

Code splitting w React (np. z użyciem React.lazy i Suspense) rzeczywiście pomaga, ale tylko do pewnego momentu. Problemy zaczynają się tam, gdzie:

  • znaczna część aplikacji jest interaktywna od razu – np. dashboardy, narzędzia B2B, edytory;
  • od początku trzeba zhydratować duże drzewo komponentów, bo użytkownik może wejść w interakcje w wielu miejscach;
  • wzorce architektoniczne doprowadziły do mocno sprzężonych komponentów, które trudno „rozciąć”;
  • aplikacja mocno opiera się na globalnych store’ach (Redux, Zustand, MobX), które inicjalizują się w całości na start.

Lazy loading komponentów przynosi zyski głównie przy dużych, rzadko używanych fragmentach UI (np. modal konfiguracji, panel analityczny). Nie rozwiązuje natomiast problemu podstawowego: „dlaczego w ogóle muszę tyle JS zainicjalizować po wejściu na stronę?”. Qwik odpowiada: nie musisz, możesz wznowić stan kiedy będzie naprawdę potrzebny. SolidJS odpowiada: jeśli już coś musi działać po stronie klienta, niech będzie aktualizowane jak najtaniej.

Rosnące znaczenie lekkich frameworków

Presja na wydajność frontendu rośnie z kilku kierunków jednocześnie:

  • Core Web Vitals – wskaźniki takie jak LCP, FID (lub Interaction to Next Paint) i CLS mają bezpośrednie przełożenie na SEO i konwersję;
  • edge rendering – platformy takie jak Vercel, Netlify czy Cloudflare Workers premiują rozwiązania zoptymalizowane pod SSR/ISR i niskie zasoby;
  • mobile‑first realnie, nie tylko w CSS – rośnie liczba użytkowników na słabszych telefonach, w realnych warunkach sieci;
  • koszty utrzymania – mniej JS to mniej problemów z debuggingiem, mniej regresji wydajności i tańsze zasoby obliczeniowe.

Stąd zainteresowanie frameworkami, które nie są „ciężkimi kombajnami” jak typowe SPA, ale też nie są powrotem do epoki jQuery. SolidJS i Qwik reprezentują dwie bardzo konkretne filozofie: maksymalnie precyzyjna reaktywność oraz maksymalna leniwość ładowania i hydratacji. Oba podejścia dobrze wpisują się w obecne wymagania biznesowe i techniczne.

SolidJS i Qwik w jednym kadrze – przegląd i filozofia

SolidJS – fine‑grained reactivity bez virtual DOM

SolidJS to framework, który łączy znajome API w stylu Reacta z mechanizmem reaktywności wywodzącym się bardziej z Knockout, Svelte czy MobX. Kluczowe cechy:

  • brak virtual DOM – Solid nie diffuje drzewa komponentów przy każdej zmianie stanu; zamiast tego tworzy graf zależności i aktualizuje dokładnie te fragmenty DOM, które muszą się zmienić;
  • fine‑grained reactivity – najmniejszą jednostką obserwacji jest pojedynczy sygnał (signal), a nie cały komponent;
  • API podobne do Reacta – JSX, komponenty funkcyjne, hook‑opodobne prymitywy typu createSignal, createEffect;
  • statyczna kompilacja JSX – JSX jest kompilowany do wydajnych wywołań operujących bezpośrednio na DOM.

Model SolidJS odciąża główny wątek, ponieważ nie wykonuje kosztownych re‑renderów całych poddrzew. Zmiana jednego sygnału skutkuje aktualizacją jednego tekstu, a nie przeliczeniem wszystkiego „po drodze”. Z punktu widzenia programisty całość wygląda jednak znajomo – zamiast useState i useEffect są createSignal i createEffect, a reszta konceptów jest bliska Reactowi.

Qwik – resumability i „leniwy do granic możliwości” frontend

Qwik idzie w inną stronę. Jego głównym celem jest resumability – możliwość przerwania wykonywania aplikacji po stronie serwera i wznowienia jej po stronie przeglądarki bez pełnej hydratacji. Oznacza to, że:

  • przeglądarka dostaje już gotowy stan i event‑handlery zakodowane w atrybutach HTML;
  • nie ma potrzeby inicjalizować całego drzewa komponentów na starcie, aby reagować na pierwsze interakcje;
  • kod JS ładuje się w mikro‑pakietach, dopiero gdy konkretny fragment UI tego wymaga.

Qwik stosuje framework‑level lazy loading. Każdy komponent, każda funkcja event handlera jest potencjalnym kandydatem do wydzielenia w osobny chunk. Developer nie musi ręcznie dzielić kodu – robi to kompilator na bazie importów i specjalnego mechanizmu QRL (Qwik Resource Locator). Filozofia Qwik to: ładować i wykonywać tylko to, czego użytkownik faktycznie dotknie.

Mit: „Qwik to po prostu kolejny framework SSR z hydratacją”. Rzeczywistość: Qwik nie „hydratyzuję” w klasycznym sensie. Zamiast odtwarzać stan w pamięci po stronie klienta, korzysta z zapisanego w HTML kontekstu i „wznawia” aplikację na tyle, na ile to potrzebne dla danej interakcji.

Porównanie SolidJS i Qwik – kluczowe różnice

CechaSolidJSQwik
Model reaktywnościFine‑grained signals, graf zależnościStores + tasks, event‑driven, resumability
Virtual DOMBrak, bezpośrednia praca na DOMBrak klasycznego VDOM, praca na zserializowanym stanie
SSRSSR + streaming (SolidStart i integracje)SSR + pełna resumability, gotowe pod edge
HydratacjaKlasyczna hydratacja, ale lżejsza dzięki precyzyjnej reaktywnościBrak pełnej hydratacji, wznawianie tylko potrzebnych części
EkosystemBardzo aktywny, mniejszy niż React, rosnącyMłody, ściśle związany z Qwik City
Próg wejścia dla React devówNiski – JSX + podobne API do hookówWyższy – nowe koncepcje (QRL, tasks, resumability)
Typowe use case’ySPA, dashboardy, interaktywne aplikacje, microfrontendsStrony marketingowe, e‑commerce, aplikacje z naciskiem na TTFB i TTI

Kiedy lepiej sięgnąć po SolidJS, a kiedy po Qwik

SolidJS zwykle wygrywa tam, gdzie:

  • potrzebne jest bogate, stale aktywne UI – np. panel administracyjny, narzędzie analityczne;
  • aplikacja ma intensywny klientowy stan, który często się zmienia (formularze, interakcje drag&drop, live preview);
  • chce się przejść z Reacta na coś wydajniejszego przy minimalnej zmianie sposobu myślenia.

Qwik natomiast lśni w scenariuszach, gdzie:

  • priorytetem jest pierwsze wrażenie i Core Web Vitals – landing pages, e‑commerce, portale contentowe;
  • duża część użytkowników jest „spacerowiczami”, którzy wykonują kilka prostych interakcji i odchodzą;
  • liczy się skalowanie na edge (Vercel, Netlify, Cloudflare Workers) z minimalnym kosztem wykonania JS po stronie klienta.

Mit, który często się pojawia: „nowy framework to zabawka do pet projectów, do produkcji się nie nadaje”. Tymczasem SolidJS i Qwik działają w produkcji w realnych firmach – od mniejszych SaaS‑ów po serwisy nastawione na SEO i konwersję. Organizacje, które na poważnie liczą milisekundy, inwestują w takie narzędzia właśnie dlatego, że klasyczne SPA przestają im się domykać biznesowo.

Zbliżenie na kolorowy kod HTML na ekranie monitora
Źródło: Pexels | Autor: Pixabay

Model reaktywności SolidJS – od sygnałów do widoków

Podstawowe prymitywy: createSignal, createEffect, createMemo, context

SolidJS opiera się na prostym, ale bardzo mocnym zestawie prymitywów reaktywności:

  • createSignal – odpowiednik useState, ale działający na poziomie pojedynczej wartości i jej obserwatorów;
  • createEffect – odpowiednik useEffect, który automatycznie śledzi sygnały użyte wewnątrz i reaguje na ich zmiany;
  • createMemo – wydajny sposób na obliczanie pochodnych wartości na podstawie sygnałów;
  • Context – mechanizm do przekazywania danych w głąb drzewa komponentów bez prop drillingu.

Przykład prostego licznika w SolidJS:

import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);

  const increment = () => setCount(count() + 1);

  return (
    <button onClick={increment}>
      Kliknięto: {count()}
    </button>
  );
}

Różnica względem Reacta jest subtelna, ale fundamentalna: count jest funkcją, a nie wartością. Wywołanie count() rejestruje aktualny kontekst jako obserwatora tego sygnału. Gdy stan się zmieni, Solid wie dokładnie, który fragment drzewa DOM ma zostać zaktualizowany – bez re‑renderu komponentu jako całości.

Reaktywność fine‑grained vs. re‑render komponentów

React przy każdej zmianie stanu wykonuje funkcję komponentu ponownie, porównuje poprzednie i nowe drzewo JSX (virtual DOM), a następnie aplikuje różnice do prawdziwego DOM. Solid działa inaczej:

Reaktywność jako graf zależności, a nie cykl renderu

Solid buduje w trakcie wykonywania kodu graf zależności pomiędzy sygnałami, memami a efektami. Każdy odczyt sygnału w obrębie aktywnego kontekstu (np. createEffect) powoduje zarejestrowanie zależności. Aktualizacja sygnału uruchamia tylko te węzły grafu, które faktycznie z niego korzystają. Nie odpala się „globalny” cykl re‑renderu, nie ma potrzeby przeliczania całego JSX.

Mit: „fine‑grained reactivity jest nieprzewidywalna i trudna do debugowania”. W praktyce jest odwrotnie – skoro zmiana jednego sygnału rusza tylko kilka efektów, to dużo łatwiej prześledzić drogę stanu niż w aplikacji, która re‑renderuje hurtowo całe poddrzewo. Debugowanie sprowadza się często do sprawdzenia, który efekt lub memo reaguje nie tak, jak trzeba.

Dobrym przykładem jest lista z filtrowaniem i paginacją. W Reactowym podejściu łatwo doprowadzić do sytuacji, gdzie zmiana jednego inputa powoduje re‑render kilkunastu komponentów. W Solidzie można mieć osobne sygnały dla tekstu filtra, aktualnej strony, liczby elementów na stronę – a efekty i mema policzą tylko to, co od nich zależy.

const [filter, setFilter] = createSignal("");
const [page, setPage] = createSignal(1);
const [pageSize] = createSignal(20);

const filteredItems = createMemo(() =>
  items().filter(item => item.name.includes(filter()))
);

const pagedItems = createMemo(() => {
  const start = (page() - 1) * pageSize();
  return filteredItems().slice(start, start + pageSize());
});

Przy zmianie filter() przelicza się filteredItems i pagedItems, ale nie wszystkie inne części aplikacji. Przy zmianie page() przelicza się już tylko pagedItems.

Kompozycja logiki: derived state i brak „hook rules”

Solid nie ma odpowiednika „Rules of Hooks”. Funkcje reaktywne (sygnały, efekty) nie są wiązane z cyklem życia komponentu w ten sam sposób co w React. Można je tworzyć warunkowo, wewnątrz pętli, w zwykłych funkcjach narzędziowych – byle robić to podczas inicjalizacji logiki, a nie w odpowiedzi na jakiś event po fakcie.

Mit: „skoro nie ma reguł hooków, to łatwo stworzyć nieprzewidywalny bałagan”. Rzeczywistość: bałagan i tak da się stworzyć w każdym frameworku, ale brak sztywnych reguł ułatwia ekstrakcję logiki do custom stores czy modułów. Zamiast kopiować fragmenty hooków pomiędzy komponentami, można zbudować mały moduł reaktywny i użyć go w kilku miejscach.

function createAuthStore() {
  const [user, setUser] = createSignal<User | null>(null);
  const [loading, setLoading] = createSignal(false);

  const isLoggedIn = createMemo(() => !!user());

  const login = async (credentials: Credentials) => {
    setLoading(true);
    const result = await api.login(credentials);
    setUser(result.user);
    setLoading(false);
  };

  const logout = async () => {
    await api.logout();
    setUser(null);
  };

  return { user, loading, isLoggedIn, login, logout };
}

Taki store można wykorzystać w dowolnym komponencie, a także spięć z Contextem na poziomie całej aplikacji, nie martwiąc się o kolejność wywołań hooków.

Kontrola nad efektem: createEffect, onCleanup i pułapki

Efekty w SolidJS działają bardziej jak reaktywne „watchery” niż znane z Reacta useEffect z tablicą zależności. Zależności wykrywane są automatycznie na podstawie odczytów sygnałów w ciele efektu. To wygodne, ale wymusza pewne nawyki:

  • zamiast kombinować z ręcznym zarządzaniem tablicą zależności, wystarczy pilnować, żeby w efekcie odczytywać tylko te sygnały, od których ma on zależeć;
  • wewnętrzne funkcje pomocnicze, które też odczytują sygnały, są częścią grafu – efekt „widzi” wszystkie odczyty wykonane w trakcie swojego działania.
createEffect(() => {
  console.log("Aktualny user:", authStore.user());
});

Czyszczenie subskrypcji, timerów czy nasłuchów wykonuje się przez onCleanup:

createEffect(() => {
  const listener = (e: KeyboardEvent) => {
    if (e.key === "Escape") closeModal();
  };
  window.addEventListener("keydown", listener);

  onCleanup(() => window.removeEventListener("keydown", listener));
});

Problem, który czasem zaskakuje osoby przychodzące z Reacta: efekt w Solidzie odpala się natychmiast po utworzeniu i ponownie przy każdej zmianie zależnych sygnałów. Nie ma tu odpowiednika useEffect(() => ..., []) jako „run once after mount” w obrębie komponentu, ale zwykle to zaleta – logika inicjalizacji jest związana raczej z tworzeniem lub niszczeniem całego drzewa.

Model Qwik – resumability i myślenie o stanie na nowo

Stan jako zserializowana struktura, a nie proces w pamięci

Qwik zakłada, że aplikacja może zostać w dowolnym momencie „uśpiona”, wysłana w HTML do przeglądarki, a następnie obudzona dopiero wtedy, gdy użytkownik wejdzie w interakcję z konkretnym elementem. Żeby to zadziałało, stan musi być zserializowany. Nie można polegać na tym, że w pamięci procesu serwera lub klienta żyje jakiś singleton z danymi.

Stan aplikacji jest trzymany głównie w store’ach – strukturach, które Qwik potrafi serializować i odtwarzać. Zamiast skomplikowanych klas i obiektów z metodami, lepiej trzymać proste dane – liczby, stringi, obiekty bez prototypów. Kod, który manipuluje stanem, jest lazy‑ładowany i podpinany tylko wtedy, gdy potrzeba.

QRL i event‑handlery na żądanie

Kluczowym mechanizmem Qwika jest QRL (Qwik Resource Locator). Każdy event‑handler, funkcja serwisowa czy komponent może zostać „opakowany” w QRL i dzięki temu załadowany osobno. W JSX widać to na przykładzie $():

import { component$, useStore, $ } from '@builder.io/qwik';

export const Counter = component$(() => {
  const state = useStore({ count: 0 });

  const increment = $(() => {
    state.count++;
  });

  return (
    <button onClick$={increment}>
      Kliknięto: {state.count}
    </button>
  );
});

Funkcja przekazana do $() zostaje wyeksportowana jako osobny chunk, a w HTML Qwik umieszcza do niej „adres”. Przeglądarka nie pobiera kodu increment, dopóki użytkownik nie kliknie w przycisk. Wtedy mechanizm eventowy Qwika ładuje odpowiedni fragment JS, uruchamia go z aktualnym stanem i aktualizuje DOM.

Mit: „to tylko inny zapis lazy importu”. Różnica polega na tym, że Qwik robi to na poziomie całego modelu programowania, a nie pojedynczej funkcji, którą developer musi sam podzielić. Nie trzeba myśleć o granicach chunków i ręcznie konfigurować import() – kompilator Qwika radzi sobie z tym automatycznie.

Tasks, effects i strumień życia komponentu

Qwik nie ma dokładnych odpowiedników hooków z Reacta, ale stosuje tasks i effects, które można powiązać z cyklem serwer/klient. Typowy wzorzec to useTask$, useVisibleTask$ i useClientEffect$:

  • useTask$ – działa zarówno po stronie serwera, jak i klienta; dobry do pracy z danymi, reagowania na zmiany wejściowych propsów czy store’ów;
  • useVisibleTask$ – odpala się dopiero, gdy komponent jest widoczny w viewport (po stronie klienta); idealne miejsce na integracje z DOM API, które nie ma sensu dla elementów spoza ekranu;
  • useClientEffect$ – czysto klientowy efekt, np. do integracji z bibliotekami działającymi tylko w przeglądarce.

Przykładowy pattern dla ładowania danych w reakcji na zmianę adresu (np. produktu w sklepie):

export const Product = component$((props: { id: string }) => {
  const state = useStore({ product: null as Product | null, loading: true });

  useTask$(async ({ track }) => {
    const id = track(() => props.id);
    state.loading = true;
    state.product = await fetchProduct(id);
    state.loading = false;
  });

  if (state.loading) return <p>Ładowanie...</p>;

  return <div>{state.product?.name}</div>;
});

Qwik serializuje aktualny state, więc gdy użytkownik odświeży stronę lub wejdzie w nią z wyszukiwarki, serwer odtworzy stan i wyśle już gotowy HTML. Klient wznawia działanie tylko w tych miejscach, które faktycznie będą reagować na interakcję.

Myślenie o „zimnym starcie” na każdym kroku

Tworząc aplikację w Qwiku, trzeba założyć, że klient zawsze może startować „na zimno”. Każda interakcja powinna być odporna na sytuację, w której dopiero co załadował się minimalny fragment JS. To wymusza nieco inne nawyki niż w typowym SPA:

  • logika biznesowa musi być deterministyczna przy danym stanie serializowalnym – brak ukrytych singletonów i globalnych mutowalnych modułów;
  • event‑handlery nie mogą zakładać, że coś już „na pewno” się zainicjalizowało po stronie klienta;
  • komponenty, które nie mają interakcji, nie powinny w ogóle posiadać kodu klientowego.

To podejście szczególnie premiuje projekty, w których gros ruchu stanowią użytkownicy wykonujący pojedyncze akcje: wejście z reklamy, kliknięcie „dodaj do koszyka”, ewentualnie przewinięcie strony. Aplikacje typu „dashboard” też da się w Qwiku napisać, ale wymagają więcej świadomych decyzji przy projektowaniu stanu i przepływów.

Kod HTML na ekranie monitora pokazujący strukturę strony internetowej
Źródło: Pexels | Autor: anshul kumar

Integracja z ekosystemem: Vite, TypeScript, testy i narzędzia

SolidJS + Vite: szybki dev‑server i prosta konfiguracja

Solid ma oficjalny plugin do Vite (vite-plugin-solid), który ogarnia kompilację JSX i optymalizacje specyficzne dla modelu reaktywności. Nowy projekt można utworzyć jednym poleceniem, np. przez npm create solid@latest, co pod spodem generuje konfigurację Vite z ustawionym pluginem.

Domyślna konfiguracja jest lekka – bez rozbudowanego bundlera, skomplikowanych aliasów i nadmiarowych loaderów. W praktyce sprowadza się to do:

// vite.config.ts
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";

export default defineConfig({
  plugins: [solidPlugin()],
  build: {
    target: "esnext",
  },
});

Włączenie SSR czy integracji z serwerem (np. Node, Cloudflare Workers) odbywa się zwykle przez SolidStart lub dedykowane adaptery. Dla prostych aplikacji wystarczy jednak zwykły build SPA lub SSR z prostą warstwą Node.

Qwik + Vite: Qwik City i routing first‑class

Qwik również opiera się na Vite, ale idzie krok dalej, dostarczając Qwik City – warstwę routingu, SSR, integracji z edge i konwencji plików. Tworząc projekt przez npm create qwik@latest, dostaje się skonfigurowane środowisko z:

  • plikiem src/routes/ jako głównym miejscem definiowania stron;
  • SSR gotowym pod Vercel/Netlify/Cloudflare;
  • podziałem kodu na poziomie trasy i komponentu bez dodatkowej konfiguracji.

Konfiguracja Vite jest „opakowana” w plugin Qwika, ale wciąż można dodawać własne pluginy – np. do obsługi MDX, SVG czy specyficznych transformacji. W e‑commerce często przydaje się możliwość łatwego dodania obsługi CMS lub generatora statycznych treści; Qwik City integruje się z tym na poziomie routingu i loaderów danych.

TypeScript jako obywatel pierwszej kategorii

Zarówno SolidJS, jak i Qwik stawiają na TypeScript jako domyślny język. To nie jest tylko „opcja”, ale spójna część doświadczenia:

  • kompilacja JSX w Solidzie jest świadoma typów – propsy komponentów, sygnały i mema można typować statycznie, co mocno ogranicza klasę błędów;
  • Qwik silnie korzysta z typów przy QRL i serializacji stanu – pomaga to unikać przypadków, w których do store’a trafiają nietypowalne konstrukcje (np. instancje klas, funkcje).

W praktyce sensowne jest trzymanie logiki domenowej w czystym TypeScripcie, a warstwę UI w Solid/Qwik traktować jako cienką powłokę. Sprzyja temu fakt, że oba frameworki nie wymagają magicznych dekoratorów czy klas – zwykłe funkcje i obiekty wystarczą.

Testowanie: unit, integracja, testy E2E

W projektach opartych o Reacta częste jest automatyczne przyjmowanie zestawu: Jest + React Testing Library + Cypress/Playwright. SolidJS i Qwik nie wymuszają konkretnego stosu, ale dobrze współpracują z podobnymi narzędziami.

W SolidJS sensownym wyborem jest kombinacja Vitest (test runner + assertion) i solid-testing-library jako odpowiednik RTL. Ze względu na brak VDOM testy komponentów często są szybsze, a snapshoty oddają realny DOM generowany przez framework.

Qwik i testy: interakcje bez pełnego uruchamiania aplikacji

Qwik ma własny zestaw narzędzi testowych, ale dobrze działa także z Vitestem czy Playwrightem. Kluczowe jest to, że logika biznesowa nie musi być testowana przez pryzmat pełnego SPA. Dużą część przypadków da się ogarnąć na poziomie czystych funkcji i store’ów.

Do testowania komponentów można użyć @builder.io/qwik/testing. Pozwala on wyrenderować komponent w środowisku zbliżonym do SSR i wykonywać asercje na wygenerowanym HTML:

import { render } from '@builder.io/qwik/testing';
import { Counter } from './counter';

test('Counter pokazuje domyślną wartość', async () => {
  const { screen } = await render(<Counter />);
  expect(screen.querySelector('button')?.textContent)
    .toContain('Kliknięto: 0');
});

Mit: „Qwik jest trudny do testowania, bo wszystko jest lazy”. Rzeczywistość jest inna – lazy‑loading dotyczy bundlingu i runtime’u w przeglądarce, natomiast w środowisku testowym kod wykonuje się normalnie, jak w każdym innym frameworku korzystającym z Vite/Vitesta.

Przy testach E2E sensownym wyborem jest Playwright – szczególnie wtedy, gdy aplikacja działa na edge/SSR. Qwik City wystawia klasyczny serwer HTTP, więc konfiguracja sprowadza się do uruchomienia dev‑servera przed testami i punktowania tych samych URL‑i, które odwiedza użytkownik. Ciekawym patternem w Qwiku jest świadome testowanie „zimnego startu” – czyli weryfikacja, że kluczowe interakcje działają poprawnie nawet przy wolnym sieciowo dociąganiu event‑handlerów.

Debugowanie i DX: narzędzia przeglądarkowe dla SolidJS i Qwik

SolidJS ma rozbudowany DevTools jako rozszerzenie do Chrome/Firefox. Pokazuje strukturę komponentów, sygnałów i zależności reaktywnych – coś w rodzaju „mapy” tego, kto nasłuchuje na czyje zmiany. Przy bardziej skomplikowanych przepływach, gdzie wiele sygnałów zależy od siebie, to często szybsza droga niż śledzenie konsolą, który createMemo się odpalił.

Qwik udostępnia Qwik DevTools, które poza komponentami i store’ami potrafią pokazać także granice chunków i status „obudzonych” fragmentów aplikacji. Dzięki temu łatwo wyłapać, że np. podstrona produktu niepotrzebnie budzi pół dashboardu admina, bo gdzieś przemycił się wspólny singleton z logiką. To szczególnie przydatne, gdy celem jest trzymanie bundle’a klientowego w ryzach.

Mit: „lekkie frameworki = słabsze narzędzia”. W praktyce to często odwrotny kierunek – Solid i Qwik intensywnie inwestują w DX, bo konkurują z dojrzałym ekosystemem Reacta i muszą nadganiać komfort pracy.

Przykładowa architektura aplikacji w SolidJS – od komponentu do modułu

Warstwy: UI, stan lokalny i logika domenowa

SolidJS nie narzuca struktury folderów, ale przy większym projekcie przydaje się prosty podział na trzy warstwy:

  • warstwa UI – komponenty prezentacyjne, bezpośrednio używające JSX i stylów;
  • warstwa stanu widoku – hooki i kompozycje sygnałów (createSignal, createStore, createResource), które łączą się z API i zarządzają interakcjami;
  • warstwa domenowa – czysty TypeScript, modele, walidacje, usługi do komunikacji z backendem.

Przykładowa struktura dla modułu „produktów” w e‑commerce może wyglądać tak:

src/
  modules/
    product/
      ui/
        ProductCard.tsx
        ProductList.tsx
        ProductDetails.tsx
      view-model/
        useProductList.ts
        useProductDetails.ts
      domain/
        product.model.ts
        product.api.ts
        product.service.ts

Komponenty w ui/ skupiają się na tym, jak coś wygląda i jak reaguje na wejściowe propsy. view-model/ to miejsce na kompozycje sygnałów, które pobierają dane, zarządzają stanem ładowania czy paginacją. domain/ natomiast nie wie nic o Solidzie – można go później wykorzystać z Qwikiem, Reactem czy serwerowym Node.

Komponent prezentacyjny: ProductCard

Komponent prezentacyjny w Solidzie może być zaskakująco prosty, jeśli nie dźwiga na sobie logiki pobierania i mutacji stanu. Przykładowa karta produktu:

import { Component } from "solid-js";
import type { Product } from "../domain/product.model";

type ProductCardProps = {
  product: Product;
  onAddToCart?: (productId: string) => void;
};

export const ProductCard: Component<ProductCardProps> = (props) => {
  const handleAddToCart = () => {
    props.onAddToCart?.(props.product.id);
  };

  return (
    <article class="product-card">
      <img src={props.product.imageUrl} alt={props.product.name} />
      <h3>{props.product.name}</h3>
      <p class="price">{props.product.price} zł</p>
      <button type="button" onClick={handleAddToCart}>
        Dodaj do koszyka
      </button>
    </article>
  );
};

Ten komponent nie ma własnych sygnałów ani store’ów. Cały stan przychodzi z góry, a eventy są „wypychane” na zewnątrz przez callback. Taka prostota sprzyja testowaniu i ponownemu wykorzystaniu w innych widokach (lista, rekomendacje, wyszukiwarka).

View‑model listy produktów: sygnały, resource i paginacja

Warstwa view‑modelu przejmuje odpowiedzialność za pobieranie danych, stan ładowania i błędy. Typowy hook w Solidzie może wyglądać tak:

import { createSignal, createResource, Accessor } from "solid-js";
import { fetchProducts } from "../domain/product.api";
import type { Product } from "../domain/product.model";

type UseProductListResult = {
  products: Accessor<Product[] | undefined>;
  loading: Accessor<boolean>;
  error: Accessor<Error | undefined>;
  page: Accessor<number>;
  setPage: (page: number) => void;
};

export function useProductList(): UseProductListResult {
  const [page, setPage] = createSignal(1);

  const [products, { error, loading }] = createResource(
    page,
    (page) => fetchProducts({ page })
  );

  return {
    products,
    loading,
    error,
    page,
    setPage,
  };
}

Mit: „Solid wymaga trzymania wszystkiego w jednym globalnym store”. W praktyce lepiej sprawdza się rozproszenie stanu na wyspecjalizowane hooki i sygnały. Globalny kontekst przydaje się do cross‑cutting concerns (autentykacja, feature flagi), ale nie do każdej listy i formularza.

Widok listy: składanie UI z view‑modelu

Widok modułu „produkty” może korzystać z useProductList i prezentacyjnych komponentów, nie znając szczegółów domeny:

import { Component, For, Show } from "solid-js";
import { useProductList } from "../view-model/useProductList";
import { ProductCard } from "./ProductCard";
import { useCart } from "../../cart/view-model/useCart";

export const ProductListView: Component = () => {
  const { products, loading, error, page, setPage } = useProductList();
  const { addToCart } = useCart();

  return (
    <section>
      <header class="list-header">
        <h2>Produkty</h2>
        <nav>
          <button
            disabled={page() === 1}
            onClick={() => setPage(page() - 1)}
          >
            Poprzednia
          </button>
          <button onClick={() => setPage(page() + 1)}>
            Następna
          </button>
        </nav>
      </header>

      <Show when={!loading()} fallback={<p>Ładowanie...</p>}>
        <Show when={!error()} fallback={<p>Błąd ładowania.</p>}>
          <div class="product-grid">
            <For each={products()}>{(product) => (
              <ProductCard
                product={product}
                onAddToCart={addToCart}
              />
            )}</For>
          </div>
        </Show>
      </Show>
    </section>
  );
};

Podejście z <Show> i <For> przypomina Reactowe {condition && ...} oraz array.map, ale różnica jest w wydajności. Solid nie przerysowuje listy od zera, tylko aktualizuje pojedyncze węzły DOM w oparciu o sygnały. W praktyce oznacza to, że zmiana page() nie generuje całej struktury HTML jeszcze raz.

Moduł koszyka: globalny stan przez kontekst

Niektóre fragmenty stanu rzeczywiście wymagają zasięgu większego niż pojedynczy widok. Dobrym przykładem jest koszyk – dostępny na wielu podstronach, reagujący na akcje z różnych miejsc. W Solidzie można go zrealizować przez standardowy context:

import {
  createContext,
  useContext,
  ParentComponent,
  createStore,
} from "solid-js";
import type { CartItem } from "../domain/cart.model";

type CartState = {
  items: CartItem[];
};

type CartContextValue = {
  state: CartState;
  addToCart: (productId: string) => void;
  removeFromCart: (productId: string) => void;
};

const CartContext = createContext<CartContextValue>();

export const CartProvider: ParentComponent = (props) => {
  const [state, setState] = createStore<CartState>({
    items: [],
  });

  const addToCart = (productId: string) => {
    const existing = state.items.find((i) => i.productId === productId);
    if (existing) {
      setState("items", (item) => item.productId === productId, "qty", (q) => q + 1);
    } else {
      setState("items", (items) => [...items, { productId, qty: 1 }]);
    }
  };

  const removeFromCart = (productId: string) => {
    setState("items", (items) => items.filter((i) => i.productId !== productId));
  };

  return (
    <CartContext.Provider
      value={{ state, addToCart, removeFromCart }}
    >
      {props.children}
    </CartContext.Provider>
  );
};

export function useCart() {
  const ctx = useContext(CartContext);
  if (!ctx) throw new Error("useCart must be used within CartProvider");
  return ctx;
}

Mit: „kontekst w Solidzie ma te same problemy co w React (nadmierne renderowanie)”. Różnica jest w tym, że Solid propaguje zmiany na poziomie sygnałów i store’ów, nie pełnego komponentu. Jeśli komponent z koszykiem odczytuje tylko liczbę przedmiotów, reakcja dotyczy wyłącznie konkretnego odczytu, a nie całego drzewa potomków.

Skalowanie modułów: od katalogów do pakietów

Przy rosnącej liczbie modułów naturalne jest przejście z prostego modules/ do bardziej zaawansowanej struktury, np. monorepo z pakietami feature’ów. Solid dobrze znosi podejście, w którym każdy większy obszar biznesowy staje się osobnym pakietem:

packages/
  product/
    src/
      ui/...
      view-model/...
      domain/...
    package.json
  cart/
    src/...
apps/
  web-shop/
    src/
      modules/ - cienka warstwa składająca UI
    vite.config.ts

W takim układzie moduły przestają być „folderami w jednym projekcie”, a stają się logicznie wydzielonymi bibliotekami. Można je wtedy łatwo dzielić między aplikacjami, testować w izolacji, a nawet publikować jako prywatne paczki npm. Solid, jako zwykła biblioteka JS + kompilator JSX, nie wprowadza tu dodatkowego tarcia.

Architektura a SSR/SPA: decyzje na poziomie modułu

SolidStart umożliwia budowanie zarówno klasycznych SPA, jak i aplikacji z SSR/ISR. Modułowa architektura pomaga w podejmowaniu decyzji, co powinno być „serwerowe”, a co „klientowe”. Prosta zasada: wszystko, co da się obliczyć po stronie serwera bez interakcji użytkownika, powinno działać tam domyślnie.

Przykładowo, moduł „product” może mieć stronę [id].tsx, która renderuje szczegóły produktu na serwerze, ale koszyk dalej będzie interaktywny po stronie klienta. Dzięki rozdzieleniu na domain/ i view-model/ łatwo przenieść część logiki (np. formatowanie ceny, wybór wariantu) na serwer, nie ruszając UI.

W praktyce to często wygląda tak: serwer przygotowuje ProductDetailsView z kompletem danych w HTML, a klient „dokleja” tylko drobne interakcje (wybór rozmiaru, dodanie do koszyka, zapisanie do listy życzeń). Solid nie potrzebuje w tym scenariuszu pełnego hydratation niczym React – reakcje opierają się na sygnałach zainicjalizowanych po stronie klienta wyłącznie tam, gdzie są potrzebne.

Przenoszalność koncepcji między SolidJS a Qwik

Choć SolidJS i Qwik mają inne runtime’y, bardzo podobnie patrzą na modularność i separację odpowiedzialności. Warstwę domenową można bez większych zmian wykorzystać w obu. Różnice pojawiają się głównie w view‑modelu i sposobie podpinania eventów.

Ekosystemowe narzędzia (Vite, TypeScript, Vitest, Playwright) są wspólne, więc zespół może eksperymentować: jeden moduł zbudować w Solidzie, prototyp landing‑page’a w Qwiku, a logikę domenową dzielić między nimi. Dla projektów, które rosną organicznie i muszą działać zarówno jako cięższy dashboard, jak i ultralekki landing, taki wachlarz technologii daje sporą elastyczność bez konieczności wymiany całego stacku.

Najczęściej zadawane pytania (FAQ)

Czym SolidJS i Qwik różnią się od Reacta pod względem wydajności?

React opiera się na virtual DOM i re-renderowaniu komponentów, co przy dużych aplikacjach prowadzi do kosztownej hydratacji, dużych bundli JS i przeciążenia głównego wątku. SolidJS eliminuje virtual DOM i używa fine-grained reactivity – aktualizuje dokładnie ten fragment DOM, który faktycznie się zmienił, zamiast przeliczać całe poddrzewa komponentów.

Qwik atakuje problem z innej strony. Zamiast optymalizować samą aktualizację UI, minimalizuje ilość JS potrzebnego na starcie dzięki tzw. resumability. Aplikacja jest „wznawiana” w przeglądarce na podstawie stanu zakodowanego w HTML, bez pełnej hydratacji całego drzewa komponentów.

Kiedy warto rozważyć przesiadkę z Reacta na SolidJS lub Qwik?

Sygnalizatorami są m.in.: długi czas pierwszego wejścia na stronę (zimny start), rosnący koszt hydratacji po SSR, lagujące UI na dashboardach lub rozbudowanych formularzach oraz coraz większy, trudny do podziału bundle JS. Jeśli na słabszych telefonach użytkownicy „czują” opóźnienia przy każdej interakcji, optymalizacje w React (memo, lazy, Suspense) często tylko maskują problem.

SolidJS ma sens, gdy dużo logiki rzeczywiście musi działać po stronie klienta, ale chcesz ją aktualizować jak najtaniej. Qwik jest szczególnie atrakcyjny przy projektach, gdzie kluczowe jest „klikam i od razu działa” przy minimalnym JS na starcie – np. content + interaktywne fragmenty, aplikacje renderowane na edge (Vercel, Netlify, Cloudflare Workers).

Czy SolidJS i Qwik są „magicznie szybsze”, czy to tylko kwestia konfiguracji?

Mit: „Każdy framework jest tak samo szybki, liczy się tylko optymalizacja”. Rzeczywistość: architektura frameworka narzuca sufit wydajności i rodzaj kompromisów. React z virtual DOM ma wbudowany koszt re-renderów i hydratacji, którego nie da się całkowicie „wyklikać” optymalizacjami.

SolidJS dzięki fine-grained reactivity ogranicza liczbę aktualizacji do absolutnego minimum, a Qwik przez resumability i framework-level lazy loading zdejmuje z klienta konieczność inicjalizacji całego drzewa komponentów na start. Konfiguracja i dobre praktyki nadal mają znaczenie, ale punkt wyjścia jest po prostu korzystniejszy.

Czy lazy loading w React wystarczy, żeby dorównać Qwikowi?

Lazy loading w React (React.lazy, dynamiczne importy) pomaga głównie przy dużych, rzadko używanych fragmentach UI – np. panel administracyjny, moduł raportowy, skomplikowany formularz w osobnej podstronie. Problem zaczyna się, gdy większość aplikacji jest interaktywna od razu, a duże drzewo komponentów i globalne store’y muszą zainicjalizować się na starcie.

Qwik stosuje lazy loading na poziomie frameworka – dzieli na mikro‑pakiety nie tylko ekrany, ale też poszczególne komponenty i event‑handlery. Mit, że „agresywny code splitting w React wystarczy”, rozbija się o to, że React nadal musi zhydratować sporą część aplikacji, zanim UI stanie się w pełni interaktywny.

Jak SolidJS i Qwik wpływają na Core Web Vitals (LCP, FID/INP, CLS)?

SolidJS poprawia głównie metryki związane z responsywnością UI (FID/INP), bo aktualizuje tylko te fragmenty DOM, które faktycznie się zmieniają. Mniej pracy w głównym wątku to mniej mikro‑lagów przy wpisywaniu w formularze, filtrowaniu list czy przewijaniu z dynamicznym doładowywaniem.

Qwik szczególnie pomaga w LCP i FID/INP, bo minimalizuje ilość JS ładowanego i wykonywanego na starcie. HTML jest gotowy od razu, a logika JS „dokleja się” dopiero wtedy, gdy użytkownik wchodzi w interakcje z konkretnymi elementami. W praktyce często to właśnie „odczucie szybkości” na tanich smartfonach poprawia się najbardziej.

Czy SolidJS i Qwik nadają się do dużych aplikacji produkcyjnych, czy to wciąż „eksperymenty”?

Mit: „Lekkie frameworki są tylko do małych projektów lub jako ciekawostka”. Rzeczywistość jest bardziej przyziemna: ciśnienie na Core Web Vitals, mobile‑first i koszty utrzymania wymusza inwestycję w lżejsze rozwiązania także w dużych firmach. SolidJS i Qwik powstały właśnie jako odpowiedź na bóle dużych aplikacji SPA, a nie jako hobby‑projekty.

SolidJS, z API zbliżonym do Reacta, jest często wybierany do migrowania istniejących SPA krok po kroku. Qwik bywa używany tam, gdzie liczy się SSR/edge rendering i ekstremalnie niski koszt hydratacji. Oczywiście ekosystem jest mniejszy niż wokół Reacta, ale z perspektywy wydajności i architektury to pełnoprawne narzędzia do wymagających projektów.

Czy przejście z Reacta na SolidJS lub Qwik wymaga zmiany sposobu myślenia o stanie aplikacji?

W SolidJS dochodzi myślenie w kategoriach sygnałów i zależności: zamiast globalnych re-renderów całych komponentów, pracujesz z drobnymi, reaktywnymi „źródłami prawdy”. To sprzyja projektowaniu stanu bardziej lokalnie i świadomie – duże, centralne store’y przestają być naturalnym domyślnym wyborem.

W Qwik ważne jest, by zaakceptować ideę „nie inicjalizuj niczego, dopóki użytkownik tego nie dotknie”. Stan i event‑handlery są serializowane i wznawiane, więc kod musi być pisany tak, by dało się go bezpiecznie podzielić na mikro‑chunk’i. To inny sposób myślenia niż w klasycznym SPA, ale daje spore zyski wydajnościowe przy dużej skali.