Strona główna / Artykuły

Jak przechowywanie w przeglądarce potajemnie niszczy dane użytkownika i jak temu zapobiec

Pamięć przeglądarki może uszkodzić dane użytkownika bez wyświetlania błędów: utracone aktualizacje, przestarzałe zrzuty stanu, zmiany w schemacie oraz urządzenia współdzielone. Wzorce dla localStorage, które są odporne.

Wydawca: REACTAPP.TOP14 min czytania3109 słów

Źródła: The Browser Storage Mistake That Quietly Corrupts User Data

Zapisy w przeglądarce są zaskakująco proste. Zserializuj obiekt za pomocą JSON.stringify, odczytaj go ponownie za pomocą JSON.parse, a preferencje użytkownika, częściowo wypełniony formularz, filtry czy projekty przetrwają kolejne załadowanie. Żadnego schematu, żadnego połączenia sieciowego, żadnej migracji na serwer. Kilka linijek JavaScripta może sprawić, że aplikacja będzie wydawać się trwała w ciągu zaledwie kilku minut.

Ta prostota skłania do ryzykownego założenia: że cokolwiek zapisane w przeglądarce można traktować jako małą lokalną bazę danych.

Problemy zwykle zaczynają się od jednego dużego obiektu stanu. Aplikacja ładuje całość z localStorage, zmienia wartość jakiejś właściwości w pamięci i zapisuje całość z powrotem. Każdy krok wydaje się rozsądny – JSON jest poprawny, zapis udaje się, a na ekranie pokazuje się oczekiwana wartość. Nic nie wydaje się nieprawidłowe.

Problemy pojawiają się, gdy inna część aplikacji przechowuje przestarzałą kopię tego samego obiektu. Być może druga karta zmienia ustawienie, w tle wykonywane zadanie edytuje wersję roboczą, albo wcześniejsza wersja aplikacji interpretuje ten obiekt inaczej. Może też jakiś komponent odświeża stare dane tuż po tym, jak użytkownik już coś zmienił. Ponieważ każdy z tych mechanizmów zapisuje pełny zrzut stanu, zmiana jednego pola może potajemnie przywrócić stare wartości we wszystkich pozostałych polach.

Nie jest potrzebny żaden błąd składniowy, ani nie występuje żadne wyjątkowe zdarzenie związane z przechowywaniem danych. Każdy z tych mechanizmów może przechowywać w pełni poprawne dane, a mimo to zatrzeć ważną aktualizację innego mechanizmu.

API do przechowywania w rzeczywistości nie jest winne. Błąd polega na oczekiwaniu, że zwykły magazyn ciągów znaków, w którym decyduje ostatnia zapisana wartość, będzie zachowywać się tak, jakby znał rekordy, pola, transakcje, wersje, właścicieli oraz dostęp równoczesny. W rzeczywistości nie zna nic z tego i po prostu pamięta ostatnią przypisaną do klucza wartość w formie ciągu znaków.

Niewinna operacja czytaj-zmieniaj-zapisz

Typowa implementacja wygląda następująco:

function updatePreferences(changes) {
  const saved = localStorage.getItem("user-settings");
  const settings = saved ? JSON.parse(saved) : {};

  const nextSettings = {
    ...settings,
    ...changes,
  };
  localStorage.setItem(
    "user-settings",
    JSON.stringify(nextSettings)
  );
}

Z jedną kartą i jedną aktualną wersją aplikacji może ona działać przez lata. Każda aktualizacja odczytuje najnowszą wartość, łączy zmiany i zapisuje wynik. Jest krótka, łatwa do przeczytania i zazwyczaj wystarczająca do przechowywania podstawowych ustawień.

Ryzyko pojawia się, gdy upływa czas między odczytem a zapisem lub gdy aplikacja przechowuje kopię obiektu w pamięci.

Załóżmy, że dwie karty ładują te same ustawienia:

{
  "theme": "light",
  "fontSize": 16,
  "sidebarCollapsed": false
}

Pierwsza karta zmienia temat na ciemny. Druga zwiększa rozmiar czcionki do osiemnastu. Jeśli każda z nich edytuje swoją własną kopię w pamięci i zapisuje cały obiekt, to ta, która zapisze się ostatnia, usunie zmiany drugiej.

Pierwsza karta może przechowywać następujące dane:

{
  "theme": "dark",
  "fontSize": 16,
  "sidebarCollapsed": false
}

