Przejdź do treści

Strategie cache w Next.js 16: unstable_cache czy Redis?

Poznaj architekturę cache w Next.js 16. Zobacz, kiedy stosować "use cache", stare unstable_cache, a kiedy Redis i jak ograć inwalidację danych.

Maciej Sala

Founder StriveLab

9 min czytaniaOpublikowano 24 września 2025 (Aktualizacja 12 lipca 2026)

Porównam tu cache Next.js z Redisem. W Next.js 16 unstable_cache ma już status legacy, ale nadal spotkasz je w projektach. Nowy kierunek to use cache i Cache Components.

Wbudowany cache w Next.js

W App Routerze masz kilka warstw, które nie robią tego samego, więc nie wrzucajmy ich do jednego worka.

1. Rozszerzony fetch w Next.js

Code
// Domyślnie: bez Data Cache
const data = await fetch('https://api.example.com/posts')
 
// Rewalidacja co 60 sekund
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
})
 
// Bez cache
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
})

2. unstable_cache w Next.js

Przy danych z bazy, SDK albo innego źródła nie-HTTP wygląda to tak:

Code
import { unstable_cache } from 'next/cache'
 
const getCachedPosts = unstable_cache(
  async () => {
    return db.post.findMany()
  },
  ['posts'], // klucz cache
  {
    revalidate: 3600, // 1 godzina
    tags: ['posts'], // do rewalidacji on-demand
  },
)

unstable_cache uwzględnia argumenty funkcji w kluczu. keyParts służy do dodatkowych fragmentów niewidocznych w argumentach, np. wartości zamkniętych w closure — komentarz „klucz cache” jest więc uproszczeniem. Nie odczytuj cookies() ani headers() wewnątrz cache'owanej funkcji; pobierz potrzebną wartość wcześniej i przekaż jawnie jako argument. Dokumentacja Next.js 16 mówi wprost, że to API zostało zastąpione przez use cache.

3. use cache i Cache Components w Next.js 16

Najpierw włącz nowy model:

Code
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

Następnie oznacz funkcję, komponent albo cały plik jako cache'owany:

Code
// lib/posts.ts
import { cacheLife, cacheTag } from 'next/cache'
 
export async function getPost(slug: string) {
  'use cache'
 
  cacheLife({ stale: 300, revalidate: 900, expire: 86400 })
  cacheTag('posts', `post-${slug}`)
 
  return db.post.findUnique({ where: { slug } })
}

Argumenty i odwołania do zamkniętych wartości współtworzą klucz. Wynik musi być serializowalny. Scope cache nie może bezpośrednio czytać request-time APIs takich jak cookies(), headers() czy searchParams; wartości zależne od żądania przekaż jako argument albo pozostaw poza współdzielonym cache. Uważaj zwłaszcza na identyfikator użytkownika i tenant — pominięcie ich w kluczu może ujawnić dane między odbiorcami.

Domyślny handler runtime to LRU w pamięci procesu. Przy samodzielnym hostowaniu wpisy mogą przetrwać kolejne requesty tego procesu, ale znikają po restarcie. W kolejne requesty mogą trafić do różnych instancji, więc runtime cache zwykle nie jest wspólnym, trwałym magazynem. Build-time cache i integracja platformy mogą zachowywać się inaczej — sprawdź dokumentację hostingu.

4. React cache

Deduplikacja w ramach jednego renderowania:

Code
import { cache } from 'react'
 
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})
 
// Wielokrotne wywołania z tym samym id w jednym renderze/requestcie = jedno query

React cache służy do memoizacji i deduplikacji pracy podczas renderowania po stronie serwera; nie traktuj go jako trwałego cache między niezależnymi requestami ani zamiennika Redis. W development zachowanie może być dodatkowo zaciemnione przez cache Next.js.

Gdzie działa cache Next.js?

Tutaj sytuacja staje się bardziej złożona, ponieważ może się wydawać, że Next.js w prosty sposób zarządza pamięcią podręczną. Tak nie jest co pokazuje środowisko produkcyjne.

Data Cache Next.js

W środowisku produkcyjnym fetch(..., { cache: 'force-cache' }) oraz unstable_cache() wykorzystują Data Cache Next.js. Ten mechanizm buforowania jest w stanie przetrwać wiele żądań, a niekiedy nawet kolejne wdrożenia. Nie jest to jednak zwykła pamięć procesu, ponieważ jego topologia jest zależna od platformy, regionu oraz sposobu hostingu. Zatem, jeśli założymy globalnie spójny cache bez wcześniejszego sprawdzenia infrastruktury, ryzykujemy napotkanie różnych nieoczekiwanych błędów.

