Generator stron statycznych#

Tworzenie i późniejsze rozwijanie strony można wykonywać za pomocą przeróżnych narzędzi, wszystko uzależnione jest od rodzaju i rozmiaru projektu, zasobów finansowych, a także wiedzy jego autora. Przy prostych stronach statycznych z niewielką liczbą podstron, np. wizytówkach (tzw. portfolio), tłumaczeniach konkretnych dokumentacji, instrukcji i tym podobnych treści, w zupełności wystarczy jakiś prosty edytor tekstowy ze wsparciem technologii webowych.

Sprawy ulegają komplikacji w chwili, kiedy ilość podstron wzrasta, a każda zmiana (nawet ta najmniejsza) wymaga ręcznego nanoszenia poprawek w każdej z nich. Co prawda wciąż można posiłkować się edytorem tekstowym z obsługą wyrażeń regularnych, ale na dłuższą metę będzie to strata czasu. W takich przypadkach trzeba sięgnąć po sprawdzone rozwiązania ułatwiające zarządzaniem treścią, do których zaliczyć można blogi i ogólnie pojęte systemy CMS. W przypadku stron statycznych kuszącą propozycją okazują się generatory stron statycznych, o których będzie tutaj mowa.

Wprowadzenie#

Typowa witryna internetowa zarządzana przez CMS działa po stronie serwera, gdzie z każdym żądaniem strony następuje pobranie jej zawartości z bazy danych i przetworzenie jej za pomocą mechanizmu szablonów. Pomijam tu różne warianty cache'owania, chodzi mi o zasadę działania. W przypadku większości stron internetowych jest to całkowicie zbędne i niepotrzebnie zwiększa złożoność projektu, generuje problemy z wydajnością, a przede wszystkim z bezpieczeństwem. Strony te najczęściej modyfikowane są w chwili, kiedy autorzy odpowiedzialni za treść lub wygląd dokonują tych zmian, dlatego ciągłe generowanie tych samych treści jest nieoptymalne.

Generator stron statycznych # (static site generator), w skrócie SSG, to swoisty kompromis między ręcznym tworzeniem strony statycznej a pełnym CMS-em, przy jednoczesnym zachowaniu korzyści z obu tych technik. Przyjmowane jest tu inne podejście, gdzie wszystkie strony serwisu generowane są tylko raz, kiedy faktycznie zachodzą w nich zmiany, ale z użyciem podobnych rozwiązań zaczerpniętych z CMS-ów (np. szablonów). Treść można pobierać z bazy danych, choć zwykle są to pliki Markdown.

W praktyce oznacza to, że w już wdrożonej witrynie nie ma żadnych ruchomych części. Buforowanie staje się o wiele łatwiejsze, wydajność rośnie, a statyczne witryny stają się o wiele bezpieczniejsze. Generowanie najczęściej odbywa się na wydajnej maszynie po stronie dewelopera i kwestie wydajnościowe są tutaj drugorzędne (kilka sekund/minut w jedną czy drugą stronę nie ma znaczenia). Trzeba wyraźnie zaznaczyć, że zakres generacji można ograniczyć tylko do wprowadzanych zmian, co przyspiesza pracę samego dewelopera. W pewnych sytuacjach do procesu generacji wykorzystuje się nawet serwery pośrednie. Efekty pracy są następnie wgrywane na serwer produkcyjny. Końcowi użytkownicy zwykle nie dostrzegają żadnej różnicy między różnymi technikami generowania treści.

Warto wspomnieć jeszcze o powiązanej koncepcji zwanej headless/decoupled CMS (czyli bezgłowych/rozłącznych CMS-ach). Używają one interfejsu, takiego jak Wordpress, do administrowania projektem, ale umożliwiają innym systemom pobieranie treści za pośrednictwem REST API. Dzięki temu SSG mogą budować statyczną stronę internetową przy użyciu treści Wordpressa wyodrębnionej z wewnętrznego serwera. Powstałe pliki można przesłać na publiczny serwer webowy, choć instalacja Wordpressa nie musi być dostępna poza organizacją.