Później druga karta, nadal zawierająca starszy zrzut ekranu, przechowuje następujące dane:

{
  "theme": "light",
  "fontSize": 18,
  "sidebarCollapsed": false
}

Zmiana rozmiaru czcionki zostaje zachowana, a temat cicho wraca do wersji jasnej.

To jest skutek uszkodzenia spowodowanego brakiem aktualizacji. Ostateczny obiekt nie jest nieprawidłowo sformatowany, a każde pole zawiera prawidłową wartość. Problem polega na tym, że jedna ważna aktualizacja zniknęła bez żadnych oznak konfliktu.

Te błędy pozostają niewidoczne podczas rozwoju, ponieważ programiści testują aplikację po jednej karcie, na jednym urządzeniu i przy jednej akcji naraz. Prawdziwi użytkownicy trzymają aplikacje otwarte przez godziny, tworzą kopie kart, przywracają sesje, przechodzą między oknami i wracają na strony, których stan w pamięci jest starszy od tego, co jest obecnie zapisane.

API do przechowywania nie wie, że jedna karta zmieniła tylko temat, a druga tylko rozmiar czcionki. Widzi dwie pełne wartości i ta ostatnia zwycięża.

Zdjęcie stanu nie jest aktualnym stanem

Głębszym błędem jest przekonanie, że obiekt załadowany z pamięci przechowywania pozostaje połączony z nią. Tak nie jest. Funkcja JSON.parse tworzy wartość w pamięci tymczasowej reprezentującą zapisaną ciąg znaków w danym momencie – to jedynie jego zrzut ekranu. Tymczasem inna karta może zaktualizować klucz, komponent może go zastąpić, a nowsza wersja aplikacji może przenieść dane w inne miejsce, przy czym kopia już w pamięci o tym nie wie.

Zarządzanie stanem pogarsza tę sytuację. Aplikacja może na starcie załadować dane z localStorage i następnie przechowywać całą tę bazę po każdej zmianie. Kilka komponentów pracuje z tym zrzutem ekranu, podczas gdy inne elementy aplikacji modyfikują ten sam klucz.

Opóźnione zapisy powiększają okno czasowe. Wiele aplikacji stosuje mechanizm debounce w celu optymalizacji działania funkcji przechowywania danych, dzięki czemu localStorage.setItem nie jest wykonywany przy każdym naciśnięciu klawisza. Użytkownik edytuje pole, zapis jest planowany na kilkaset milisekund później, a funkcja closure przechowuje obiekt stanu z tego momentu. Zanim nastąpi zapis, inna karta lub komponent może zapisać nowsze dane. Gdy w końcu zostanie wywołana funkcja callback, zapisuje ona swój starszy zrzut stanu nad nowszymi wartościami.

Mechanizm debounce poprawił wydajność, ale pogorszył spójność, ponieważ nikt nie określił, co robić ze starymi zrzutami stanu.

Inicjalizacja asynchroniczna daje ten sam efekt. Komponent jest renderowany z ustawieniami domyślnymi, użytkownik może od razu z nim interakcjonować, a przechowywany stan trafia później za pośrednictwem efektu lub abstrakcji frameworka. Krok hydratacji następnie zastępuje obecny stan starszym zapisanym zrzutem. Dla użytkownika wydaje się, że zmiana została cofnięta chwilę po jej dokonaniu.

Takie błędy wydają się losowe, ponieważ każda zaangażowana wartość jest ważna. Prawdziwym problemem jest hierarchia czasowa: czy powinna przeważyć wcześniej załadowana wartość, późniejsza akcja użytkownika, czy dane wpisane w innym kontekście? Jeśli nie ma ustalonych zasad, decyduje to, co zostanie wykonane jako ostatnie.

Persystencja bez transakcji

localStorage jest zazwyczaj opisywany jako synchroniczne przechowywanie w formacie klucz-wartość. To prawda, ale może budzić większe zaufanie, niż API na to zasługuje.

Jedna wywołanie zastępuje wartość synchronicznie w bieżącym kontekście. Ta atomowość dotyczy tylko jednego wywołania. Sekwencja odczytu, modyfikacji i zapisu rozproszona na kilka kroków nie jest transakcyjna pomiędzy kartami lub procesorami. Nie ma mechanizmu porównania i zamiany, blokowania wierszy, konfliktów wersji, łączenia na poziomie pól ani możliwości cofnięcia działań.