W praktyce wygląda to tak:

  • development zachowuje się inaczej niż produkcja i potrafi trzymać odpowiedzi także przez HMR,
  • Vercel dobrze integruje się z Data Cache, ale nadal myśli kategoriami regionów i opóźnień propagacji,
  • samodzielne hostowanie może wymagać świadomej strategii, jeśli potrzebujesz współdzielonego cache między instancjami.

Właśnie w tym kontekście Redis zyskuje na znaczeniu. Nie wynika to z niedoskonałości wbudowanego cache'u Next.js, lecz z faktu, że Redis zapewnia własny, współdzielony magazyn, który jest łatwiejszy do inspekcji, gdy pojawiają się nieoczekiwane zachowania.

Redis jako cacheHandler, a nie osobna warstwa aplikacyjna

Next.js 16 pozwala skonfigurować własny backend dla dyrektywy cache. To ważna trzecia opcja między lokalnym LRU a ręcznym redis.get() w każdej funkcji:

Code
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
  cacheHandlers: {
    remote: require.resolve('./cache-handlers/redis-handler.js'),
  },
}
 
export default nextConfig
Code
export async function getCatalog() {
  'use cache: remote'
  cacheLife('hours')
  cacheTag('catalog')
 
  return db.product.findMany()
}

Handler musi implementować kontrakt Next.js dotyczący odczytu, zapisu, wygaśnięcia i tagów; nie wystarczy podmienić Map na dwa wywołania Redisa. Użyj implementacji dostawcy albo napisz testy kontraktowe, współbieżności i invalidacji. Zaletą jest zachowanie cacheTag i semantyki Cache Components. Wadą pozostaje sieciowy roundtrip oraz koszt i dostępność zewnętrznego magazynu.

Możesz jednocześnie używać zwykłego 'use cache' dla lokalnego LRU i 'use cache: remote' dla danych wymagających współdzielenia. To nadal cache wielopoziomowy, ale granice są jawne w kodzie, a Next.js zarządza kluczami i tagami.

Redis jako zewnętrzny cache

Redis działa jako zewnętrzny cache, do którego zagląda każda instancja aplikacji:

Code
Użytkownik A → Instancja 1 → Redis MISS → Fetch → Redis WRITE
Użytkownik B → Instancja 2 → Redis HIT → Zwrot danych (bez fetch!)

Efekt jest prosty: instancja 1 zapisuje dane, instancja 2 może je odczytać. Bez zgadywania, co akurat trzyma lokalna warstwa.

Setup Redis z Upstash

Code
// lib/cache.ts
import 'server-only'
import { Redis } from '@upstash/redis'
 
const redis = Redis.fromEnv()
 
export async function cached<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number = 3600,
): Promise<T> {
  const cacheKey = `v1:${key}`
  const cached = await redis.get<T>(cacheKey)
  if (cached !== null) {
    return cached
  }
 
  const data = await fn()
 
  // Jitter ogranicza jednoczesne wygasanie wielu kluczy.
  const ttlWithJitter = ttl + Math.floor(Math.random() * Math.max(1, ttl * 0.1))
  await redis.setex(cacheKey, ttlWithJitter, data)
 
  return data
}
 
export async function invalidate(key: string): Promise<void> {
  await redis.del(`v1:${key}`)
}

Klucz odczytu i zapisu musi mieć ten sam namespace, więc buduj go w jednej funkcji, np. v1:${tenantId}:posts:${filterHash}. Wersja umożliwia natychmiastowe logiczne unieważnienie całej rodziny bez skanowania keyspace. Nie wkładaj do klucza surowych tokenów, e-maili ani innych danych osobowych, ponieważ klucze są widoczne w narzędziach administracyjnych i logach.

Powyższy helper wymaga jeszcze decyzji o awarii i dla treści publicznej błąd GET można potraktować jako miss i pobrać bazę, a błąd SET zalogować bez psucia odpowiedzi. Dla sesji, limitu żądań lub blokady takie „fail open” może być błędem bezpieczeństwa. Zachowanie ustal per przypadek, dodaj timeout i telemetrykę hit/miss/error.

Code
// lib/data.ts
import { cached } from './cache'
 