Generatory stron statycznych mają coraz większe uznanie na tych witrynach, gdzie treść nie podlega częstym aktualizacjom. Są bardzo popularne wśród autorów technicznych, ale coraz częściej sięgają po nie różne agencje i korporacje. Ich prawdziwy rozkwit przypadł na rok 2016 i ciągle trwa.

Na dzień dzisiejszy istnieje sporo gotowców tworzonych w różnych językach programowania, najpopularniejsze to Jekyll, Pelican, Hugo i Metalsmith. Większy zbiór i dodatkowe informacje można zaczerpnąć z poniższych źródeł:

SSG nie sprawdzą się we wszystkich projektach. Wymagają one od użytkowników szerszej wiedzy programistycznej, szczególnie na starcie, gdzie z tworzeniem treści w już zainstalowanych systemach CMS radzą sobie nawet osoby nietechniczne. W samych SSG (bez integracji z innymi usługami) z reguły nie ma żadnych paneli administracyjnych i dodawanie treści wiąże się z ręczną edycją plików Markdown. W przestrzeni webowej dla SSG istnieje zdecydowanie mniej poradników, szablonów i pluginów. W porównaniu z najpopularniejszymi silnikami blogowymi społeczność skupiona wokół SSG wciąż jest niewielka, co dla większości osób jedynie generujących treść może być barierą nie do przebicia. To się powinno stopniowo zmieniać wraz z upływem czasu i rozwojem takich narzędzi, niektóre platformy z narzędziami opakowującymi SSG już teraz pozwalają wszystko wyklikać.

Można zatem przyjąć, że SSG świetnie sprawdzają się w projektach zawierających nie więcej niż setki stron, z kilkoma nowymi postami na tydzień. Wraz z rozwojem serwisu można osiągnąć jednak taki punkt, w którym wykorzystanie CMS-a lub innego dedykowanego rozwiązania stanie się praktyczniejszą opcją. Prawda jest taka, że nie ma tutaj żadnej żelaznej reguły. Za przykład niech posłuży znana witryna Smashing Magazine, gdzie zmigrowano z popularnego Wordpressa na generator Hugo i technikę JAMStack z użyciem usługi Netlify:

W dłuższej perspektywie czasu przy małych i średnio skomplikowanych projektach SSG mogą całkowicie wyprzeć obecne systemy CMS. Już teraz zachęcam do wypróbowania jakiegoś darmowego rozwiązania i zapoznania się ze wszystkimi jego zaletami jak i wadami.

WebRef StaticGen#

Prezentowane tutaj opisy mają charakter wyłącznie informacyjny. Na chwilę obecną nie udostępniam żadnych gotowych plików czy fragmentów kodu. Całość jest jeszcze zbyt świeża, zawiera wiele spersonalizowanych ustawień pod jeden projekt i nie nadaje się do upublicznienia. Niemniej jednak zachęcam do samodzielnego eksperymentowania w tej materii.

Mimo szerokiej oferty darmowych rozwiązań warto rozważyć napisanie własnego SSG. Większość obecnych gotowców jest tak rozbudowana, że samo przestudiowanie ich możliwości może zająć więcej czasu niż utworzenie prostego szkieletu własnego generatora. Do zalet takiego podejścia można zaliczyć:

Warto podjąć takie wyzwanie, szczególnie w prywatnych projektach, gdzie nie jesteśmy ograniczeni konkretnymi ramami czasowymi. Niejednokrotnie zdarzało się, że osoby przesiadające się na ogólnodostępne SSG dochodziły do podobnych wniosków. Testując darmowy wariant na żywym organizmie zwykle kończyło się to utworzeniem własnego, ściśle zoptymalizowanego rozwiązania.

Wymagania#

W przypadku mojej witryny narzędzie do jej generacji musiało spełnić następujące wymagania:

