Flyweight pattern - Optymalizacja pamięci w aplikacjach webowych

Schemat ilustruje wzorzec projektowy flyweight: wspólne obiekty (Shared Object) współdzielone przez unikalne instancje (Unique Instance) użytkowników, co optymalizuje zasoby.

Napisano przez

Jacek Zając

Opublikowano

16 cze 2026

Spis treści

W projektach, które renderują tysiące podobnych elementów, pamięć i liczba obiektów potrafią stać się realnym wąskim gardłem. flyweight pattern pomaga ograniczyć ten koszt przez współdzielenie tego, co wspólne, i przeniesienie tego, co zmienne, poza sam obiekt. W tym artykule pokazuję, jak ten wzorzec działa, kiedy rzeczywiście się opłaca, gdzie łatwo go nadużyć i jak zastosować go sensownie w aplikacjach webowych.

Najkrócej mówiąc, chodzi o dzielenie powtarzalnego stanu między wieloma obiektami

  • Wzorzec rozdziela dane wspólne od danych zależnych od kontekstu.
  • Najlepiej działa przy bardzo dużej liczbie drobnych, podobnych obiektów.
  • Realny zysk to mniejsze zużycie pamięci i często mniej kosztowna inicjalizacja.
  • Nie jest dobrym wyborem, jeśli stan prawie się nie powtarza albo kod ma być maksymalnie prosty.
  • W aplikacjach webowych sprawdza się m.in. przy znacznikach na mapie, ikonach, tokenach składni i kafelkach UI.

Co tak naprawdę rozwiązuje wzorzec flyweight

Najprościej: ten wzorzec ma sens wtedy, gdy chcesz reprezentować bardzo wiele drobnych obiektów, ale nie chcesz powielać w każdym z nich tych samych danych. To nie jest sztuczka do „oszczędzania pamięci za wszelką cenę”, tylko sposób na sensowne rozdzielenie informacji, które są wspólne, od tych, które zależą od konkretnego użycia. W klasycznych przykładach pojawiają się znaki w edytorze tekstu, glify, markery na mapie albo drobne elementy UI, które różnią się głównie pozycją, etykietą lub stanem chwilowym.

W praktyce patrzę na to tak: jeśli obiekt ma 5 pól wspólnych dla tysięcy instancji i 1 pole zmienne, to pełne kopiowanie całości jest zwykle niepotrzebne. Stan wewnętrzny (intrinsic) zostaje współdzielony, a stan zewnętrzny (extrinsic) jest przekazywany przy użyciu obiektu. Dzięki temu nie tworzysz osobnej kopii każdego powtarzalnego fragmentu danych tylko dlatego, że potrzebujesz kolejnej instancji logicznej.

To podejście działa szczególnie dobrze, gdy obiekt sam w sobie jest prosty, ale liczba wystąpień jest duża. Jeśli natomiast każdy egzemplarz różni się niemal wszystkim, korzyść szybko znika. Wtedy lepiej iść w prostszy model danych niż dokładać warstwę współdzielenia na siłę. Dalej rozbiję to na konkretny mechanizm, żeby było jasne, gdzie kończy się teoria, a zaczyna użyteczny kod.

Diagram ukazuje, jak Component A i B uzyskują tę samą instancję Loggera (Singleton), ilustrując wzorzec flyweight.

Jak działa współdzielenie stanu w praktyce

Jeśli projektuję taki mechanizm, myślę o nim w trzech warstwach: co jest wspólne, co jest kontekstowe i kto pilnuje ponownego użycia. Wzorzec nie musi mieć rozbudowanej architektury, ale musi być konsekwentny. Gdy ta konsekwencja znika, szybko kończy się na chaosie zamiast optymalizacji.

Stan wewnętrzny

To dane, które są identyczne dla wielu obiektów i mogą być trzymane raz. W przykładzie z markerami mapy będą to np. ikona, kolor, rozmiar lub typ znacznika. Taki stan powinien być możliwie niemutowalny, bo wtedy bezpiecznie go współdzielisz między wieloma użyciami.

Stan zewnętrzny

To wszystko, co zależy od konkretnego miejsca użycia: współrzędne, czas, indeks w liście, aktualna wartość licznikowa, tekst etykiety, kontekst użytkownika. Tych danych nie opłaca się wkładać do współdzielonego obiektu, bo zmieniają się zbyt często. Zamiast tego przekazujesz je do metody renderującej lub operującej na flyweight.