export async function getPosts() {
  return cached(
    'posts:all',
    () => db.post.findMany({ orderBy: { createdAt: 'desc' } }),
    300, // 5 minut
  )
}

unstable_cache kontra Redis

AspektCache Next.jsRedis
APIuse cache; legacy unstable_cacheKomendy ręczne lub cacheHandler
Współdzielenieuse cache: lokalne domyślnie; unstable_cache: zależne od Data Cache/platformyWspólne dla klientów tej bazy
Latencjause cache: pamięć procesu; unstable_cache: zależna od Data Cache/platformyRoundtrip sieciowy
Trwałośćuse cache: proces; unstable_cache: zależna od Data Cache/platformyZależna od konfiguracji persistence/TTL
InvalidacjaTagi, profile i ścieżki Next.jsDEL, wersje, indeksy lub własne eventy
Typy danychSerializowalne wyniki renderowaniaStrings, hashes, sets, sorted sets itd.
KosztPamięć/limity platformyUsługa, transfer, komendy, operacje
ObserwowalnośćZależna od hostinguZależna od dostawcy i telemetryki app

Kiedy zostać przy cache Next.js?

Zostań przy cache Next.js, gdy problem jest prosty i siedzi blisko renderowania:

  1. Dane rzadko się zmieniające — konfiguracja, tłumaczenia, statyczne treści
  2. Hosting zapewnia semantykę cache, której potrzebujesz — albo lokalność wpisów jest akceptowalna
  3. Brak potrzeby dokładania osobnej usługi — mniej rzeczy do utrzymania
  4. Rewalidacja blisko Next.js — tagi, ścieżki i renderowanie w jednym modelu
Code
// Dobre użycie unstable_cache
import { unstable_cache } from 'next/cache'
 
// Konfiguracja aplikacji — zmienia się rzadko
export const getConfig = unstable_cache(
  async () => {
    return db.config.findFirst()
  },
  ['app-config'],
  { revalidate: 3600 }, // 1 godzina
)

Statycznych modułów dołączonych do bundla, takich jak lokalne pliki tłumaczeń, zwykle nie trzeba dodatkowo wkładać do runtime cache. Cache ma sens dla kosztownego odczytu lub obliczenia, nie dla każdego import.

Kiedy dodać Redis do cache w Next.js?

Redis dorzucam dopiero wtedy, gdy wbudowany cache zaczyna robić się za mało przewidywalny:

  1. Dane współdzielone między instancjami. Liczniki, leaderboardy, blokady; sesje traktuj jako osobny state store.
  2. Wiele procesów lub regionów. Domyślny lokalny LRU nie zapewnia wspólnego widoku.
  3. Kontrolowana persistence i retencja. Jeśli dostawca, eviction policy i backupy są tak skonfigurowane.
  4. Operacje typowe dla Redisa. Sorted sets, pub/sub, TTL per klucz
  5. Debugowanie. Chcesz zobaczyć, co naprawdę siedzi w cache.
Code
// Dobre użycie Redis
 
// Stan sesji — osobny model bezpieczeństwa, nie cache odtwarzalny z bazy
await redis.setex(`session:${hashedSessionId}`, 86400, minimalSessionState)
 
// Liczniki w czasie rzeczywistym
await redis.incr(`pageviews:${slug}`)
 
// Leaderboard
await redis.zadd('leaderboard', { score: 100, member: 'user:123' })
 
// Cache z różnymi TTL
await redis.setex('hot-data', 60, data) // 1 minuta
await redis.setex('warm-data', 3600, data) // 1 godzina
await redis.setex('cold-data', 86400, data) // 24 godziny

Hybrydowy cache Next.js i Redis

W produkcji można połączyć cache Next.js z Redisem. W Next.js 16 najpierw sprawdź, czy wystarczy use cache: remote z handlerem Redis — zachowuje jeden model kluczy i tagów. Poniższa ręczna hybryda zagnieżdża Redis pod Data Cache, ale nie gwarantuje lokalnego L1, ponieważ topologię warstwy Next.js określa hosting. Stosuj ją tylko wtedy, gdy świadomie potrzebujesz dwóch niezależnych TTL i akceptujesz okno niespójności.

Code
// lib/cache.ts
import { unstable_cache } from 'next/cache'
import { Redis } from '@upstash/redis'
 
const redis = Redis.fromEnv()
 
// Poziom 1: unstable_cache / Data Cache (topologia zależy od hostingu)
// Poziom 2: Redis (wolniejszy, współdzielony)
 