Są to ogólniki, ale prezentują clue sprawy. Testując gotowe projekty nie byłem w stanie osiągnąć powyższych założeń, z góry skazywani jesteśmy na sztywne ramy nakreślone przez ich autorów. Co prawda żadnego z tych rozwiązań nie poznałem od podszewki i nie wykluczam, że wszystko to byłoby osiągalne, ale czas jaki musiałbym na to poświęcić zawsze wolałem przeznaczyć na rozwój własnych umiejętności. Chęć podążania własną drogą jest u mnie silniejsza od wygody i nie inaczej było w przypadku tworzenia tej witryny.

Wersja v2#

Perturbacje z poprzednim zapleczem serwerowym oraz ograniczenia dotychczasowej generacji zmobilizowały mnie do utworzenia czegoś lepszego. Z racji tego, że najwięcej czasu piszę w JavaScripcie to wybór platformy w postaci Node.js był oczywisty. Moja wiedza z zakresu Node była marginalna, ale zakup pierwszej z brzegu książki wystarczył, by rozpocząć przygodę z nowym środowiskiem. W krótkim czasie utworzyłem bazową strukturę generatora, która spełniała wszystkie stawiane przed nim wymagania brzegowe.

Projekt wciąż jest we wstępnej fazie i na bieżąco dopisuję do niego kolejne mechanizmy. Tam, gdzie to możliwe staram się unikać zbędnych zależności, ale nie zawsze jest to możliwe i wygodne. W związku z tym posiłkuję się następującymi modułami:

Oczywiście najwięcej czasu zajęło mi przerzucenie starej treści do nowego formatu danych (ok. 90% z całości). Była to ręczna i mozolna praca, o której chciałbym jak najszybciej zapomnieć (notabene do dnia dzisiejszego nie wszystko jeszcze przeniosłem). Obecny format jest elastyczniejszy i w przypadku kolejnej migracji będzie można wszystko zautomatyzować.

Wydajność#

Po wstępnym utworzeniu generatora i zaaplikowaniu mu ok. 1000 plików z danymi można przyjrzeć się sprawom wydajnościowym. Nie jest to duży zbiór danych, no ale już teraz pozwala wyciągnąć pewne wnioski.

Przy testowaniu pojedynczej strony za wzorzec # niech posłuży nam "Kurs DOM - Parsowanie i serializacja". Strona ta zawiera sporo tekstu, obrazków i fragmentów kodu, przez co w dużym zakresie absorbuje sam generator, jak i przeglądarkę internetową.

Node.js#

Node.js jest zdecydowanie cięższym środowiskiem niż stosowane wcześniej rozwiązanie, porównując same pliki binarne otrzymujemy 22 MB vs 12 KB. Do tego należy doliczyć rozmiar katalogu z użytymi modułami (obecnie ok. 30 MB). To wszystko powoduje, że rozruch generatora i praca z nim trwa nieco dłużej. Jest to szczególnie widoczne przy generowaniu pojedynczej strony, otwieraniu konkretnego pliku treści przez przeglądarkę internetową, a przede wszystkim przy pierwszym uruchomieniu aplikacji, kiedy system nie zbuforował jeszcze wszystkich tych zależności. Z początku da się to wyczuć gołym okiem, ale przy dłuższym użytkowaniu człowiek się przyzwyczaja i przestaje zwracać na to uwagę. Z drugiej strony nie jest to aż taki rząd wielkości by dłużej się nad tym rozwodzić, po prostu warto odnotować taki fakt.

Dla strony wzorcowej pełna generacja trwa ok. 1,1 s. Po wywołaniu zapisu w NPP bez problemu można przeskoczyć do przeglądarki internetowej i czekać jeszcze na przeładowanie wyniku, co wcześniej nie miało miejsca. Trzeba jednak zaznaczyć, że w poprzednim podejściu nie wykonywano żadnych skomplikowanych operacji, a samo sklejanie plików nie może stanowić dobrego punktu odniesienia. Poniższa tabela zawiera pełniejszy zestaw danych dla nowego generatora i pojedynczego pliku.

