Strona główna / Artykuły / Zrozumienie dyrektywy „use cache” oraz ponownej walidacji opartej na tagach w Next.js 16

Zrozumienie dyrektywy „use cache” oraz ponownej walidacji opartej na tagach w Next.js 16

Dowiedz się, jak działa dyrektywa „use cache” w Next.js 16, jej towarzyszące funkcje ponownej walidacji oraz jak zastosować cacheowanie uwzględniające różne użytkowników w aplikacjach wielu użytkowników.

1215 słów

Kasowanie cache’u w Next.js tradycyjnie kojarzyło się z czymś w rodzaju „czarnej skrzynki” — mieszanką ustawień na poziomie plików, opcji przekazywanych do fetch oraz domyślnych ustawień frameworka, które tak bardzo zmieniały się pomiędzy wersjami, że nawet doświadczeni programiści trzymali otwartą dokumentację dla pewności. Dyrektywa "use cache" wprowadzona w Next.js 16, będąca częścią szerszego modelu Cache Components, zmienia tę sytuację, pozwalając wyraźnie określić zachowanie kasowania cache’u na poziomie komponentu lub funkcji, zamiast odziedziczać je domyślnie i polegać na pamięci.

Poniżej znajduje się opis tego, co faktycznie robi ta dyrektywa, oraz sytuacji, w których ma sens jej używanie.

Co faktycznie robi ta dyrektywa

Umieszczenie "use cache" wewnątrz funkcji lub komponentu informuje Next.js o konieczności zapisywania w pamięci tym, co zwraca dana funkcja. Koncepcyjnie pełni rolę podobną do "use client", z tą różnicą, że zamiast oznaczać granicę strony klienckiej, oznacza granicę cache’owania:

async function getDashboardStats(tenantId: string) {
  "use cache";
  const stats = await db.query.stats.findMany({ where: { tenantId } });
  return stats;
}

Wynik funkcji jest zapisywany w pamięci i klasyfikowany na podstawie przekazanych argumentów, a następnie ponownie wykorzystywany w kolejnych żądaniach, dopóki coś go nie unieważni. To istotna odmiana w porównaniu z starszym podejściem polegającym na cache’owaniu pojedynczego wywołania fetch lub całego segmentu trasy – teraz można cache’ować na dowolnym poziomie szczegółowości odpowiadającym danym, aż po pojedynczą funkcję.

Trzy towarzyszące funkcje

Oprócz tej dyrektywy, Cache Components wprowadzają niewielki zestaw interfejsów API do celowego zarządzania danymi zapisanymi w pamięci, zamiast po prostu czekać na wygaśnięcie timera:

  • revalidateTag(tag) — usuwa wszystkie wpisy z pamięci podręcznej powiązane z danym tagiem. Jest to przydatne, gdy jedna modyfikacja wpływa na dane, od których zależą kilka różnych funkcji przechowujących dane w pamięci podręcznej.
  • updateTag(tag) — węższa wersja tej samej koncepcji, przeznaczona do aktualizacji konkretnych wpisów z pamięci podręcznej powiązanych z jednym tagiem, a nie wszystkiego, co znajduje się pod nim.
  • refresh() — odświeża dane przechowywane w pamięci podręcznej dla bieżącego żądania.

Oto wzorzec, który sprawia, że cały system funkcjonuje sprawnie: przypisz sensowny tag do każdej funkcji zapisanej w pamięci cache — coś w rodzaju tenant-stats lub invoice-list — a za każdym razem, gdy nastąpi zmiana wpływająca na te dane, wywołaj revalidateTag z odpowiednim tagiem, zamiast próbować ustalić okno wygaśnięcia oparte na czasie, które jest albo zbyt krótkie (co unieważnia sens cacheowania), albo zbyt długie (co powoduje dostarczanie przestarzałych danych).

async function getInvoices(tenantId: string) {
  "use cache";
  cacheTag(`invoices-${tenantId}`);
  return db.query.invoices.findMany({ where: { tenantId } });
}
// After creating an invoice:
async function createInvoice(data: InvoiceInput) {
  await db.insert(invoices).values(data);
  revalidateTag(`invoices-${data.tenantId}`);
}

To połączenie — tagowanie podczas odczytu i ponowna walidacja podczas zapisu — stanowi w istocie całą ideę w skrócie. Prawie wszystko inne, co będziesz robić z tym systemem, to wariacja tego samego połączenia.

Częste źródła zamieszania

Najczęstszym błędem, jaki popełniają zespoły, jest założenie, że "use cache" to prosta zamiennica opcji next: { revalidate } w funkcji fetch(). W rzeczywistości rozwiązują one różne, choć powiązane ze sobą, problemy. Opcja na poziomie fetch() kontroluje pojedynczą żądanie sieciowe. Natomiast "use cache" przechowuje w pamięci cachowej wynik całej funkcji lub komponentu, który wewnętrznie może wykonywać o wiele więcej operacji – łączyć się z bazą danych, przeprowadzać obliczenia, a nawet samodzielnie wywoływać funkcję fetch. Jeśli potrzebujesz jedynie zapamiętać wynik jednego zewnętrznego żądania API, korzystanie z cache’ingu na poziomie fetch() jest zazwyczaj prostszym rozwiązaniem. "use cache" okazuje się przydatne, gdy chcesz zapamiętać cały obliczony wynik, a nie tylko pojedyncze żądanie, które do niego prowadzi.