Koncepcyjnie sekwencja pozostaje taka sama:

const current = read();
const next = modify(current);
write(next);

Inny kontekst może wykonać własną sekwencję operacji na innym zrzutu stanu. Każda operacja odczytu i zapisu kończy się sukcesem, ale łączny efekt skutkuje utratą danych.

Rozdzielanie kategorii na oddzielne klucze zmniejsza konflikty. Temat przechowywany pod jednym kluczem nie może nadpisać niepowiązanego projektu przechowywanego pod innym kluczem. To lepsze niż przechowywanie jednego ogromnego obiektu stanu, chociaż nie rozwiązuje wszystkich problemów z jednoczesnym dostępem, ponieważ dwóch użytkowników nadal może dojść do konfliktu o ten sam projekt lub rekord preferencji.

Przeglądarka wysyła również zdarzenie storage, aby poinformować inne dokumenty o zmianie klucza w localStorage. Pomaga to w synchronizacji, ale nie jest to mechanizm transakcyjny. Gdy inna karta otrzyma to zdarzenie, może już działać na podstawie przestarzałego stanu lub zaplanować zapis, który przywróci starszą wersję. Zdarzenie to informuje o dokonaniu kolejnego zapisu. Aplikacja nadal musi zdecydować, jak pogodzić to z niesoхранionymi lokalnymi zmianami.

Tutaj wiele implementacji staje się skomplikowanych. Programiści dodają słuchacze, łączą obiekty, aktualizują zbiory danych i tłumią zapisy cyrkularne, a wkrótce budują częściowy system synchronizacji na bazie API wybranego dlatego, że wydawało się ono prostsze niż rzeczywista warstwa danych.

To wszystko nie oznacza, że localStorage nie powinien przechowywać danych aplikacji. Oznacza to, że gwarancje, których potrzebujesz, muszą być zgodne z tym, co oferuje API. Dla małych wartości o wyraźnym właścicielu oraz przy tolerancji na nadpisywanie jest to dobry wybór. Staje się jednak kruchy, gdy kilka podmiotów niezależnie edytuje jeden wspólny, zstrukturyzowany rekord.

Jeden duży obiekt JSON ma duży zasięg wpływu

Przechowywanie całego stanu pod jednym kluczem jest wygodne, ponieważ proces hydratacji wymaga tylko jednego odczytu i parsowania. Oznacza to również, że każda aktualizacja nadpisuje wszystko.

Niewielka modyfikacja ustawienia bocznej pasku powoduje przepisanie wszystkich innych elementów dzielących ten sam obiekt: nieukończonego projektu, metadanych autoryzacji, ostatnich wyszukiwań, odrzuconych banerów oraz wartości form z pamięci podręcznej. Jeśli autor posiada przestarzałe dane dotyczące choćby jednego z tych elementów, niezwiązana z tym edycja przywraca stare wartości. Im więcej tematów jest zebranych w jednym kluczu, tym większe jest prawdopodobieństwo, że jeden autor nie zauważy zmian dokonanych przez innego.

Migracje również cierpią na to. Nowa wersja może przekształcić jedną część obiektu, zachowując resztę. Jeśli migracja nie jest kompletna, aplikacja może zapisać obiekt łączący pola z różnych wersji, a starsza wersja może później zachować swoją poprzednią strukturę, efektywnie cofając migrację.

Rozdzielanie przechowywania według właściciela i cyklu życia zmniejsza te konflikty. Ustawienia tematów, tymczasowe wersje, odrzucone powiadomienia oraz wpisy z pamięci podręcznej nie muszą dzielić się tą samą wersją, polityką wygaśnięcia ani ścieżką zapisu. Niezależne klucze utrudniają usunięcie zmian jednej kategorii przez wpływ na inną.

Rozdzielanie nie powinno być również mechaniczne. Tworzenie osobnego klucza dla każdej małej właściwości staje się trudne do zarządzania i nadal może prowadzić do sprzecznych kombinacji, gdy kilka wartości musi ulec zmianie jednocześnie. Dobrą granicą jest stan, który ma jednego właściciela, jedną strukturę oraz jedną zasadę aktualizacji. Wartości, które muszą być spójne jako całość, mogą należeć do siebie. Wartości zmieniające się z niepowiązanych powodów nie zyskują nic na dzieleniu się zrzutem stanu, poza dodatkowym narażeniem na problemy.

Traktuj klucz jako nośnik jednej spójnej informacji. Nie powinien on zastępować całej pamięci aplikacji.

