Jeden z najbardziej użytecznych błędów produkcyjnych, jakie zobaczyłem po wdrożeniu, wyglądał niemal banalnie:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
Pojawił się w tym samym strumieniu co awarie narzędzi analitycznych, skryptów reklamowych, ogólne komunikaty Script error. i przerwane odtwarzanie wideo. Większość tego strumienia była szumem. Ten przypadek był inny: nie załadował się zasób należący do mojej własnej aplikacji Next.js. Jeżeli przeglądarka rzeczywiście nie mogła go pobrać, część strony mogła przestać działać.
Z samego logu nie wiedziałem jednak, dlaczego chunk się nie załadował. Mogła wystąpić chwilowa awaria sieci, problem z serwerem pośredniczącym lub CDN, albo pliku faktycznie mogło nie być. Możliwe też, że stara strona prosiła o chunk z poprzedniego wdrożenia już po tym, gdy serwer zastąpił tamtą wersję nową.
Ten ostatni scenariusz łatwo zlekceważyć, ponieważ nowe wdrożenie może być całkowicie sprawne. Każdy nowy odwiedzający dostaje nową wersję i wszystko działa, podczas gdy karta otwarta od kilku godzin po cichu pozostaje klientem poprzedniej wersji.
Ten artykuł dotyczy właśnie tej luki zgodności: dlaczego stare karty Next.js mogą przestać działać po wdrożeniu, jak nieaktualny HTML i brakujące zasoby /_next/static prowadzą do rozjazdu wersji, dlaczego agresywne czyszczenie pogarsza sytuację oraz jak projektowałbym wdrożenie, przechowywanie, monitoring i odzyskiwanie, aby udane wydanie nie pozostawiało użytkowników z już otwartą aplikacją na uszkodzonej stronie.
Pierwszy wniosek: nie każdy błąd skryptu jest błędem wdrożenia
Pierwotny strumień błędów zawierał kilka zupełnie różnych klas awarii. Zewnętrzne skrypty analityczne i reklamowe mogą być blokowane przez filtry treści, filtry DNS, mechanizmy prywatności, ograniczenia regionalne, program antywirusowy albo sieć użytkownika. Obietnica play() dla wideo może zostać przerwana przez późniejsze pause(), mimo że sama aplikacja działa poprawnie. Ogólny międzydomenowy komunikat Script error. często zawiera zbyt mało informacji, by cokolwiek zdiagnozować.
Nieudane wczytanie własnego chunka Next.js zasługuje na inny priorytet. Użyteczna granica nie przebiega między „jest błąd JavaScript” a „nie ma błędu JavaScript”, lecz raczej tak:
nie powiódł się zasób zewnętrzny
-> zwykle cierpi telemetria lub funkcja opcjonalna
nie powiódł się własny /_next/static/*.js
-> kod aplikacji może być niedostępny
To rozróżnienie ma znaczenie, bo zbyt głośny system raportowania potrafi ukryć awarie, które rzeczywiście korelują z niedziałającymi stronami. W moim przypadku istotnym zdarzeniem było żądanie /_next/static/chunks/9253.647385b4be0958e4.js. Log dowodził, że własny skrypt aplikacji się nie załadował. Nie dowodził, że przyczyną był rozjazd wersji po wdrożeniu.
Celowo utrzymuję tę granicę dowodową: prawdopodobna przyczyna nie jest jeszcze przyczyną potwierdzoną.
Otwarta karta przeglądarki jest w praktyce klientem starszego wydania
Model, który uporządkował mi ten problem, jest prosty: po wdrożeniu kilka wersji aplikacji może jednocześnie pozostawać przy życiu.
Załóżmy, że wydanie A działa od 10:00. Użytkownik otwiera stronę i otrzymuje HTML wraz z JavaScriptem potrzebnym dla tej trasy. O 10:30 zastępuje je wydanie B. Nowi odwiedzający dostają B. Jednak wcześniej otwarta karta nie staje się automatycznie B tylko dlatego, że zmienił się serwer.
W tej karcie nadal mogą znajdować się:
- środowisko wykonawcze JavaScript wczytane z wydania A;
- odwołania do tras i chunków wygenerowane przez wydanie A;
- wstępnie pobrane dane nawigacyjne z wydania A;
- stan React utworzony podczas działania wydania A;
- podzielone moduły z A, które zostały już pobrane;
- odwołania do modułów A, które nie zostały jeszcze pobrane.
Ostatni punkt to miejsce, w którym problem staje się widoczny.
Jeśli każdy chunk, którego strona kiedykolwiek będzie potrzebować, znajduje się już w pamięci podręcznej przeglądarki, użytkownik może niczego nie zauważyć. Współczesne aplikacje Next.js dzielą jednak kod. Przejście na inną trasę, import dynamiczny, okno modalne, edytor lub funkcja użyta dopiero później mogą wymagać kolejnego pliku JavaScript. Stare środowisko wykonawcze żąda wtedy zasobu pod adresem URL, który był prawidłowy dla wydania A.
Jeśli serwer nadal ma ten zasób, wszystko może działać dalej. Jeśli wdrożenie go usunęło, stary klient może otrzymać 404, mimo że samo wydanie B jest w pełni sprawne.
Chunki z haszem zawartości są przeznaczone do długiego przechowywania w pamięci podręcznej
Next.js celowo nadaje naprawdę niezmiennym zasobom długą semantykę pamięci podręcznej. Aktualna dokumentacja samodzielnego hostingu podaje, że niezmienne zasoby z haszem SHA w nazwie pliku są wysyłane na przykład z roczną polityką:
Cache-Control: public, max-age=31536000, immutable
Ma to sens, ponieważ URL zmienia się wraz z zawartością. Plik nazwany na podstawie zawartości nie musi być ponownie sprawdzany przy każdym żądaniu. Jeśli późniejsza kompilacja tworzy inne bajty, powinna też utworzyć inny URL zasobu.
Ważną konsekwencję łatwo przeoczyć: stary URL zachowuje znaczenie tak długo, jak długo może się do niego odwołać stary dokument lub stare środowisko wykonawcze.
Możliwość przechowywania zasobu z haszem przez rok nie pomaga, jeżeli przeglądarka nie pobrała akurat tego pliku przed wdrożeniem, a serwer źródłowy zdążył go już usunąć.
Dlatego zdania „nasze pliki statyczne są niezmienne” i „możemy natychmiast usunąć pliki statyczne poprzedniego wydania” nie są równoważne. Niezmienność sprawia, że stare zasoby można bezpiecznie zachować. Nie sprawia, że stare klienty przestają o nie pytać.
Aktualny przewodnik Next.js po samodzielnym hostingu wprost wymienia brakujące pliki JavaScript lub CSS jako jeden z objawów rozjazdu wersji podczas stopniowego wdrażania lub pracy wielu serwerów. To ta sama rodzina problemów także wtedy, gdy rozjazd występuje między starą kartą a właśnie zaktualizowanym serwerem źródłowym, a nie między dwoma jednocześnie aktywnymi serwerami.
Wersje mogą się rozjechać na kilka różnych sposobów
„Problem z pamięcią podręczną” jest zbyt ogólny, by był użyteczną diagnozą. Rozróżniam co najmniej cztery mechanizmy, ponieważ wymagają różnych napraw.
1. Stara karta żąda zasobu, którego nie wczytała przed wdrożeniem
To klasyczny przypadek długo otwartej karty. Dokument i środowisko wykonawcze pochodzą z wydania A. Wydanie B zastępuje pliki na serwerze. Później użytkownik wykonuje czynność, która uruchamia ładowany na żądanie chunk z A. Jeśli zasób A został usunięty, żądanie kończy się błędem.
2. Nieaktualny HTML wskazuje chunki, których już nie ma
CDN, odwrotny serwer pośredniczący, Service Worker, pamięć podręczna przeglądarki lub warstwa hostingu statycznego mogą przechować starszy dokument HTML dłużej, niż zakładano. Taki HTML nadal może zawierać odwołania do wydania A, podczas gdy serwer źródłowy zawiera już wyłącznie B.
Jest to szczególnie niebezpieczne, gdy HTML przez pomyłkę otrzyma długą politykę immutable. JavaScript z haszem i HTML nie powinny być traktowane jak ten sam obiekt pamięci podręcznej. Chunk może być niezmienny, ponieważ jego URL jest wersjonowany zawartością. To HTML decyduje, które adresy chunków mają do siebie pasować.
3. Stopniowe wdrożenie lub wiele instancji serwuje różne wydania
Wyobraźmy sobie dwie instancje Next.js za modułem równoważenia obciążenia. Jedna działa już na wydaniu B, druga nadal na A. Dokument może pochodzić z jednej wersji, a kolejne żądanie nawigacji trafić do drugiej. Aktualna dokumentacja Next.js nazywa to rozjazdem wersji i wskazuje, że może on powodować brakujące zasoby, niezgodności Server Functions i błędy nawigacji.
Najbezpieczniejsza praktyka domyślna to skompilować wydanie raz i uruchamiać ten sam artefakt kompilacji na wszystkich instancjach uczestniczących w danym wdrożeniu. Dokumentacja Next.js dotycząca samodzielnego hostingu zaleca też używanie tej samej kompilacji i spójnego identyfikatora kompilacji we wszystkich kontenerach, zamiast niezależnie kompilować każdą replikę.
4. Samo wdrożenie publikuje pliki w niewłaściwej kolejności
Nawet bez starych kart nieatomowe przesyłanie może na chwilę utworzyć niemożliwy stan:
nowy HTML jest już widoczny
+
nowe pliki chunków nie są jeszcze dostępne
albo odwrotnie:
stary HTML jest nadal widoczny
+
stare pliki chunków zostały już usunięte
Wystarczy bardzo krótkie okno. Użytkownik musi trafić w nie tylko raz.
Niebezpieczny wzorzec wdrożenia to „zastąp wszystko i usuń stare drzewo”
Prosty skrypt wdrożeniowy często zaczyna życie mniej więcej tak:
build
rsync --delete new-output/ production/
restart
To kuszące, bo katalog produkcyjny zawsze dokładnie odpowiada najnowszej kompilacji. Jest jednak nieprzyjazne dla długo żyjących klientów.
Przy statycznych zasobach z haszem ograniczenie katalogu do dokładnie jednego wydania daje przeglądarce niewiele. Stare pliki nie kolidują z nowymi, ponieważ mają inne URL-e. Ich usuwanie przede wszystkim oszczędza miejsce na dysku, ale jednocześnie zmienia każde nadal prawidłowe odwołanie ze starego klienta w potencjalny 404.
Stare chunki traktuję dziś jako materiał zapewniający zgodność wdrożeń, a nie jako śmieci.
Nie oznacza to przechowywania każdej kompilacji na zawsze. Oznacza, że sprzątanie powinno być oddzielną polityką przechowywania, a nie przypadkowym skutkiem ubocznym publikacji najnowszego wydania.
Przechowywanie pomaga, ale żadne skończone okno nie daje pełnej ochrony
W praktycznym środowisku samodzielnie hostowanym można zachowywać starsze zasoby /_next/static przez określony okres zgodności. Jego długość zależy od sposobu użycia. Witryna, na której użytkownik otwiera stronę, czyta dwie minuty i wychodzi, ma inny profil ryzyka niż aplikacja pozostająca otwarta przez cały dzień pracy.
Przybliżony minimalny okres przechowywania można opisać tak:
okno przechowywania >=
oczekiwany czas życia nieaktualnego HTML
+ realistyczny czas życia długo otwartej karty
+ okno wycofania wydania
+ margines propagacji wdrożenia
To nie jest matematyczna gwarancja. Karta przeglądarki może pozostać otwarta tygodniami. Żadna skończona liczba godzin nie wyklucza awarii starych klientów.
Dlatego wolę ochronę warstwową:
- przechowywać niezmienne zasoby wcześniejszych wydań na tyle długo, by zwykłe stare sesje mogły dalej działać;
- wykrywać rozjazd wersji, aby klient mógł przejść do bieżącego wydania;
- zapewnić bezpieczne, jednorazowe przeładowanie albo widoczną dla użytkownika ścieżkę odzyskiwania, gdy zasób jest rzeczywiście niedostępny;
- monitorować brakujące własne chunki, aby dostrajać okres przechowywania na podstawie realnych danych.
Warstwa przechowywania zapobiega większości awarii. Warstwa odzyskiwania obsługuje pozostałe przypadki, których żadne skończone okno nie wyeliminuje całkowicie.
Nie usuwaj starych chunków według ślepej reguły wieku pliku
Naiwna reguła typu „usuń każdy plik starszy niż siedem dni” również może być błędna. Bieżące wydanie może ponownie wykorzystywać starszy plik z haszem, którego czas modyfikacji jest dawny tylko dlatego, że jego zawartość się nie zmieniła.
Lepszy model usuwania nieużywanych plików powinien znać wydania:
- zachowywać manifesty lub inwentarze zasobów każdego wydania, które nadal mieści się w oknie zgodności;
- zbudować sumę wszystkich ścieżek zasobów wskazywanych przez te wydania;
- nigdy nie usuwać niczego z tego chronionego zbioru;
- usuwać zasoby bez odwołań dopiero po dodatkowym okresie bezpieczeństwa.
Jeśli dla małego wdrożenia to za dużo mechaniki, celowo obszerny katalog zasobów statycznych jest często tańszy niż diagnozowanie rzadkich błędów klientów. Pliki z haszem nadają się do tego szczególnie dobrze, ponieważ identyczna zawartość naturalnie ponownie wykorzystuje stabilne adresy URL albo przynajmniej nie może nadpisać niezwiązanej zawartości pod tą samą nazwą z haszem.
Reguła, której bym unikał, jest prosta: nie wykonywać --delete na wspólnym drzewie /_next/static w tej samej operacji, która aktywuje nowe wydanie.
Next.js ma jawną ochronę przed rozjazdem wersji, ale nie jest ona magazynem starych zasobów
Aktualny Next.js obsługuje deploymentId jako zabezpieczenie przed rozjazdem wersji. Konfiguracja może wyglądać tak:
// next.config.js
const nextConfig = {
deploymentId: process.env.DEPLOYMENT_VERSION,
}
module.exports = nextConfig
Zgodnie z aktualną dokumentacją Next.js dotyczącą deploymentId po skonfigurowaniu tego pola do adresów statycznych zasobów zarządzanych przez framework dodawany jest parametr ?dpl=<deploymentId>, żądania nawigacyjne po stronie klienta przenoszą informacje o wdrożeniu, a serwer sygnalizuje swój identyfikator wdrożenia w odpowiedziach. Gdy Next.js wykryje niezgodność podczas nawigacji, może wykonać pełną nawigację zamiast kontynuować przejście po stronie klienta z niezgodnymi danymi.
?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-id
To wartościowe, ale nie należy przypisywać tej funkcji większych możliwości, niż ma naprawdę. Dokumentacja wprost mówi, że Next.js nie używa przychodzącego parametru ?dpl= do kierowania żądania do odpowiedniej wersji. Parametr służy do ominięcia nieaktualnej pamięci podręcznej. Jeśli własny serwer źródłowy fizycznie usunął stary zasób, parametr zapytania nie odtworzy pliku.
Dlatego traktuję deploymentId jako mechanizm wykrywania rozjazdu i odzyskiwania, a nie zamiennik poprawnego wdrażania lub przechowywania zasobów.
Na platformach obsługujących routing zależny od wersji infrastruktura może zrobić więcej. Na przykład aktualna dokumentacja Vercel dotycząca ochrony przed rozjazdem opisuje przypięcie do wersji, dzięki któremu żądania zarządzane przez framework mogą nadal trafiać do wdrożenia, które pierwotnie obsłużyło klienta. To funkcja platformy, której nie zakładam w dowolnej konfiguracji Nginx lub CDN.
Identyfikator kompilacji i identyfikator wdrożenia rozwiązują powiązane, ale różne problemy
Next.js tworzy również identyfikator kompilacji podczas next build. Jeśli kilka kontenerów ma obsługiwać to samo wdrożenie, nie powinny po cichu stać się różnymi kompilacjami tylko dlatego, że każdy serwer niezależnie wykonał własny etap budowania.
Deterministyczny identyfikator kompilacji można powiązać z identyfikatorem wydania, na przykład z rewizją Git:
// next.config.js
const nextConfig = {
generateBuildId: async () => process.env.GIT_SHA,
deploymentId: process.env.GIT_SHA,
}
module.exports = nextConfig
To przykład ilustracyjny, a nie fragment skopiowany z mojego kodu produkcyjnego. Ważna zasada architektoniczna jest taka, że jedno logiczne wydanie powinno mieć jeden spójny artefakt kompilacji i jedną tożsamość wdrożenia we wszystkich instancjach, które je obsługują.
generateBuildId identyfikuje kompilację Next.js. deploymentId jest udokumentowany konkretnie jako mechanizm ochrony przed rozjazdem wersji i omijania nieaktualnej pamięci podręcznej. Pojęcia są powiązane, ale traktowanie ich nazw jako synonimów utrudnia diagnozowanie.
Publikowałbym zasoby przed przełączeniem ruchu na nowy dokument
Bezpieczniejsza kolejność wdrożenia jest celowo asymetryczna. Nowe niezmienne zasoby mogą istnieć, zanim ktokolwiek zacznie się do nich odwoływać. Nowy HTML nie powinien natomiast wskazywać zasobów, które nie są jeszcze dostępne.
W uproszczeniu chcę takiej kolejności:
1. skompilować wydanie B jeden raz
2. przesłać zasoby /_next/static wydania B
3. sprawdzić, czy wymagane zasoby rzeczywiście można pobrać
4. uruchomić lub przygotować serwer i środowisko wykonawcze B
5. sprawdzić stan B
6. atomowo przełączyć ruch nowych dokumentów na B
7. pozostawić dostępne zasoby statyczne A
8. monitorować B
9. stare zasoby usunąć później
Jeśli aplikacja jest statycznym eksportem, obowiązuje ta sama zasada: najpierw przesłać wersjonowane zasoby, potem opublikować HTML, który się do nich odwołuje. Jeśli jest to renderowanie po stronie serwera za odwrotnym proxy, przygotować nowy serwer i przełączyć ruch dopiero po potwierdzeniu jego zdrowia.
Wycofanie wydania powinno być symetryczne. Zachowanie katalogu poprzedniej wersji i jej zasobów statycznych pozwala wrócić do aplikacji bez prób odtwarzania starych plików po fakcie.
Nie czyni to bezpiecznym każdego wycofania. Migracja bazy danych lub niezgodny kontrakt usługi backendowej mogą uniemożliwić działanie starej wersji aplikacji nawet wtedy, gdy jej JavaScript nadal istnieje. Przechowywanie zasobów statycznych rozwiązuje problem zgodności statycznej, a nie każdy problem zgodności między wydaniami całego systemu.
Wspólny katalog niezmiennych zasobów dobrze sprawdza się przy prostym samodzielnym hostingu
W małym wdrożeniu opartym na Nginx prostym wzorcem jest oddzielenie bieżącego wydania aplikacji od wspólnego magazynu zasobów statycznych.
Przykładowy układ może wyglądać tak:
/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/
/srv/app/shared/_next/static/...
Każde wdrożenie dodaje nowe pliki /_next/static do wspólnego katalogu bez usuwania plików z wcześniejszych zachowanych wydań. Nginx może udostępniać tę ścieżkę z polityką niezmienności:
location ^~ /_next/static/ {
root /srv/app/shared;
add_header Cache-Control "public, max-age=31536000, immutable";
}
Ta konfiguracja jest przykładem, a nie opisem dokładnej konfiguracji Nginx, której używałem. W prawdziwym wdrożeniu trzeba uwzględnić uprawnienia, typy MIME, warianty kompresji, zachowanie CDN i dokładny układ plików wynikowych.
Najważniejsza jest architektura: zmienny wskaźnik bieżącego wydania i głównie dopisywany magazyn wersjonowanych zasobów mają różne cykle życia.
HTML potrzebuje innej polityki pamięci podręcznej niż chunki z haszem
Najłatwiej odtworzyć problem, przechowując HTML w pamięci podręcznej tak, jakby był zasobem z haszem zawartości.
Dla dynamicznie renderowanych stron Next.js framework zwykle stosuje odpowiedzi, których nie należy buforować, jeśli zawierają dynamiczne dane właściwe dla użytkownika. Strony statyczne i ISR mają inne zasady i CDN może je prawidłowo przechowywać. Statyczny eksport serwowany przez Nginx jest jeszcze bardziej zależny od nagłówków ustawionych przez administratora.
Dlatego nie stosuję jednej uniwersalnej reguły pamięci podręcznej do „całej witryny”. Myślę kategoriami obiektów:
zasób z haszem w /_next/static
długi max-age
immutable
bezpieczny do przechowywania
HTML / dokument trasy
musi móc przejść do nowego wydania
polityka zależy od sposobu renderowania
nie może żyć dłużej niż zasoby, do których się odwołuje
RSC / dane nawigacyjne / dane API
osobne reguły zgodności i aktualności
Jeśli używany jest CDN, po wdrożeniu może być potrzebne unieważnienie ścieżki nowego dokumentu, zależnie od projektu pamięci podręcznej. Usuwanie starych chunków z haszem z CDN tylko dlatego, że istnieje nowe wydanie, zwykle działa przeciwskutecznie: jeśli serwer źródłowy również je usunął, znika ostatnia kopia, która mogła uratować starego klienta.
Przewodnik Next.js po pamięci podręcznej CDN jest tu przydatny, ponieważ oddziela buforowanie stron od rocznej polityki immutable stosowanej do zasobów /_next/static.
Automatyczne przeładowanie jest narzędziem odzyskiwania, a nie główną strategią wdrożenia
Typową odpowiedzią na błąd chunka jest „po prostu przeładuj stronę”. Często działa, bo pełna nawigacja pobiera bieżący dokument, który odwołuje się do bieżącej kompilacji.
Ślepe przeładowywanie przy każdym błędzie skryptu tworzy jednak nowe problemy:
- błąd zewnętrznego skryptu może spowodować bezsensowne przeładowanie;
- prawdziwa awaria serwera może utworzyć nieskończoną pętlę przeładowań;
- niezapisany formularz może utracić dane użytkownika;
- stan komponentów React znika przy pełnej nawigacji;
- to samo uszkodzone wdrożenie po prostu może zawieść ponownie.
Aktualna dokumentacja Next.js sama ostrzega, że pełna nawigacja wykorzystywana do odzyskiwania przy rozjeździe może utracić stan komponentów, taki jak useState, natomiast stan zapisany w URL lub trwałej pamięci przeglądarki może przetrwać.
Jeśli dodaję odzyskiwanie po stronie klienta, chcę, aby było wąskie i jednorazowe. Przykładowa implementacja może wyglądać tak:
const RECOVERY_KEY = 'next-chunk-recovery-attempted'
function isOwnNextAsset(url: string) {
try {
const parsed = new URL(url, window.location.href)
return (
parsed.origin === window.location.origin &&
parsed.pathname.startsWith('/_next/static/')
)
} catch {
return false
}
}
window.addEventListener(
'error',
(event) => {
const target = event.target
if (!(target instanceof HTMLScriptElement)) return
if (!isOwnNextAsset(target.src)) return
reportChunkFailure({
page: window.location.href,
asset: target.src,
})
if (sessionStorage.getItem(RECOVERY_KEY)) return
sessionStorage.setItem(RECOVERY_KEY, '1')
window.location.reload()
},
true,
)
To celowo tylko przykład. Wersja produkcyjna powinna uwzględniać również chunki arkuszy stylów, znane postacie błędów frameworka, przepływy użytkownika, w których przeładowanie niszczy pracę, oraz sposób usunięcia znacznika odzyskiwania po poprawnym załadowaniu.
W edytorze, procesie zakupu lub długim formularzu mogę woleć komunikat „Dostępna jest nowa wersja; zapisz pracę i przeładuj stronę” zamiast wymuszonego odświeżenia.
Dane monitoringu powinny pokazać, czy to rzeczywiście rozjazd wersji
Komunikat mówiący tylko „nie udało się wczytać skryptu” nie wystarcza. Aby odróżnić usunięty stary chunk od losowej awarii sieci, potrzebuję kontekstu wdrożenia.
Przydatne pola to:
- URL zasobu, którego nie udało się załadować;
- URL bieżącej strony;
- czy zasób należy do samej aplikacji;
- identyfikator wydania lub wdrożenia widoczny dla klienta;
- przeglądarka i system operacyjny;
navigator.onLinejako słaby sygnał, a nie dowód łączności;- czas od załadowania strony;
- czy błąd nastąpił krótko po wdrożeniu;
- czy była to pierwsza próba odzyskiwania;
- status HTTP, gdy można go zobaczyć po stronie serwera;
- wydanie obsługujące w danej chwili żądanie na serwerze źródłowym lub proxy.
Wtedy wzorce stają się znacznie bardziej informacyjne.
Jeśli wielu użytkowników z różnych sieci żąda starych adresów chunków z haszem, a serwer źródłowy zaraz po wydaniu zwraca 404, brak zachowanych starych zasobów staje się mocnym wyjaśnieniem. Jeśli tylko jeden użytkownik widzi błąd sieciowy bez odpowiedzi HTTP, rozjazd wersji jest znacznie mniej pewny. Jeśli chunk wraca z kodem 200, ale z błędnym typem MIME albo jako dokument HTML z błędem, problem leży w routingu lub konfiguracji proxy, a nie tylko w przechowywaniu.
Oddzieliłbym też alerty o błędach własnych chunków od awarii zasobów zewnętrznych. To zmiana monitoringu najsilniej uzasadniona moimi pierwotnymi logami: istotny sygnał był zmieszany z dużą ilością niezwiązanego z awarią aplikacji szumu przeglądarki.
Test odtworzenia jest prosty, ale trzeba zachować starą kartę
Ten rodzaj błędu łatwo przeoczyć w zwykłych testach wydania, ponieważ inżynierowie mają tendencję do odświeżania strony natychmiast po wdrożeniu. W ten sposób niszczą dokładnie ten warunek, który próbują zbadać.
Lepszy test ręczny wygląda tak:
- wdrożyć wydanie A;
- otworzyć kartę w warunkach zbliżonych do produkcyjnych z włączoną pamięcią podręczną przeglądarki;
- odwiedzić tylko część aplikacji, aby niektóre trasy lub funkcje ładowane na żądanie pozostały niewczytane;
- pozostawić tę kartę otwartą;
- wdrożyć wydanie B;
- nie odświeżać starej karty;
- uruchomić trasę lub funkcję dynamiczną wymagającą kodu, który nie był wcześniej wczytany;
- sprawdzić Network i Console;
- zweryfikować, czy stare adresy zasobów nadal zwracają 200;
- zweryfikować, czy wykrycie rozjazdu uruchamia kontrolowaną pełną nawigację, gdy jest to potrzebne.
Powtórzyłbym ten sam test z CDN przed aplikacją, z dwiema instancjami serwera podczas stopniowego wdrożenia oraz po wygaśnięciu ustawionego okna przechowywania.
Subtelny błąd testowy to włączenie „Disable cache” w DevTools dla wszystkiego. Przy niektórych diagnozach jest to użyteczne, ale zmienia zachowanie przeglądarki. Scenariusz długo otwartej karty trzeba również testować z realistyczną pamięcią podręczną, bo pamięć przeglądarki jest częścią systemu.
Nie każdy błąd chunka da się naprawić przechowywaniem starych plików
Przechowywanie jest skuteczne właśnie dlatego, że rozwiązuje jeden konkretny mechanizm. Nie powinno stać się kolejnym uniwersalnym wyjaśnieniem.
Własny chunk może się nie załadować, ponieważ:
- żądanie w ogóle nie dotarło do serwera;
- połączenie zostało przerwane;
- rozszerzenie przeglądarki zablokowało żądanie;
- węzeł CDN miał chwilową awarię;
- Nginx nieprawidłowo skierował ścieżkę;
- serwer zwrócił dokument błędu HTML zamiast JavaScript;
- kompresja lub
Content-Encodingzostały uszkodzone; - uprawnienia pliku były nieprawidłowe;
- częściowe wdrożenie nigdy nie przesłało chunka;
- plik istniał, ale został usunięty zbyt wcześnie;
- klient i serwer działały na niezgodnych wydaniach.
Kod odpowiedzi i czas zdarzenia mają znaczenie. Powtarzający się 404 dla starego adresu URL z haszem po każdym wydaniu opowiada zupełnie inną historię niż ERR_CONNECTION_RESET w jednej sieci komórkowej.
Dlatego nie przepisałbym pierwotnego incydentu jako „udowodniłem, że nieaktualny HTML zepsuł witrynę”. Tego nie udowodniłem. Zobaczyłem prawdziwą awarię własnego chunka i wskazałem rozjazd wersji jako jeden z poważnych mechanizmów, które warto wyeliminować na poziomie projektu.
Najbezpieczniejsze wdrożenie traktuje starych klientów jako część powierzchni wydania
Głębszym błędem jest założenie, że wdrożenie zastępuje wersję A wersją B w jednym momencie.
Na serwerze może tak wyglądać zmiana dowiązania symbolicznego albo decyzja orkiestratora kontenerów. W sieci stare obiekty CDN mogą nadal istnieć. W przeglądarkach dokumenty z A mogą działać długo po uruchomieniu B. Podczas stopniowego wydania obie wersje serwera mogą być aktywne. Przy wycofaniu B może zniknąć, a A znów stać się bieżąca.
Rzeczywista powierzchnia wydania jest więc przedziałem czasu, a nie punktem.
Moje zasady wdrażania aplikacji Next.js opierają się teraz na tej idei:
- Kompiluj raz dla każdego logicznego wydania. Nie pozwalaj replikom po cichu tworzyć niezależnych wyników.
- Publikuj niezmienne zasoby przed opublikowaniem odwołań do nich.
- Przechowuj stare zasoby z haszem przez świadomie wybrane okno zgodności.
- Nie dawaj zmiennemu HTML tej samej polityki pamięci podręcznej co chunkom z haszem.
- Używaj
deploymentId, jeśli model wdrożenia może powodować rozjazd wersji. - Korzystaj z ochrony platformowej tylko wtedy, gdy hosting rzeczywiście zapewnia routing świadomy wersji.
- Niech odzyskiwanie będzie jednorazowe i uwzględnia stan użytkownika.
- Monitoruj błędy własnych chunków jako osobny sygnał produkcyjny.
- Testuj wdrożenie ze starą kartą nadal otwartą.
- Usuwaj stare zasoby później, nie w trakcie aktywowania nowego wydania.
Zasada, której używam teraz
Udana kompilacja i poprawnie działająca świeżo otwarta strona nie dowodzą, że wdrożenie jest bezpieczne dla użytkowników, którzy byli już w aplikacji.
Stara karta nie jest niepotrzebną pozostałością. To prawdziwy klient uruchamiający prawdziwe poprzednie wydanie.
Kiedy zacząłem patrzeć na wdrożenia w ten sposób, problem chunków stał się mniej tajemniczy. Hasz zawartości nadaje zasobowi stabilną tożsamość. Długie buforowanie pozwala wydajnie z niej korzystać. Ale system wdrożeniowy musi respektować tę tożsamość wystarczająco długo albo zapewnić klientowi kontrolowany sposób przejścia dalej.
Nie potrzebuję utrzymywać każdego starego wydania na zawsze. Potrzebuję, aby system przetrwał okres, w którym stare klienty i nowe serwery zgodnie z założeniem współistnieją.
To jest kontrakt wdrożeniowy, na którym mi zależy: nowi użytkownicy dostają nowe wydanie, starzy nie tracą plików, o które ich bieżąca wersja nadal potrafi poprosić, a każdy pozostały rozjazd kończy się przewidzianą ścieżką odzyskiwania zamiast uszkodzoną stroną.