Przeczytaj również: Wzorzec szablonowa metoda – kiedy upraszcza kod, a kiedy szkodzi?

Fabryka lub rejestr obiektów

Żeby współdzielenie nie było ręczne i kruche, zwykle używam prostego rejestru lub fabryki. Jej zadanie jest banalne: jeśli obiekt o takim samym stanie wewnętrznym już istnieje, zwraca istniejącą instancję. Jeśli nie, tworzy nową i zapisuje ją w cache. W JavaScript lub TypeScript często wystarcza zwykły Map, o ile klucz jest dobrze zbudowany i jednoznaczny.

type MarkerStyle = {
  icon: string;
  color: string;
  size: number;
};

class MarkerFlyweight {
  constructor(private readonly style: MarkerStyle) {}

  render(x: number, y: number, label: string) {
    return { ...this.style, x, y, label };
  }
}

class MarkerFactory {
  private pool = new Map();

  get(icon: string, color: string, size: number) {
    const key = `${icon}:${color}:${size}`;
    let flyweight = this.pool.get(key);

    if (!flyweight) {
      flyweight = new MarkerFlyweight({ icon, color, size });
      this.pool.set(key, flyweight);
    }

    return flyweight;
  }
}

W takim układzie masz kilka wspólnych wariantów stylu, a nie tysiące niemal identycznych obiektów z powielonym opisem wyglądu. Właśnie tu widać sedno wzorca: obiekt logicznie istnieje dla konkretnego elementu interfejsu, ale fizycznie część jego danych jest wspólna. To prowadzi naturalnie do pytania, kiedy taki wysiłek naprawdę się zwraca.

Kiedy ten wzorzec daje realny zysk

Najczęściej zaczynam od prostego pytania: czy powtarzalny stan jest na tyle duży, że naprawdę boli? Jeśli nie, zysk będzie kosmetyczny, a złożoność wzrośnie natychmiast. Ten wzorzec nie jest uniwersalną odpowiedzią na wszystkie problemy z pamięcią.

Sygnał w projekcie Co to zwykle oznacza Czy flyweight ma sens
10 000+ podobnych rekordów Dużo pamięci idzie na powtarzalne pola Tak, szczególnie gdy 70-90% pól się powtarza
Elementy różnią się głównie pozycją, etykietą lub czasem Stan kontekstowy można wyciągnąć na zewnątrz Tak
Każdy obiekt ma unikalny zestaw danych Mało współdzielenia, dużo wariantów Zwykle nie
Masz tylko kilkaset obiektów Koszt pamięci jest raczej mały Rzadko

Jeśli każdy z 20 000 obiektów dubluje 150 bajtów stałych danych, powstaje około 3 MB samego powtórzonego stanu. To nie zawsze jest katastrofa, ale w frontendzie, gdzie dochodzą DOM, grafika i inne struktury, taki koszt potrafi być odczuwalny. Zysk trzeba jednak potwierdzić profilem pamięci, nie intuicją.

W praktyce dobry moment na ten wzorzec widzę tam, gdzie dane są masowe, ale schemat powtarzalny: znaczniki na mapie, komórki planszy, tokeny składni, zestawy ikon albo elementy canvas renderowane w dużej liczbie. Jeśli Twoja aplikacja operuje na setkach, a nie dziesiątkach tysięcy elementów, częściej wygrywa prostota. Z tego miejsca naturalnie przechodzę do pytania, jak taki wzorzec wdrożyć bez zrobienia z kodu niepotrzebnej układanki.

Jak wdrożyć go w aplikacji webowej

Jeśli miałbym rozpisać wdrożenie na prosty plan, zrobiłbym to w pięciu krokach. Dzięki temu nie zaczynasz od abstrakcji, tylko od danych i ich charakteru. To ważne, bo źle wydzielony stan psuje cały sens podejścia.

  1. Wyłapuję pola, które powtarzają się w wielu instancjach.
  2. Oddzielam je od danych zależnych od konkretnego użycia.
  3. Tworzę fabrykę albo rejestr z kluczem opartym o stan wspólny.
  4. Przekazuję stan zewnętrzny przy renderowaniu lub wykonywaniu operacji.
  5. Mierzę pamięć i czas renderu przed oraz po zmianie.