Drugim częstym błędem jest całkowite pominięcie kroku nadawania tagów, co później powoduje zamieszanie, gdy mutacja nie usuwa danych z pamięci podręcznej, które powinna usunąć. Bez dołączonego tagu jedynym mechanizmem unieważniania jest czas, co podważa większość zalet stosowania tego modelu – bierzesz na siebie dodatkową złożoność wyraźnego cacheowania, nie uzyskując przy tym pełnej kontroli nad unieważnianiem danych.

Tagi uwzględniające użytkowników w aplikacjach wieloużytkowniczych

Jeśli pracujesz nad czymś typu multi-tenant, istnieje jeden szczegół, który warto od razu zaznaczyć: tagi muszą kodować informacje o konkretnym użytkowniku, a nie tylko typ danych przechowywanych w pamięci cache. Ogólny tag taki jak invoices, używany przez wszystkich użytkowników, oznacza, że unieważnienie danych jednego z nich wpłynie na wszystkich – co może prowadzić albo do problemów z poprawnością (jeden użytkownik otrzymuje przestarzałe wyniki, ponieważ modyfikacja innego użytkownika wywołała wspólną ponowną weryfikację), albo do problemów z wydajnością (pamięć cache jest opróżniana znacznie częściej, niż to konieczne). Coś w rodzaju invoices-${tenantId}, jak pokazano we wcześniejszym przykładzie, nie jest kwestią stylu – to właśnie ono odróżnia skuteczną strategię unieważniania od tej, która nie działa.

Dlaczego warto to poprawnie skonfigurować od samego początku

Słoj cache’owania z subtelną wadą rzadko powoduje oczywiste problemy. Zamiast tego objawia się jako niejasne zgłoszenia wsparcia w stylu „dlaczego ten panel nadal pokazuje dane z zeszłego tygodnia” – błędy, których trudno jest dokładnie zlokalizować, ponieważ błędna ścieżka unieważniania znajduje się często w miejscu, którego nikt od miesięcy nie dotykał. Wdrożenie wcześnie spójnej strategii tagowania, stosowanej jednolicie we wszystkich funkcjach z cache’em w bazie kodu, to jedna z tych decyzji infrastrukturalnych, które przy prawidłowym wdrożeniu na początku są niewiele kosztujące, ale późniejsza korekta jest znacznie droższa.

Część zestawów startowych do paneli kontrolnych opiera swoją warstwę danych dokładnie na tej zasadzie. Jako przykład można podać szablon takiego jak szablon panelu kontrolnego Ovyqen dla projektów Next.js i SaaS, który stosuje parowanie „tag przy odczytaniu, ponowna walidacja przy zapisie” we wszystkich elementach, włączając identyfikatory użytkowników do każdego tagu od samego początku, zamiast je dodawać później po błędzie w buforowaniu między użytkownikami. Jeśli rozważasz szablony paneli kontrolnych, a niewiarygodność mechanizmu unieważniania bufora stanowi problem w twojej obecnej aplikacji, to jest uzasadniony powód, by zacząć od fundamentu, który już prawidłowo radzi sobie z tym zagadnieniem.

Częste pytania

Czy każde wezwanie fetch() powinno zostać przeniesione na "use cache"? Niekoniecznie — oba narzędzia się pokrywają, ale nie są ze sobą zamienne. Cacheowanie na poziomie fetch() nadal jest odpowiednie w prostych scenariuszach z jednym żądaniem. Użyj "use cache", gdy potrzebujesz zapisać w pamięci ciągłe wyniki obliczeń lub wyjście całego komponentu.

Czy "use cache" jest wystarczająco dojrzałe do użycia w panelach sterowania produkcyjnych? Traktuj je tak samo jak każdy inny, stosunkowo młody mechanizm cacheowania — dokładnie przetestuj ścieżki unieważniania danych, szczególnie w przypadku danych wielu użytkowników, zanim będziesz na nich polegać w elementach skierowanych do klientów, gdzie ważna jest aktualność informacji.

Co się dzieje, jeśli funkcja z pamięci podręcznej nie zostanie oznaczona tagiem? Pamięć podręczna nadal działa, ale tracisz możliwość celowego unieważnienia jej w odpowiedzi na konkretny zdarzenie. Musisz polegać wyłącznie na terminie ważności opartym na czasie, co rzadko jest zachowaniem, którego faktycznie oczekujesz.

Jawna konfiguracja pamięci podręcznej wymaga większego wysiłku na początku niż po prostu poleganie na ustawieniach domyślnych frameworka. Jednak w przypadku danych z panelu sterowania, gdzie pokazywanie przestarzałych informacji ma rzeczywiste konsekwencje, ten dodatkowy wysiłek jest niemal zawsze właściwą decyzją.

Literatura pokrewna

  • 20 zaawansowanych patternów Next.js dla aplikacji App Router o jakości produkcyjnej — Poznaj dwadzieścia zaawansowanych patternów Next.js na poziomie ekspertów obejmujących projektowanie typu server-first, transmisję strumieniową, cacheowanie, routowanie oraz optymalizację wydajności w celu tworzenia szybszych i skalowalnych aplikacji produkcyjnych.
  • Jak inżynieria frontendu rozwinęła się od stylizacji do systemów skalowalnych — Opisuje ewolucję od prostego HTML/CSS/JS do architektury komponentów, cacheowania, monorepo oraz mechanizmów monitoringu niezbędnych do niezawodnego obsługi milionów użytkowników.