Jak wyrzuciłem Bootstrap z produkcyjnej aplikacji Django, szablon po szablonie
W serii o odruchach front-endowych pisałem, że najgorszy powód, żeby czegoś użyć, brzmi „bo zawsze tak robię”. Ten wpis jest dowodem, że biorę to na serio także na własnym, produkcyjnym kodzie – i że wyjście z zakorzenionej zależności da się przeprowadzić bez rewolucji i bez jednej nieprzespanej nocy.
Bohaterem jest zawodowe.edu.pl – platforma do egzaminów zawodowych, z której korzystają tysiące osób miesięcznie. Backend to Django, a warstwa widoku ruszyła na początku 2025 roku na Bootstrapie 5. To była dobra decyzja na start: Bootstrap dał mi siatkę, komponenty i spójny wygląd od pierwszego dnia, bez wymyślania koła. Ale po mniej więcej roku działania miałem już w planach jego usunięcie, a po półtora roku – w połowie 2026 – było zrobione.
Dlaczego w ogóle ruszać coś, co działa
To jest pierwsze pytanie, które sam sobie zadałem, bo mam prostą zasadę: legacy, które działa i się sprawuje, zostaje nietknięte. Bootstrap nie był zepsuty. Powód był inny i wart nazwania wprost.
Przez dwie dekady biblioteki front-endowe nadrabiały braki przeglądarki. Bootstrap dawał komponenty, których HTML i CSS nie miały: rozwijane menu, modale, zakładki, tooltipy. Ale przeglądarka te braki w międzyczasie uzupełniła – i to często w samym HTML-u albo CSS, o czym pisałem w części o CSS-ie, który zjadł JavaScript. W momencie, w którym modal to natywny <dialog>, a rozwijane menu to <details>, Bootstrap zaczyna być warstwą, która dokłada wagę i sztywność, nie dokładając już wartości.
Dołożę do tego prognozę, którą traktuję jak kompas: za kilka lat Bootstrap jako fundament nowego projektu będzie tym, czym dziś jest jQuery – narzędziem z epoki, w której platforma jeszcze nie umiała. Skoro tak, to lepiej zejść z niego, gdy jest to spokojna, kontrolowana operacja, niż gdy zmusi mnie do tego coś pilnego.
Zasada numer jeden: żadnego wielkiego przepisu
Najgorsze, co mogłem zrobić, to ogłosić „przepisujemy front” i na miesiąc wpaść w gałąź, która nie wchodzi na produkcję. Aplikacja żyje, użytkownicy korzystają, a wielki przepis to gwarancja tygodni bez wdrożeń i jednego bolesnego zderzenia z rzeczywistością na końcu.
Zamiast tego zrobiłem to szablon po szablonie, przy działającej aplikacji. Mechanizm był banalnie prosty i to jego prostota była kluczowa. Nowe szablony (nazwałem je „v2”) lądowały w osobnym katalogu, obok starych. Cienka warstwa pośrednia w Django sprawdzała przy każdym żądaniu: jeśli dla tego widoku istnieje wariant v2, użyj go; jeśli nie – zostaw stary Bootstrap. Te same adresy URL, ta sama logika, podmieniany tylko szablon.
Dzięki temu każdy pojedynczy szablon był osobnym, małym, wdrażalnym krokiem. Przepisałem stronę, wdrożyłem, sprawdziłem na żywo, wziąłem następną. Przez większość migracji część serwisu chodziła już na czystym HTML/CSS, a część wciąż na Bootstrapie – i było to zupełnie niegroźne, bo obie wersje żyły pod tymi samymi adresami.
Do tego jedno zabezpieczenie, które dawało spokój: globalny przełącznik. Jedna zmienna środowiskowa potrafiła awaryjnie cofnąć całą aplikację do Bootstrapa, bez wdrażania kodu. Gdyby cokolwiek poszło nie tak po wdrożeniu nowej wersji, powrót do znanego, działającego stanu był kwestią sekund, a nie odbudowy z gita.
Co zastąpiło poszczególne kawałki Bootstrapa
Najciekawsza część, bo tu widać, jak niewiele trzeba, gdy odruch „biblioteka do wszystkiego” zastąpi się pytaniem „co przeglądarka już potrafi”.
Rozwijane menu i nawigacja → <details>. Cała nawigacja z podmenu, które w Bootstrapie wymaga jego JavaScriptu, stoi teraz na natywnym <details>/<summary>. Otwieranie, zamykanie i obsługa klawiatury są wbudowane – zero skryptu.
<!-- Rozwijane menu bez ani jednej linijki JavaScriptu -->
<details class="menu">
<summary>Konto</summary>
<div class="menu-panel" role="menu">
<a href="#panel">Panel</a>
<a href="#wiadomosci">Wiadomości</a>
<a href="#wyloguj">Wyloguj</a>
</div>
</details>Modale → <dialog>. Potwierdzenia, kompozycja wiadomości, okna z formularzami – wszystko to były modale Bootstrapa z jego zarządzaniem tłem i focusem. Teraz to natywny <dialog> z showModal(), który daje pułapkę focusa i przyciemnione tło z pakietu.
Filtry i wielokrotny wybór → CSS :has(). Listy z chipami, panele pokazujące się po zaznaczeniu opcji – w Bootstrapie (albo dawnym Select2) to był JavaScript pilnujący widoczności. Dziś stan zaznaczenia opisuje sam CSS przez :has():
/* Panel pojawia się, gdy odpowiadający mu przełącznik jest zaznaczony – bez JS */
.panel { display: none; }
.grupa:has(> input:checked) .panel { display: block; }Zwykłe rozwijane listy wróciły do natywnego <select> zamiast doklejanego widżetu z JavaScriptem.
Ikony → statyczny sprite SVG. Zamiast fontu ikon ładowanego w czasie działania strony, ikony są jednym plikiem SVG (spritem), z którego szablon wyciąga pojedynczy kształt przez <use>. Zero fontu do pobrania w runtime, zero „doskakiwania” ikon po załadowaniu.
Motywy → tokeny CSS i atrybut [data-theme]. Trzy motywy (jasny, ciemny, wysoki kontrast) plus regulacja wielkości tekstu stoją na zmiennych CSS przełączanych atrybutem na elemencie głównym. Żaden z tych motywów nie potrzebuje Bootstrapa ani jego systemu kolorów.
jQuery → garść własnych funkcji. Tam, gdzie JavaScript naprawdę był potrzebny – wysłanie formularza w tle, powiadomienie, paginacja – zastąpił go mały zestaw własnych helperów na czystym fetch i standardowym DOM API. Cała reszta tego, po co kiedyś sięgało się po $, jest dziś w samej przeglądarce, co rozłożyłem na czynniki w pierwszej części serii.
Guardrail: jak nie wpuścić Bootstrapa z powrotem
To jest część, z której jestem najbardziej zadowolony, bo rozwiązuje realny problem: w dużym projekcie z wieloma szablonami łatwo, z rozpędu i starego nawyku, wkleić gdzieś klasę Bootstrapa albo data-bs-. Migracja bez ochrony przed nawrotem cicho gnije.
Dlatego napisałem testy, które wywalają się, jeśli w nowych szablonach pojawi się cokolwiek z Bootstrapa – jego klasy, atrybuty data-bs- albo styl wpisany inline w HTML. Nie ma znaczenia, czy to przeoczenie, czy pośpiech: test świeci na czerwono i sprawa nie wchodzi dalej. To ten sam pomysł, którego używam na tym blogu do pilnowania spójności treści – maszyna trzyma zasadę, na którą człowiek po miesiącu przestaje uważać.
Doszły do tego mniejsze strażniki: test pilnujący, że żadna klasa CSS nie zostaje martwa po przenosinach, i test kontrastu kolorów we wszystkich trzech motywach. Migracja przestała być jednorazowym zrywem, a stała się stanem, którego nie da się po cichu popsuć.
Sekcja uczciwości: gdzie ta metoda się sprawdza, a gdzie nie
Byłoby nieuczciwe sprzedawać tę operację jako uniwersalną receptę. Zadziałała tak gładko dzięki kilku warunkom, które nie zawsze są spełnione:
- To działa, bo widok renderuje serwer. Cała sztuczka z podmianą szablonu opiera się na tym, że w Django każdy widok ma swój szablon i da się go podmienić pojedynczo. W ciężkiej aplikacji jednostronicowej (SPA), gdzie interfejs skleja się w przeglądarce z komponentów, nie ma takiego czystego szwu „podmień ten jeden plik” – tam wyjście z frameworka UI to inna, trudniejsza operacja. O tym, kiedy framework naprawdę się broni, pisałem w ostatniej części serii.
- To nie było kasowanie zależności, tylko przepisanie tego, co dawała. Bootstrap to nie tylko komponenty – to też siatka, spójne odstępy i typografia. Wszystko to musiało dostać swój odpowiednik: własne zmienne CSS i biblioteka klas na tokenach. Usunięcie biblioteki jest darmowe dopiero wtedy, gdy najpierw odtworzysz to, za co realnie odpowiadała. To praca, nie
git rm. - Przez większość migracji żyją dwa systemy stylów naraz. Dopóki część serwisu jest na v2, a część na Bootstrapie, przeglądarka wozi jedno i drugie. To świadomy koszt przejściowy – płacisz chwilowo większym CSS-em za to, że nigdy nie masz tygodni bez wdrożeń i jednego wielkiego zderzenia na końcu. Dla mnie ta wymiana była oczywista, ale warto ją nazwać, zamiast udawać, że migracja jest za darmo.
Co z tego wynika
Przez lata trzymałem się starych narzędzi za długo – o moim romansie z Perlem, gdy świat przechodził na PHP, pisałem w pierwszej części serii. Z Bootstrapem chciałem zrobić inaczej: nie czekać, aż coś mnie zmusi, tylko zejść z niego świadomie, w kontrolowanych warunkach, gdy zauważyłem, że przestał się bronić.
Recepta okazała się mało spektakularna i właśnie dlatego działa:
- Nie przepisuj wielkim zrywem. Szablon po szablonie, każdy osobno wdrażalny, przy żywej aplikacji.
- Miej przełącznik awaryjny. Powrót do znanego, działającego stanu ma być kwestią sekund.
- Zabezpiecz się testem przed nawrotem. Bez tego stary nawyk wróci tylnymi drzwiami.
- Rusz to tam, gdzie platforma naprawdę dogoniła. Migracja nie jest celem samym w sobie – robisz ją, bo biblioteka stała się zbędnym ciężarem, a nie dla samej nowości.
Bootstrap zrobił swoje na starcie tego projektu i nie mam mu nic do zarzucenia. Ale rola, do której go wtedy wziąłem – dawać komponenty, których przeglądarka nie miała – po prostu się skończyła. A rozpoznanie momentu, w którym dobre narzędzie przestaje być potrzebne, jest trudniejsze i ważniejsze niż sam przepis kodu.