Konkretna akcjaCzas generacji - średnia [ms]Czas generacji - próbki [ms]
Baza176,6 (start)179, 178, 174, 175, 177
Połączenie z Firefoxem200,6 (+24)200, 193, 205, 202, 203
Kompresja HTML/JS/CSS369,8 (+193,2)370, 367, 369, 370, 373
Kolorowanie składni (bez jsDOM)336 (+159,4)339, 336, 338, 332, 335
Kolorowanie składni (z jsDOM)1016,6 (+840)996, 1028, 1015, 1021, 1023
Wszystkie1155,8 (koniec)1150, 1160, 1146, 1158, 1165

Widać wyraźnie, że poprawki dla Highlight.js z wykorzystaniem jsDOM są tutaj najbardziej kosztowne. Ustaliłem dodatkowo, że załadowanie modułu jsDOM zabiera aż 615 ms (więcej niż połowa czasu całej generacji!), a wykonanie wszystkich korekt to zaledwie 70 ms. Niestety, na chwilę obecną lepszego rozwiązania dla kolorowania składni nie znalazłem. Sprawa nie dotyczy tych stron (nawet bardziej obszernych), na których nie ma żadnego przykładowego kodu, jak chociażby tej tutaj, której wygenerowanie zabiera ok. 220 ms.

W przypadku kompleksowego przemielenia wszystkich plików mamy ciekawszą sytuację. Problem ze wspomnianym wyżej kolorowaniem składni jest mniej odczuwalny bo załadowanie modułu jsDOM jest jednokrotne. Z moich obserwacji wynika, że najwięcej czasu zajmuje minifikacja HTML-a i wewnętrznego JS-a/CSS-a, która na jednym procesie i przy włączonych pozostałych akcjach wydłużyła pracę całości 3-krotnie (pomijam tutaj minifikację zewnętrznych plików JS i CSS bo to wykonuję na żądanie - rzadko je zmieniam i ciągły ich przemiał jest zbędny). Co prawda minifikację wszystkich plików też można zrobić dopiero przed wrzuceniem na serwer produkcyjny, ale zamiast tego wolałem rozbić pracę generatora na więcej procesów. Poniższa tabela zawiera pełniejszy zestaw danych dla generacji 962 stron przy różnych wariantach testowych.

Konkretna akcjaCzas generacji [ms]
Liczba procesów
124681012
Baza2926222717631640176818171954
Kolorowanie składni (bez jsDOM)5657384528452679279529383081
Kolorowanie składni (z jsDOM)11759738061896872707676487900
Kompresja HTML/JS/CSS3014118431123751060299731019310688
Wszystkie39120236901629114341144871516215948

Widać wyraźnie, że na mojej maszynie z Intel i7-2600K (4 rdzenie/8 wątków) najlepiej sprawdza się rozkład pracy między 6 a 8 procesów. Całkowity czas pracy 14,5 s dla wszystkich plików jest jak najbardziej akceptowalny. Ponadto wszystkie dane (wejściowe i wyjściowe) wciąż siedzą na magnetycznym dysku, podejrzewam że po przerzuceniu ich na SSD osiągi byłyby nieco lepsze. Prawdę mówiąc to najczęściej generuje tylko jeden plik - ten który aktualnie edytuję - dlatego ten czynnik nie jest dla mnie decydujący, liczy się tylko i wyłącznie wygoda użytkowania i szerokie możliwości tworzonego generatora.

Przeglądarki internetowe#

W przypadku przeglądarek internetowych pomiary najlepiej przeprowadzać na czystej instalacji, bez dogranych rozszerzeń, a przede wszystkim tych, które analizują żądania sieciowe (np. Adblock Plus, uBlock Origin czy NoScript). Przy małych czasach wszystkie one potrafią w zauważalny sposób wpłynąć na końcowy wynik. Informacja o dograniu jakichkolwiek dodatków przy prezentowaniu swoich wyników jest obowiązkowa.

Nowy generator wreszcie pozwolił mi na długo wyczekiwaną, szerszą optymalizację projektu, szczególnie w obszarze wykonywanego kodu JavaScript. Analiza strony wzorcowej narzędziami developerskimi z Chrome pokazała, że w procesie jej pobierania i przetwarzania dużo czasu marnowane jest na wykonywanie kodu JS. Bez elastycznego generatora wszystkie używane do tej pory skrypty były jedyną opcją poszerzającą możliwości witryny.