export function hybridCache<T>(
  key: string,
  fn: () => Promise<T>,
  options: {
    nextCacheTtl?: number // TTL w Data Cache; topologia zależy od hostingu
    redisTtl?: number // TTL w Redis
    tags?: string[]
  } = {},
) {
  const { nextCacheTtl = 60, redisTtl = 3600, tags = [] } = options
  const redisKey = `v1:${key}`
 
  return unstable_cache(
    async () => {
      // Sprawdź Redis
      const cached = await redis.get<T>(redisKey)
      if (cached !== null) {
        return cached
      }
 
      // Pobierz dane
      const data = await fn()
 
      // Zapisz w Redis (@upstash/redis serializuje JSON automatycznie)
      await redis.setex(redisKey, redisTtl, data)
 
      return data
    },
    [key],
    { revalidate: nextCacheTtl, tags },
  )
}
Code
// Użycie
export const getPopularPosts = hybridCache(
  'posts:popular',
  () =>
    db.post.findMany({
      orderBy: { views: 'desc' },
      take: 10,
    }),
  {
    nextCacheTtl: 60, // Data Cache: 1 minuta
    redisTtl: 300, // Redis: 5 minut
    tags: ['posts'],
  },
)

Przepływ jest wtedy taki:

  1. Request trafia do instancji
  2. Sprawdź unstable_cache / Data Cache → HIT = zwróć
  3. Sprawdź Redis (współdzielony) → HIT = wynik trafi także do Data Cache
  4. MISS = pobierz z bazy → zapisz w Redis → wynik trafi do Data Cache

Rewalidacja cache w Next.js i Redis

Cache Next.js i wbudowane narzędzia rewalidacji

Code
import { revalidateTag, revalidatePath } from 'next/cache'
 
// Rewaliduj wszystko z tagiem 'posts'
revalidateTag('posts', 'max')
 
// Rewaliduj konkretną ścieżkę
revalidatePath('/blog')

Redis: pełna kontrola i pełna odpowiedzialność

Code
// Przy zapisie dodaj klucz do indeksu rodziny.
await redis.sadd('cache-index:posts', cacheKey)
 
export async function invalidateIndexedFamily(indexKey: string) {
  const keys = await redis.smembers<string>(indexKey)
  if (keys.length === 0) return 0
 
  // Dla bardzo dużych zbiorów iteruj SSCAN i usuwaj małymi paczkami.
  const pipeline = redis.pipeline()
  for (const key of keys) pipeline.del(key)
  pipeline.del(indexKey)
  await pipeline.exec()
  return keys.length
}

Hybrydowa rewalidacja cache

Code
// app/actions/posts.ts
'use server'
 
import { revalidateTag } from 'next/cache'
import { redis } from '@/lib/redis'
 
export async function createPost(data: PostData) {
  const post = await db.post.create({ data })
 
  // Najpierw L2, potem L1 — odwrotna kolejność może ponownie wypełnić L1 z L2.
  await redis.del('v1:posts:all', 'v1:posts:popular')
  revalidateTag('posts', 'max')
 
  return post
}

Jak uczciwie zmierzyć cache Next.js i Redis

Nie istnieje wiarygodna uniwersalna tabela typu „Next cache 1 ms, Redis 8 ms”. Lokalny LRU nie wykonuje sieciowego roundtripu, ale wynik całego requestu zależy również od serializacji, wielkości danych, cold startu i renderowania. Redis w tym samym regionie po trwałym połączeniu zachowa się inaczej niż REST do odległego regionu.

Mierz osobno:

  • Hit, miss, stale hit i błąd cache
  • p50, p95 i p99 czasu GET, SET oraz całego requestu
  • Koszt odtworzenia danych ze źródła
  • Rozmiar wartości po serializacji i transfer
  • Cold oraz warm start, jeden i wiele regionów
  • Zachowanie przy awarii Redisa oraz stampede
  • Hit ratio per rodzina kluczy, a nie tylko globalną średnią

Porównuj te same dane, tę samą wersję aplikacji i kontrolowany ruch. Cache o hit ratio 5% może dodawać koszt mimo świetnego czasu pojedynczego HIT. Z kolei wolniejszy współdzielony hit może chronić bazę skuteczniej niż szybki lokalny cache, do którego każdy proces trafia pierwszy raz.

Typowe błędy w cache Next.js i Redis

1. Brak jawnej polityki świeżości