Drift schematu przekształca stare dane w nowe zanieczyszczenia

Dane w przeglądarce często przetrwają dłużej, niż się oczekuje. Użytkownik może zachować ten sam profil przez dziesiątki wersji, a wartości zapisane przez starszą wersję mogą pojawić się ponownie po kilku miesiącach. Dlatego przechowywanie danych w przeglądarce ma swój schemat, nawet jeśli nikt go nie zaprojektował.

Załóżmy, że starsza wersja przechowywała wartość booleanową:

{
  "notifications": true
}

Nowsza wersja zastępuje go bardziej złożonym obiektem:

{
  "notifications": {
    "email": true,
    "push": false
  }
}

Kod, który zakłada nowy kształt bez sprawdzenia lub przeniesienia starej wartości, może wywołać błąd. Albo, co jeszcze bardziej niebezpieczne, może zastąpić je wartościami domyślnymi i później zapisać je zamiast przechowywanych danych, odrzucając to, co użytkownik wcześniej wybrał.

Zmienione nazwy pól powodują podobne problemy. Pozostała flaga darkMode może istnieć obok nowszej wartości theme. Jedna część aplikacji odczytuje nowe pole, starsza część zapisuje stary kształt, przez co preferencje użytkowników zmieniają się nieprzewidywalnie pomiędzy sesjami.

TypeScript nie rozwiązuje tego problemu. Kompilator może opisać aktualny oczekiwany kształt, ale JSON.parse zwraca dane z czasu wykonywania, utworzone przez nieznany wcześniej kod. Twierdzenie typu ucisza kompilator i utrudnia wykrycie niezgodności:

const settings =
  JSON.parse(savedValue) as CurrentSettings;

Ta linijka nie przekształca starego przechowywania zgodnie z obecnym schematem. Po prostu mówi TypeScriptowi, aby mu zaufał.

Bardziej bezpieczny kod traktuje zapisane wartości jako dane zewnętrzne. Analizuje je, weryfikuje ich strukturę, sprawdza wersję i celowo migruje starsze, znane formaty. Jeśli wartość nie może zostać bezpiecznie zinterpretowana, aplikacja powinna zdecydować, czy ją usunąć, zachować w celu odzyskania, czy zapytać użytkownika.

Numer wersji przechowywany obok danych sprawia, że przejścia są jawne:

{
  "version": 3,
  "data": {
    "theme": "dark"
  }
}

Liczba wersji nie uruchomi migracji, ale wskazuje kodowi, jakie założenia nadal są aktualne.

Prawidłowe parsowanie to nie gwarancja bezpieczeństwa

Rozwijający często otaczają JSON.parse blokiem try/catch i w ten sposób chronią warstwę przechowywania danych. Dzięki temu radzą sobie z niepoprawnym formatem JSON, co jest pomocne, ale poprawność składniowa to tylko pierwszy wymóg.

Obiekt po przetworzeniu może nie zawierać wymaganych pól, mieć niemożliwe wartości, odpowiadać przestarzałej schemacie lub zostać ręcznie edytowany za pomocą narzędzi programistycznych lub rozszerzeń. Wartości mogą również mieć prawidłowy typ, ale błędne znaczenie. Wartość rozmiaru strony -500 jest dopuszczalną liczbą w prawidłowym JSON. Temat o nazwie "ultraviolet" jest dopuszczalną łańcuchem tekstu. Projekt brakujący ID łączącego go z dokumentem jest dopuszczalnym obiektem. Żaden z nich nie zasługuje na automatyczne zaufanie jako stan aplikacji, ponieważ przeglądarka przechowuje łańcuchy tekstu i nic nie wie o zasadach domeny.

Niezawodne procesy hydratacji walidują to, co aplikacja jest gotowa przyjąć, wykorzystując biblioteki schematów, mały ręcznie napisany parser lub starannie zaprojektowane konstruktory. Ważne jest to, aby walidacja dostarczała znany wewnętrzny model zamiast wprowadzania surowych danych po przetworzeniu do aplikacji.