Z początku próbowałem nieco zoptymalizować kod JS tak, by jak najrzadziej sięgał do DOM, ale efekty nie były zadowalające. Najwięcej czasu wciąż zabierało kolorowanie składni i menu lewe, z którymi bez nowego narzędzia nic więcej nie byłem w stanie zrobić. W trakcie tworzenia generatora podszedłem do sprawy nieco inaczej. Cała struktura dodatkowych mechanizmów tworzona jest od razu po stronie generator, a kod JS odpowiada tylko i wyłącznie za ich zachowanie. Co prawda manewr ten zwiększa rozmiar plików HTML, ale przy obecnych łączach internetowych, przeprowadzonej minifikacji i włączonej kompresji po stronie serwera nie stanowi to żadnego problemu. Oprócz zysku czasowego zapewniamy także lepszą dostępność dla tych wszystkich osób, które z różnych powodów wyłączają lub blokują obsługę JavaScriptu w swoich przeglądarkach. Niektóre rozwiązania w całości przeniosłem do generatora i tym samym uniezależniłem je od stanu obsługi JS-a po stronie klienta.

Zamiast suchych opisów najpierw proponuję rzucić okiem na poniższe wyniki. Są to pomiary wykonane dla trzech wersji strony wzorcowej: bez optymalizacji JS-a, z optymalizacją JS-a oraz z użyciem generatora. Testy wykonałem na lokalnych plikach (bez żadnego serwera), tak aby wyeliminować ewentualne opóźnienia wynikające z operacji sieciowych. Każdemu najbardziej czasochłonnemu mechanizmowi przypisałem klarowną nazwę wskazującą do czego ów mechanizm służy. Ostatnie cztery pomiary zawierają w sobie wszystkie inne pomniejsze funkcje i polecenia, których nie wyszczególniłem.

AkcjaCzas wykonania [ms]
Bez optymalizacji JSZ optymalizacją JSGenerator
MenuLeft5047,20,1
NavBtn59,55,50,1
RainbowCode8280,4-
ExampleBtn16,310,70
InternalTarget170,3-
ExternalTarget1,10,4-
Lightbox2,71,30,5
All JS (bez Menu Left)181104,67,1
All JS231151,87,2
DOMContentLoaded312288118
Load336312141

Zysk wydajnościowy osiągnięty przy pomocy nowego generatora jest niepodważalny. Istotne jest to, że przy zwiększaniu treści na stronach czas potrzebny do ich obsługi przez kod JS nie wzrasta lawinowo i pozostaje na wciąż akceptowalnym poziomie. Całkowity czas dla strony startowej to zaledwie 3,4 ms i jego wzrost do 7,2 ms dla prezentowanej tutaj strony wzorcowej jest marginalny.

Kolejnym krokiem optymalizacyjnym dającym zauważalny zysk były kwestie związane z żądaniami sieciowymi. Liczba zewnętrznych plików JS i CSS zredukowana została do jednego, powtarzające się ozdobniki graficzne wylądowały we wspólnym spricie, a część z nich (np. logo) przepuszczono przez dedykowane narzędzia kompresujące grafikę. Na sam koniec zaaplikowano większość technik wskazywanych przez takie testery jak PageSpeed i YSlow. Wszystkie te zabiegi spowodowały, że usługa GTMetrix dla strony startowej zwróciła wynik 100/95.

Wynik z usługi GTMetrix dla witryny webref.pl

Rysunek. Wynik z usługi GTMetrix dla witryny webref.pl

Nie jest źle, chociaż czas wczytywania z Kanady (3,7 s) pozostawia wiele do życzenia. W przypadku maszyn z naszego rodzimego podwórka lub najbliższych sąsiadów (np. Niemiec) całość zamyka się w granicy 500 ms. Sporo w tym obszarze można jeszcze poprawić, np. przenieść cały projekt na CDN, zoptymalizować wszystkie obrazki, zerwać z obsługą staroci i pousuwać utworzony dla nich kod. Dalsze czynności optymalizacyjne zostaną wykonane przy okazji kolejnej, większej przebudowy serwisu.