Code
// To jest poprawne tylko dla danych niezmiennych lub unieważnianych zdarzeniem.
await redis.set('posts', data)
 
// Dla cache odtwarzalnego zwykle ustaw TTL z jitterem i/lub event invalidation.
await redis.setex('posts', 3600, data)

Nie każdy klucz Redis musi posiadać TTL; liczniki, kolejki czy rekordy sesji mają własne wymagania i nie zawsze pełnią funkcję cache. Dla danych odtwarzalnych należy jednak zdefiniować świadomą politykę stale/revalidate/expire, określając maksymalną akceptowalną nieświeżość oraz sposób unieważniania. Zachowanie typu „nigdy nie wygasa” nie powinno być przypadkowym domyślnym ustawieniem.

2. Brak obsługi cache miss

Code
// ❌ Źle — zakłada, że cache zawsze istnieje
const data = await redis.get('key')
return data.items // 💥 Error jeśli null
 
// ✅ Dobrze — obsłuż null
const data = await redis.get('key')
if (data === null) {
  return fetchFreshData()
}
return data

3. Cache stampede

wygląda niewinnie: cache wygasa, a wszystkie requesty naraz lecą po świeże dane. Baza dostaje serię takich samych zapytań i nagle „cache” staje się generatorem ruchu.

Code
// ✅ Rozwiązanie: lock
export async function cachedWithLock<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number,
): Promise<T> {
  const cached = await redis.get<T>(key)
  if (cached !== null) return cached
 
  const lockKey = `lock:${key}`
  const owner = crypto.randomUUID()
 
  for (let attempt = 0; attempt < 20; attempt++) {
    const acquired = await redis.set(lockKey, owner, { nx: true, px: 15_000 })
 
    if (acquired) {
      try {
        // Double-check po zdobyciu locka: poprzedni właściciel mógł już zapisać.
        const filled = await redis.get<T>(key)
        if (filled !== null) return filled
 
        const data = await fn()
        await redis.setex(key, ttl, data)
        return data
      } finally {
        // Usuń lock tylko wtedy, gdy nadal należy do tego requestu.
        await redis.eval(
          `if redis.call('get', KEYS[1]) == ARGV[1]
           then return redis.call('del', KEYS[1]) else return 0 end`,
          [lockKey],
          [owner],
        )
      }
    }
 
    await new Promise((resolve) =>
      setTimeout(resolve, 50 * 2 ** Math.min(attempt, 4) + Math.random() * 50),
    )
 
    const filled = await redis.get<T>(key)
    if (filled !== null) return filled
  }
 
  throw new Error('Cache refresh timeout')
}

TTL blokady powinien być dłuższy niż typowy czas odtworzenia danych lub bezpiecznie odnawiany. W przypadku wielu węzłów Redis i rygorystycznych wymagań warto zapoznać się z założeniami Redlock; blokada cache'owa nie zastępuje transakcji biznesowej. Często prostsze i skuteczniejsze okazują się strategie takie jak stale-while-revalidate, losowy jitter TTL oraz serwowanie ostatniej poprawnej wartości podczas odświeżania.

Cache Next.js czy Redis: co wybrać?

Code
Czy potrzebujesz współdzielonego cache między instancjami?
├─ NIE → `use cache` z domyślnym handlerem
└─ TAK → Czy potrzebujesz tylko semantyki cache Next.js?
         ├─ TAK → `use cache: remote` + współdzielony `cacheHandler`
         └─ NIE → Czy potrzebujesz komend Redisa lub własnego modelu danych?
                  ├─ TAK → Redis używany jawnie w warstwie aplikacyjnej
                  └─ NIE → sprawdź handler zapewniany przez platformę

Mój domyślny wybór:

  • Nowy projekt z Cache Components. use cache i jawne profile świeżości.
  • Wiele instancji wymagających wspólnego cache renderowania. use cache: remote z przetestowanym handlerem.
  • Liczniki, locki, sorted sets lub stan aplikacyjny. Jawny klient Redis, niezależnie od cache renderowania.
  • Stary projekt bez Cache Components. Utrzymaj unstable_cache do zaplanowanej migracji, zamiast mieszać modele bez testów.
Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czym różni się unstable_cache od use cache w Next.js?

unstable_cache to starsze API. W Next.js 16 nadal działa, ale ma status zastąpione przez use cache, ścieżkę Next.js 16 opartą o Cache Components i wymagająca cacheComponents: true. Nowy kod warto budować na dyrektywach cache, a migrację istniejącego unstable_cache planować wtedy, gdy przechodzisz na nowy model renderowania i przetestujesz semantykę.