Walidacja to również miejsce, gdzie powinny znajdować się wartości domyślne. Brakująca opcjonalna preferencja może bezpiecznie otrzymać wartość domyślną. Brak identyfikatora w niesoхранёnym projekcie może oznaczać, że projekt nie może zostać dołączony do żadnego dokumentu i nie powinien być automatycznie przywracany. Traktowanie obu przypadków jako nieszkodliwego braku tworzy fałszywe przekonanie, które utrzymuje problem w ukryciu. Rozwiązanie awaryjne nie powinno ukrywać niepewności. Jeśli aplikacja nie może pokazać, że przechowywane dane należą do bieżącego użytkownika, schematu i rekordu, ich przywrócenie może przynieść więcej szkody niż ich zignorowanie.

Tożsamość powinna być częścią umowy przechowywania

Współdzielone przeglądarki wprowadzają kolejne fałszywe założenie, mianowicie że przechowywanie danych należy do osoby zalogowanej. W rzeczywistości należy ono do źródła przeglądarki.

Załóżmy, że jedna osoba wylogowuje się, a druga loguje na tym samym urządzeniu. Pozostałe projekty, filtry, ustawienia lub dane z pamięci podręcznej pozostają na miejscu, chyba że aplikacja je usunie lub umieści w odpowiednich przestrzeniach nazw. Ogólne klucze takie jak draft, recent-items lub dashboard-settings mogą spowodować, że dane pierwszej osoby trafią do sesji drugiej osoby. Nawet jeśli nie chodzi o żadne poufne informacje, sytuacja ta jest interpretowana jako uszkodzenie danych, ponieważ ważna wartość została przypisana niewłaściwemu użytkownikowi.

Usuwanie całej pamięci przechowawczej przy wylogowaniu pomaga, ale jest zbyt radykalne. Może usunąć ważne ustawienia na poziomie urządzenia, takie jak język czy ustawienia dostępności, a także może zawieść, gdy wylogowanie następuje w innej karcie, podczas gdy aktualna karta zachowuje kopię w pamięci.

Bardziej przemyślany projekt dzieli dane na te specyficzne dla użytkownika, sesji, urządzenia oraz publiczne. Zapisy należące do użytkownika mogą zawierać jego identyfikator w kluczu lub w obiekcie otaczającym, a proces hydratacji może potwierdzić, że zapisany właściciel odpowiada zalogowanej tożsamości, zanim przyjmie wartość.

Często potrzebny jest również identyfikator dokumentu lub zasobu. Przywracanie „najnowszego projektu” na poziomie globalnym jest niebezpieczne, gdy otwarte są kilka zapisów, ponieważ treść z jednego może pojawić się w drugim, jeśli klucz nie wskazywał, który obiekt jest jej właścicielem. Używanie nazw przestrzeni to coś więcej niż tylko uporządkowanie – zapobiega to trafiającym w niewłaściwy kontekst danym.

Szyfrowanie nie naprawia uszkodzeń

Rozmowy na temat zagrożeń związanych z pamięcią przeglądarki często skupiają się na bezpieczeństwie. Informacje poufne nie powinny być przechowywane lekkomyślnie w miejscach dostępnych z przeglądarki, szczególnie tam, gdzie skrypty mogą je odczytać. To rzeczywiście poważny problem, ale inny niż problem spójności.

Shifrowanie przechowywanej wartości w żaden sposób nie zapobiega nadpisywaniu się dwóch kart. Nie dodaje ono żadnych transakcji, kontrol wersji, informacji o właścicielu ani mechanizmów migracji. Aplikacja może bezbłędnie zaszyfrować przestarzały zapis, a następnie równie bezbłędnie usunąć nowszy. Zespoły czasami uciekają się do kodowania, zaciemniania lub szyfrowania, aby rozwiązać problem, który w rzeczywistości jest problemem projektowym. Te środki mogą pomóc w zachowaniu poufności w określonym modelu zagrożeń, ale nie czynią procesu aktualizacji bardziej niezawodnym.

Podobnie, przechodzenie z localStorage na IndexedDB może zapewnić większą pojemność, uporządkowane przechowywanie, indeksowanie oraz zachowanie transakcyjne. Samo w sobie nie tworzy poprawnej logiki aplikacji. Transakcja musi obejmować odpowiednie rekordy, aktualizacje nadal muszą uwzględniać zasady własności, a ulepszenia schematu wymagają dodatkowego projektowania. Technologia ta oferuje silniejsze elementy bazowe, a aplikacja musi je wykorzystywać zgodnie z jasnym modelem spójności.

Zacznij od kwestii własności

Zamiast zastanawiać się, którą API przechowywania wybrać, najpierw należy zapytać, kto jest właścicielem danych oraz jak należy radzić sobie ze zmianami wprowadzanymi przez innego użytkownika.