Wersja v1#

Statyczne generowanie stron internetowych towarzyszy mi z chwilą rozpoczęcia mojej przygody z technikami webowymi, czyli gdzieś na przestrzeni 2006 roku. Wszystko za sprawą mojej pierwszej książki o HTML-u autorstwa Radosława Sokoła i zawartego w niej darmowego narzędzia compose.exe. Ten niewielki konsolowy program pozwala tworzyć szablony i generować pliki HTML w oparciu o wplecione w nie pliki, coś na zasadzie funkcji include() z PHP. Nie jest to żadne wymyślne rozwiązanie, pozwala na jednopoziomowe zagnieżdżenie plików, no ale z uwagi na ówczesny stan mojej wiedzy oraz darmowe zaplecze serwerowe i tak były to wyżyny moich możliwości.

Wraz ze zwiększającą się liczbą podstron i uciążliwością przy wprowadzaniu ogólnych zmian postanowiłem nieco podrasować compose.exe. Po opracowaniu szablonu (tj. wydzieleniu nagłówka, stopki i poszczególnych menu) całość została oskryptowana przy użyciu plików .bat i .vbs (hurtowa lub pojedyncza generacji z uwzględnieniem zmian w menu). Na zakończenie doszła jeszcze integracja ze środowiskiem pracy. Taki tandem pozwolił mi generować nowe treści przez bardzo długi okres czasu i uniknąć przesiadki na popularne w tym czasie systemy CMS.

I znów, po osiągnięciu pewnego pułapu podstron rozwiązanie to zaczynało coraz bardziej uwierać. Szablon w zasadzie był zamrożony i nie pozwalał w łatwy sposób zmieniać swoją strukturę. Także wplatana w niego treść miała z góry ustalony schemat. O większych zmianach w witrynie nie było nawet mowy, całość musiała ograniczyć się do wstrzykiwania dodatkowego kodu JS, co niekorzystnie wpływało na wydajność. Mimo wielu ograniczeń zestaw ten stanowił ciekawe rozwiązanie dla niewielkich projektów i w początkowej fazie nauki HTML-a był wystarczający. Obecnie moja wiedza i wymagania zdecydowanie wzrosły, to też projekt przeszedł na zasłużoną emeryturę i nie jest już rozwijany.

Integracja ze środowiskiem pracy#

Każdy generator powinien w jak największym zakresie pozwalać na sprzęgnięcie z używanymi przez programistę narzędziami. Ręczne wywoływanie poleceń konsolowych, odświeżanie przeglądarek czy otwieranie właściwych plików choć możliwe, to w dłuższej perspektywie jest nazbyt męczące.

W moim przypadku kluczowa była współpraca generatora z przeglądarką Firefox oraz Notepadem++. Poniżej zamieszczam kilka wskazówek, w jaki sposób można zautomatyzować najczęściej powtarzające się zadania:

Do dnia dzisiejszego taki zestaw sprawdza się znakomicie, ale z uwagi na karkołomne wymysły Mozilli dotyczące ekosystemu rozszerzeń przy aktualizacji strony zmuszony jestem przesiadywać na starszej wersji Firefoksa. W przyszłości prawdopodobnie napiszę jakiś dodatek WebExt, który będzie działał we wszystkich wiodących przeglądarkach. Trzeba tylko ustalić, w jaki sposób można spinać z nimi zewnętrzne programy. Obawiam się, że każda z nich może robić to w inny sposób, przykładowo Firefox używa mechanizmu natywnych aplikacji. Już teraz widzę, że nie będzie to idealne bo nie pozwoli na pełną przenośność projektu (w przypadku Windowsa lokalizację manifestu natywnej aplikacji trzeba wskazywać za pośrednictwem rejestru!).

Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Generator stron statycznych (H1) Wprowadzenie (H2) WebRef StaticGen (H2) Wymagania (H3) Wersja v2 (H3) Wydajność (H4) Node.js (H5) Przeglądarki internetowe (H5) Wersja v1 (H3) Integracja ze środowiskiem pracy (H3)