Kiedy Redis jest lepszy niż wbudowany cache Next.js?

Redis jest potrzebny, gdy wiele instancji musi widzieć wspólny stan albo potrzebujesz atomowych liczników, sorted sets, blokad czy kontrolowanego TTL. W Next.js 16 może być zarówno ręcznie używanym magazynem, jak i backendem use cache: remote przez cacheHandlers. Sesja to trwały stan bezpieczeństwa, a nie zwykły cache, przez co wymaga osobnego modelu awarii.

Czy Redis spowalnia aplikację?

Dokłada roundtrip sieciowy, którego koszt zależy od regionu, protokołu, połączenia, wielkości wartości i dostawcy. Nie zakładaj stałych „kilku milisekund”. Redis umieszczaj blisko compute, używaj pipeliningu dla wielu komend i mierz percentyle p50/p95/p99 w swojej architekturze.

Co to jest cache stampede i jak go uniknąć?

Cache stampede powstaje, gdy wiele żądań jednocześnie odtwarza wygasły wpis. Pomagają stale-while-revalidate, jitter TTL, request coalescing i lock przez SET NX z unikalnym tokenem. Lock trzeba zwolnić atomowo tylko wtedy, gdy nadal należy do danego procesu, samo DEL może usunąć cudzą, odnowioną blokadę.

Czy powinienem cachować wszystkie zapytania do bazy danych?

Nie. Cachuj dane, które często się powtarzają i rzadko zmieniają: konfigurację aplikacji, popularne wpisy i wyniki kosztownych zapytań. Nie wciskaj cache tam, gdzie świeżość danych decyduje o poprawnym działaniu, na przykład przy statusie zamówienia w trakcie realizacji.

Jak poprawnie unieważniać cache w architekturze hybrydowej?

W modelu hybrydowym najpierw unieważnij współdzielony poziom Redis, a potem cache Next.js. Samo wywołanie dwóch komend nie daje atomowości: równoległy request może ponownie wstawić starą wartość. Przy ostrych wymaganiach użyj wersjonowanych kluczy, outboxa lub jednego źródła invalidacji i obserwuj częściowe awarie.

Czy Redis Upstash sprawdza się na produkcji?

Upstash to serverless Redis przez HTTP, więc łatwo podłączyć go do Next.js i środowisk bez stałych połączeń TCP. Może działać produkcyjnie, ale wybór zależy od regionów, modelu kosztowego per komenda, wymaganej trwałości, replikacji i percentyli opóźnień. Porównaj REST, protokół Redis oraz innych dostawców na swoim ruchu zamiast wybierać wyłącznie po skali.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Backend dla frontendowca: cache, deployment i bezpieczeństwo

To trzecia część serii „Backend dla frontendowca”. Po fundamentach API oraz tematach real-time, webhooków i uwierzytelniania zostaje warstwa, która decyduje o tym, czy aplikacja wytrzyma prawdziwe użytkowanie: cache , kolejki, pliki, deployment, monitoring i bezpieczeństwo .

Maciej Sala

Maciej Sala

Founder StriveLab

Cloudflare i samodzielne hostowanie Next.js

Samodzielnie hostowany Next.js na VPS daje kontrolę nad kosztami i konfiguracją, ale też zrzuca na nas odpowiedzialność za DNS, TLS, cache, ochronę przed atakami i prawdziwe IP klienta w logach. Cloudflare może stanąć przed Twoim serwerem jako bezpłatna warstwa CDN, SSL i podstawowej ochrony DDoS, o ile nie zaczniesz mechanicznie cachować całej aplikacji.

Maciej Sala

Maciej Sala

Founder StriveLab

Next.js 15 — co nowego i czy warto migrować z 14?

Next.js 15 zacieśnia współpracę z najnowszym React 19, stabilizuje Turbopacka w środowisku deweloperskim i wprowadza eksperymentalne Partial Prerendering . Z perspektywy czasu to jedno z największych tąpnięć od debiutu App Routera w „trzynastce" — i fundament, na którym Next.js 16 dopiął model Cache Components oraz domyślny Turbopack. W tym artykule rozkładam najważniejsze zmiany na czynniki pierwsze i podpowiadam, kiedy migracja z czternastki realnie się opłaca.

Maciej Sala

Maciej Sala

Founder StriveLab