Zwykłe preferencje często mogą funkcjonować według zasady „ostatni wpis zwycięża”. Jeśli dwie karty ustawiają różne tematy, zachowanie najnowszej wersji może być dokładnie tym, czego się chce, choć aplikacja powinna nadal reagować na zmiany w innych kartach, jeśli widoczna interfejs użytkownika musi zostać natychmiast zaktualizowana.

Niedokończony szkic to zupełnie inna sprawa. Zasada „ostatni wpis zwycięża” może kosztować użytkownika prawdziwego wysiłku, dlatego zapisy mogą wymagać numeru rewizji, daty i godziny zmiany, identyfikatora dokumentu oraz polityki rozwiązywania konfliktów. Przed nadpisywaniem aplikacja może porównać różne wersje, przechować obie sprzeczne wersje lub ostrzec użytkownika, że inna karta edytowała ten sam szkic.

W przypadku ważnych zapisów udostępnianych między urządzeniami pamięć przeglądarki w ogóle nie powinna być źródłem prawdy. Może służyć jako cache offline lub tymczasowa kopia robocza, podczas gdy serwer przechowuje autorytatywne wersje i rozwiązuje konflikty.

Odpowiedni projekt zależy od tego, ile są warte dane i ilu osób je edytuje. Porównaj preferencje wyświetlania z formularzem prawniczym, długim szkicem artykułu czy zapisem finansowym offline – stawki są tu wyraźnie różne. Traktowanie ich wszystkich jako ciągów znaków pod dowolnymi kluczami ignoruje koszty utraty jednej aktualizacji.

Jedna ścieżka zapisu również pomaga. Gdy każdy komponent może czytać, łączyć i zastępować tę samą kluczową wartość, kiedy tylko mu się podoba, nikt nie może sprawdzić zasad spójności. Jeden moduł przechowywania może centralizować walidację, migrację, synchronizację oraz obsługę konfliktów. Nie jest to konieczne dla rozbudowanej warstwy repozytorium opartej na trzech ustawieniach. Dzięki temu kod odpowiadający za trwałość danych nie rozrasta się tak bardzo, że nikt nie jest w stanie określić, która wersja jest aktualna.

Zachowaj dane, nie reinterpretuj ich potajemnie

Najgorsze błędy w systemie przechowywania nie dają o sobie znać poprzez błąd QuotaExceededError czy niepowodzenie parsowania JSON. Takie problemy są widoczne, można je rejestrować, testować i rozwiązywać. Najbardziej szkodliwe są zapisy, które udają się, mimo że zapisuje się niewłaściwą wersję inaczej poprawnych danych.

Wyobraź sobie zastarzałą kartę, która edytuje jedno pole i przywraca pięć starych, opóźniony powrót funkcji, który niszczy nowszy projekt, stary build, który zapisuje przestarzałe schemat, lub nowe logowanie, które dziedziczy stan ostatniego konta. Z punktu widzenia API do przechowywania każda z tych operacji wykonała się pomyślnie. Zepsucie danych występuje dlatego, że aplikacja nigdy nie zdefiniowała wersji, właściciela ani reguł konfliktów.

Pamięć przeglądarki znajduje zastosowanie tam, gdzie jej ograniczenia odpowiadają zadaniu: przechowywanie ustawień, buforowanie wartości nieważnych, obsługa pracy offline, przywracanie nieukończonej pracy. Jednak przechowywanie czegoś zamienia tymczasowy stan w trwały dane wejściowe, co wymaga takiej samej troski jak w przypadku danych z serwera lub bazy danych.

W praktyce oznacza to sprawdzanie tego, co wraca, wersjonowanie danych, trzymanie oddzielnie niespowiązanych rekordów, dodawanie identyfikatorów użytkowników i zasobów tam, gdzie ważna jest odpowiedzialność za nie, oraz unikanie przepisywania dużych zrzutów stanu z powodu drobnych zmian. Oznacza to również świadome decydowanie o tym, czy zasada „ostatni, kto napisał, wygrywa”, jest do przyjęcia, aby przypadkowo nie stała się oficjalną zasadą.

Przeglądarka wiernie zachowa ostatnią otrzymaną wartość. To na Ciebie spada obowiązek upewnienia się, że osoba, która ostatnia coś napisała, rzeczywiście miała prawo decydować o tym, co zostanie zachowane.