W kodzie frontendowym najczęściej pilnuję dwóch rzeczy: żeby stan współdzielony był niemutowalny oraz żeby klucz cache był naprawdę jednoznaczny. Jeśli klucz jest zbyt szeroki, łączysz rzeczy, które nie powinny być wspólne. Jeśli jest zbyt wąski, przestajesz odzyskiwać pamięć, bo wariantów robi się za dużo. To drobny detal, ale właśnie na takich detalach ten wzorzec się wygrywa albo przegrywa.

W praktyce lubię sprawdzać efekt na jednym konkretnym ekranie, nie na całej aplikacji. Na przykład na widoku mapy z tysiącami znaczników albo na liście z bardzo podobnymi kartami. Jeśli zmiana poprawia pamięć i nie komplikuje renderowania, wtedy dopiero rozciągam ją na większy fragment systemu. To prowadzi do kolejnej, bardzo ważnej sprawy: nie mylić flyweightu z innymi technikami, które wyglądają podobnie, ale rozwiązują inny problem.

Czym różni się od cache, memoizacji i puli obiektów

To są pojęcia, które łatwo ze sobą pomylić, bo wszystkie kręcą się wokół ponownego użycia. Różnica jest jednak istotna. Gdy mylisz te mechanizmy, kończysz z kodem, który niby działa, ale trudno go utrzymać i jeszcze trudniej zoptymalizować dalej.

Podejście Co robi Główna korzyść Kiedy uważać
Flyweight Współdzieli stan wewnętrzny między wieloma obiektami Mniej powtórzonej pamięci i mniej duplikacji Gdy stan nie jest naprawdę wspólny albo obiektów jest mało
Cache / memoizacja Zapamiętuje wynik obliczenia lub utworzony obiekt Szybsze kolejne wywołania Gdy przechowujesz zbyt wiele wpisów albo wynik zależy od ukrytego kontekstu
Pula obiektów Recyklinguje instancje, zwykle tymczasowo i często mutowalnie Mniej kosztownych alokacji Gdy obiekty są trudne do „resetowania” albo żyją zbyt krótko
Singleton Zapewnia jedną globalną instancję Jedno miejsce dostępu Gdy zaczynasz zbyt mocno sprzęgać cały system z globalnym stanem

Najważniejsza różnica jest taka, że flyweight nie służy do „przyspieszania wszystkiego”. On służy do tego, by wiele podobnych bytów nie niosło ze sobą tej samej porcji danych. Cache może być częścią implementacji, ale nie każdy cache jest flyweightem. Jeśli z tego rozróżnienia coś zapamiętasz, to właśnie to.

Najczęstsze błędy przy takim podejściu

W tym wzorcu najłatwiej popełnić błąd nie techniczny, tylko projektowy. Ktoś widzi dużą liczbę obiektów i od razu chce je „zmniejszać”, zamiast najpierw zrozumieć, co faktycznie się powtarza. To prowadzi do niepotrzebnej komplikacji.

  • Współdzielone dane nie są naprawdę stałe. Jeśli coś zmienia się co chwilę, nie powinno siedzieć w wspólnym obiekcie. Inaczej dostajesz ukryte zależności i trudne do śledzenia skutki uboczne.
  • Klucz rejestru jest źle zaprojektowany. Za szeroki klucz scala różne przypadki, za wąski tworzy za dużo wariantów. W obu sytuacjach tracisz kontrolę nad współdzieleniem.
  • Patrzysz na liczbę obiektów zamiast na koszt. Sama duża liczba instancji nie jest problemem, jeśli ich stan jest mały. Najpierw profil, dopiero potem optymalizacja.
  • Wzorzec trafia do miejsca, gdzie zysk jest mały. Przy małych listach lub jednorazowych ekranach zysk zwykle nie rekompensuje złożoności architektury.
  • Dodajesz zbyt wiele warstw pośrednich. Jeśli każda operacja przechodzi przez fabrykę, cache, adapter i jeszcze serwis, debugowanie staje się uciążliwe.

Jest też druga strona medalu: flyweight bywa bardzo skuteczny, ale tylko wtedy, gdy dane są powtarzalne i dobrze ustrukturyzowane. W aplikacjach webowych często lepiej działa przy renderowaniu i reprezentacji danych niż przy logice biznesowej. To ważne rozróżnienie, bo w przeciwnym razie wzorzec zaczyna przynosić więcej abstrakcji niż korzyści.

Co zabrać z tego wzorca do projektowania architektury

Najbardziej praktyczna lekcja jest prosta: jeśli obiekt ma wiele pól stałych, a tylko niewielki fragment zależy od kontekstu, rozdziel te dwa światy. W architekturze aplikacji webowej taki ruch często upraszcza renderowanie, obniża zużycie pamięci i pozwala lepiej kontrolować koszt dużych list, map czy edytorów.

  • Najpierw profiluję, potem optymalizuję.
  • Wydzielam stan wspólny tylko wtedy, gdy powtarza się naprawdę często.
  • Trzymam wspólne dane możliwie niezmienne.
  • Przekazuję kontekst przy użyciu, a nie przy tworzeniu.
  • Nie komplikuję modelu, jeśli problem dotyczy kilkuset obiektów.

W dobrze dobranym miejscu ten wzorzec daje wyraźny efekt: mniej pamięci, mniej duplikacji i czytelniejszy podział odpowiedzialności. W źle dobranym miejscu dokłada tylko dodatkową warstwę abstrakcji, więc tu akurat prostota i profilowanie wygrywają z samą elegancją idei.

FAQ - Najczęstsze pytania

Flyweight to wzorzec projektowy, który pomaga zminimalizować zużycie pamięci poprzez współdzielenie wspólnego stanu między wieloma obiektami. Oddziela on dane wewnętrzne (wspólne) od zewnętrznych (kontekstowych), co jest idealne dla aplikacji z dużą liczbą podobnych, drobnych obiektów.

Wzorzec Flyweight ma sens, gdy masz do czynienia z tysiącami podobnych obiektów, które powtarzają ten sam stan wewnętrzny (np. znaczniki na mapie, ikony UI). Jego główną korzyścią jest redukcja zużycia pamięci, szczególnie gdy 70-90% danych obiektu jest wspólne dla wielu instancji.

Wzorzec Flyweight opiera się na trzech elementach: stanie wewnętrznym (niemutowalne dane wspólne dla wielu obiektów), stanie zewnętrznym (dane kontekstowe, przekazywane podczas użycia) oraz fabryce lub rejestrze, który zarządza tworzeniem i ponownym wykorzystaniem obiektów Flyweight.

Nie. Flyweight skupia się na współdzieleniu *stanu wewnętrznego* między obiektami w celu zmniejszenia zużycia pamięci. Cache zapamiętuje wyniki obliczeń, pula obiektów recyklinguje całe instancje, by zmniejszyć alokacje, a singleton zapewnia jedną globalną instancję. Flyweight rozwiązuje inny problem.

Najczęstsze błędy to próba współdzielenia danych, które nie są naprawdę stałe, źle zaprojektowany klucz rejestru, optymalizowanie małych zbiorów obiektów, gdzie zysk jest minimalny, oraz dodawanie zbyt wielu warstw pośrednich. Kluczowe jest profilowanie i zrozumienie, co faktycznie się powtarza.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

flyweight pattern flyweight pattern w javascript flyweight pattern przykłady flyweight pattern zastosowanie flyweight pattern a cache wzorzec projektowy flyweight

Udostępnij artykuł

Jacek Zając

Jacek Zając

Nazywam się Jacek Zając i od dziewięciu lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od fascynacji tworzeniem stron internetowych, co szybko przerodziło się w pasję do nauczania innych. Lubię dzielić się wiedzą i pomagać osobom, które stawiają pierwsze kroki w programowaniu. Skupiam się na wyjaśnianiu złożonych zagadnień w przystępny sposób, aby każdy mógł zrozumieć podstawy i rozwijać swoje umiejętności. W moich artykułach poruszam różnorodne tematy związane z programowaniem webowym, od HTML i CSS po JavaScript i frameworki. Dokładam wszelkich starań, aby informacje, które prezentuję, były rzetelne, aktualne i łatwe do przyswojenia. Regularnie śledzę nowinki w branży, co pozwala mi na dostarczanie czytelnikom treści zgodnych z najnowszymi trendami. Wierzę, że dobrze zorganizowana wiedza to klucz do sukcesu w karierze programisty.

Napisz komentarz