Next.js Pytania Rekrutacyjne - Kompletny Przewodnik 2026
Next.js w 2026 to nie jest "React z SSR". To pełnoprawny framework fullstack z Server Components, App Router, Server Actions i Partial Prerendering. Jeśli rekrutujesz na stanowisko frontend developer i w ofercie jest Next.js - musisz znać te koncepcje na wylot.
W tym przewodniku znajdziesz 40 najczęstszych pytań rekrutacyjnych z Next.js - od podstaw App Router po zaawansowane Server Actions i optymalizację. Każda odpowiedź w formacie "30 sekund / 2 minuty" - dokładnie tak, jak na prawdziwej rozmowie.
Spis Treści
- Podstawy i Architektura
- Routing w App Router
- Renderowanie: SSR, SSG, ISR
- Pobieranie Danych
- Optymalizacja i Wydajność
- Metadata i SEO
- API i Backend
- Konfiguracja i Deployment
Podstawy i Architektura
1. Czym jest Next.js i jakie problemy rozwiązuje w porównaniu do czystego React?
Odpowiedź w 30 sekund:
Next.js to framework React oferujący Server Components, automatyczne SSR/SSG/ISR, routing oparty na plikach, optymalizację obrazów i fontów. Rozwiązuje problemy, które w czystym React wymagają ręcznej konfiguracji: SEO (SSR), wydajność (code splitting), routing (React Router), i konfigurację buildu.
Odpowiedź w 2 minuty:
Next.js rozwiązuje kilka kluczowych problemów czystego React:
SEO i First Contentful Paint:
// Czysty React - klient renderuje pustą stronę, potem JS ładuje content
// Google może nie zobaczyć treści
// Next.js - serwer wysyła gotowy HTML
// app/products/page.tsx
export default async function ProductsPage() {
const products = await getProducts(); // Wykonuje się na serwerze
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
// Google widzi pełną treść od razu
Routing bez dodatkowych bibliotek:
app/
├── page.tsx # /
├── about/page.tsx # /about
├── products/
│ ├── page.tsx # /products
│ └── [id]/page.tsx # /products/123
Optymalizacje out-of-the-box:
- Automatyczny code splitting per route
- Image optimization z next/image
- Font optimization z next/font
- Prefetching linków
Server Components:
- Komponenty renderowane na serwerze, zero JS do klienta
- Bezpośredni dostęp do bazy danych bez API
- Mniejszy bundle size
2. Jakie są różnice między App Router a Pages Router w Next.js?
Odpowiedź w 30 sekund:
App Router (od Next.js 13) używa React Server Components jako domyślnych, oferuje layouts, loading states, error boundaries i Server Actions. Pages Router to starsze API z getServerSideProps/getStaticProps. App Router jest rekomendowany dla nowych projektów.
Odpowiedź w 2 minuty:
Główne różnice między Pages Router a App Router obejmują architekturę komponentów, sposób pobierania danych oraz obsługę stanów aplikacji - poniższa tabela pokazuje porównanie:
| Cecha | Pages Router | App Router |
|---|---|---|
| Lokalizacja | /pages |
/app |
| Komponenty | Wszystkie Client | Server Components domyślnie |
| Data fetching | getServerSideProps, getStaticProps | async komponenty, fetch |
| Layouts |
_app.tsx, _document.tsx
|
Zagnieżdżone layout.tsx |
| Loading states | Ręczne | loading.tsx |
| Error handling | Ręczne | error.tsx |
| Metadata | Head component | Metadata API |
Pages Router (stare API):
// pages/products/[id].tsx
export async function getServerSideProps({ params }) {
const product = await getProduct(params.id);
return { props: { product } };
}
export default function ProductPage({ product }) {
return <div>{product.name}</div>;
}
App Router (nowe API):
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await getProduct(params.id); // Bezpośrednio w komponencie!
return <div>{product.name}</div>;
}
App Router oferuje też:
- Streaming z Suspense
- Parallel Routes (
@modal,@sidebar) - Intercepting Routes (
(.)photo,(..)settings) - Server Actions bez API routes
3. Czym są Server Components i Client Components w Next.js 13+?
Odpowiedź w 30 sekund:
Server Components renderują się na serwerze, nie wysyłają JS do klienta i mogą bezpośrednio czytać z bazy danych. Client Components (z dyrektywą 'use client') działają w przeglądarce i obsługują interakcje, hooks useState/useEffect, eventy. W App Router komponenty są Server Components domyślnie.
Odpowiedź w 2 minuty:
Server Components (domyślne w App Router):
// app/products/page.tsx - Server Component
import { db } from '@/lib/db';
export default async function ProductsPage() {
// Bezpośredni dostęp do bazy - bez API!
const products = await db.product.findMany();
return (
<ul>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</ul>
);
}
// Ten komponent NIE trafia do bundle JS klienta
Client Components:
// components/AddToCartButton.tsx
'use client'; // Dyrektywa wymagana!
import { useState } from 'react';
export function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Dodawanie...' : 'Dodaj do koszyka'}
</button>
);
}
Kiedy używać którego:
| Server Component | Client Component |
|---|---|
| Pobieranie danych | useState, useEffect |
| Dostęp do backendu | Event handlers (onClick) |
| Wrażliwe dane (API keys) | Browser APIs (localStorage) |
| Duże zależności (nie idą do bundle) | Interaktywne UI |
Kompozycja - Server owija Client:
// app/products/[id]/page.tsx (Server)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component wewnątrz Server Component */}
<AddToCartButton productId={product.id} />
</div>
);
}
4. Kiedy używać dyrektywy "use client" a kiedy "use server"?
Odpowiedź w 30 sekund:
'use client' oznacza komponent kliencki z interaktywnością (hooks, eventy). 'use server' definiuje Server Actions - funkcje wywoływane z klienta, wykonywane na serwerze (mutacje danych, operacje DB). Domyślnie komponenty w App Router są Server Components - nie wymagają dyrektywy.
Odpowiedź w 2 minuty:
'use client' - dla interaktywności:
'use client';
import { useState, useEffect } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Mounted in browser');
}, []);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
'use server' - dla Server Actions:
// app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title');
await db.post.create({ data: { title } });
revalidatePath('/posts'); // Odśwież cache
}
Użycie Server Action w formularzu:
// app/posts/new/page.tsx (Server Component)
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Utwórz</button>
</form>
);
}
// Działa nawet bez JavaScript! (Progressive Enhancement)
Zasada: Nie dodawaj 'use client' "na wszelki wypadek" - tracisz korzyści Server Components (mniejszy bundle, bezpośredni dostęp do danych).
5. Jak działa hydracja w Next.js i jakie są jej implikacje dla wydajności?
Odpowiedź w 30 sekund:
Hydracja to proces, gdzie React "przejmuje" statyczny HTML wysłany przez serwer i dodaje interaktywność (event handlers, state). W Next.js Server Components nie wymagają hydracji (zero JS), tylko Client Components. To znacząco zmniejsza czas do interaktywności (TTI).
Odpowiedź w 2 minuty:
Proces hydracji:
- Serwer renderuje HTML i wysyła do przeglądarki
- Przeglądarka wyświetla HTML (użytkownik widzi content)
- JavaScript się ładuje
- React "hydruje" - łączy HTML z event handlers
- Strona staje się interaktywna
Problem z tradycyjnym SSR:
Serwer: renderuje wszystko → HTML
Klient: ładuje CAŁY JS → hydruje WSZYSTKO
↓
Długi Time to Interactive (TTI)
Next.js z Server Components:
Server Components: renderują na serwerze → HTML (zero JS)
Client Components: tylko te wymagające interaktywności → JS + hydracja
↓
Mniejszy bundle, szybszy TTI
Streaming i Suspense:
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Natychmiast pokazuje header */}
<Suspense fallback={<p>Ładowanie statystyk...</p>}>
<SlowStats /> {/* Streamuje gdy gotowe */}
</Suspense>
<Suspense fallback={<p>Ładowanie wykresu...</p>}>
<SlowChart /> {/* Niezależnie streamuje */}
</Suspense>
</div>
);
}
Implikacje:
- Unikaj "use client" na wysokim poziomie drzewa
- Client Components głęboko w hierarchii = mniej JS do hydracji
- Selective Hydration - React hydruje najpierw to, z czym użytkownik wchodzi w interakcję
Routing w App Router
6. Jak działa system routingu oparty na plikach w App Router?
Odpowiedź w 30 sekund:
W App Router struktura folderów w /app definiuje routing. Folder = segment URL, page.tsx = renderowalna strona, layout.tsx = współdzielony layout. Specjalne pliki: loading.tsx, error.tsx, not-found.tsx obsługują stany UI automatycznie.
Odpowiedź w 2 minuty:
Struktura folderów = URL:
app/
├── page.tsx # /
├── layout.tsx # Root layout (wymagany)
├── loading.tsx # Loading UI dla /
├── error.tsx # Error UI dla /
├── not-found.tsx # 404 dla /
│
├── about/
│ └── page.tsx # /about
│
├── blog/
│ ├── page.tsx # /blog
│ ├── layout.tsx # Layout dla /blog/*
│ └── [slug]/
│ └── page.tsx # /blog/my-post
│
├── products/
│ ├── page.tsx # /products
│ └── [id]/
│ ├── page.tsx # /products/123
│ └── reviews/
│ └── page.tsx # /products/123/reviews
Specjalne pliki:
| Plik | Funkcja |
|---|---|
page.tsx |
UI strony (wymagany do renderowania) |
layout.tsx |
Współdzielony layout (nie re-renderuje przy nawigacji) |
loading.tsx |
Loading state (Suspense boundary) |
error.tsx |
Error boundary |
not-found.tsx |
404 page |
route.ts |
API endpoint (Route Handler) |
Przykład page.tsx:
// app/products/[id]/page.tsx
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
if (!product) {
notFound(); // Pokazuje not-found.tsx
}
return <div>{product.name}</div>;
}
7. Czym są segmenty dynamiczne i jak je definiować w Next.js?
Odpowiedź w 30 sekund:
Segmenty dynamiczne to foldery w nawiasach kwadratowych: [id], [slug]. Wartość jest przekazywana w params. Warianty: [...slug] (catch-all: /a/b/c), [[...slug]] (optional catch-all: również /).
Odpowiedź w 2 minuty:
Podstawowy segment dynamiczny:
app/products/[id]/page.tsx → /products/123, /products/abc
// app/products/[id]/page.tsx
export default function ProductPage({
params
}: {
params: { id: string }
}) {
return <div>Product ID: {params.id}</div>;
}
Catch-all segments [...slug]:
app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
// app/docs/[...slug]/page.tsx
export default function DocsPage({
params
}: {
params: { slug: string[] }
}) {
// /docs/getting-started/installation
// params.slug = ['getting-started', 'installation']
return <div>Path: {params.slug.join('/')}</div>;
}
Optional catch-all [[...slug]]:
app/shop/[[...slug]]/page.tsx → /shop, /shop/clothes, /shop/clothes/shirts
// app/shop/[[...slug]]/page.tsx
export default function ShopPage({
params
}: {
params: { slug?: string[] }
}) {
// /shop → params.slug = undefined
// /shop/clothes → params.slug = ['clothes']
if (!params.slug) {
return <div>All products</div>;
}
return <div>Category: {params.slug.join(' > ')}</div>;
}
Generowanie statycznych ścieżek:
// app/products/[id]/page.tsx
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({
id: product.id.toString(),
}));
}
// Pre-renderuje /products/1, /products/2, etc.
8. Jak zaimplementować zagnieżdżone layouty w App Router?
Odpowiedź w 30 sekund:
Każdy folder może mieć layout.tsx, który owija page.tsx i zagnieżdżone layouty. Layouty nie re-renderują się przy nawigacji między stronami - zachowują stan. Root layout (app/layout.tsx) jest wymagany i zawiera <html> i <body>.
Odpowiedź w 2 minuty:
Struktura zagnieżdżonych layoutów:
app/
├── layout.tsx # Root Layout (html, body)
├── page.tsx # /
│
├── dashboard/
│ ├── layout.tsx # Dashboard Layout (sidebar)
│ ├── page.tsx # /dashboard
│ │
│ ├── settings/
│ │ ├── layout.tsx # Settings Layout (tabs)
│ │ └── page.tsx # /dashboard/settings
│ │
│ └── analytics/
│ └── page.tsx # /dashboard/analytics
Root Layout (wymagany):
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pl">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
Dashboard Layout:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<Sidebar /> {/* Nie re-renderuje przy nawigacji! */}
<div className="flex-1">{children}</div>
</div>
);
}
Settings Layout z tabami:
// app/dashboard/settings/layout.tsx
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav>
<Link href="/dashboard/settings">General</Link>
<Link href="/dashboard/settings/security">Security</Link>
</nav>
{children}
</div>
);
}
Wynik dla /dashboard/settings:
RootLayout
└── DashboardLayout (sidebar)
└── SettingsLayout (tabs)
└── SettingsPage
9. Jakie jest zastosowanie plików loading.tsx i error.tsx?
Odpowiedź w 30 sekund:
loading.tsx automatycznie owija page.tsx w React Suspense boundary - pokazuje się podczas ładowania async komponentów. error.tsx to Error Boundary - łapie błędy w segmencie i pozwala na recovery bez crashowania całej aplikacji.
Odpowiedź w 2 minuty:
loading.tsx - automatyczny Suspense:
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
);
}
To jest równoważne:
<Suspense fallback={<Loading />}>
<DashboardPage />
</Suspense>
error.tsx - Error Boundary:
// app/dashboard/error.tsx
'use client'; // Error boundaries muszą być Client Components!
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="p-4 bg-red-50 rounded">
<h2>Coś poszło nie tak!</h2>
<p>{error.message}</p>
<button
onClick={() => reset()}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded"
>
Spróbuj ponownie
</button>
</div>
);
}
Hierarchia błędów:
app/
├── error.tsx # Łapie błędy z całej aplikacji
├── dashboard/
│ ├── error.tsx # Łapie błędy tylko z /dashboard/*
│ └── page.tsx # Jeśli rzuci błąd → dashboard/error.tsx
global-error.tsx dla Root Layout:
// app/global-error.tsx
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Krytyczny błąd!</h2>
<button onClick={() => reset()}>Odśwież</button>
</body>
</html>
);
}
10. Czym są grupy tras (route groups) i do czego służą?
Odpowiedź w 30 sekund:
Route groups to foldery w nawiasach (nazwa), które organizują pliki bez wpływu na URL. Służą do: współdzielenia layoutów między niezwiązanymi stronami, organizacji kodu, tworzenia wielu root layoutów dla różnych sekcji aplikacji.
Odpowiedź w 2 minuty:
Organizacja bez wpływu na URL:
app/
├── (marketing)/ # Nie wpływa na URL
│ ├── layout.tsx # Layout dla marketing pages
│ ├── page.tsx # / (strona główna)
│ ├── about/page.tsx # /about
│ └── pricing/page.tsx # /pricing
│
├── (shop)/ # Osobny layout
│ ├── layout.tsx # Layout dla shop (koszyk, etc.)
│ ├── products/page.tsx # /products
│ └── cart/page.tsx # /cart
│
├── (auth)/ # Layout bez nawigacji
│ ├── layout.tsx # Minimal layout
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
Różne layouty dla różnych sekcji:
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
return (
<div>
<MarketingHeader /> {/* Z CTA "Sign Up" */}
{children}
<MarketingFooter />
</div>
);
}
// app/(shop)/layout.tsx
export default function ShopLayout({ children }) {
return (
<div>
<ShopHeader /> {/* Z koszykiem */}
{children}
<CartSidebar />
</div>
);
}
Wiele root layoutów:
app/
├── (app)/
│ ├── layout.tsx # Layout dla zalogowanych
│ └── dashboard/
│ └── page.tsx
│
├── (public)/
│ ├── layout.tsx # Layout publiczny
│ └── page.tsx
11. Jak zaimplementować middleware w Next.js i jakie ma zastosowania?
Odpowiedź w 30 sekund:
Middleware w Next.js to funkcja w middleware.ts w root projektu, wykonywana przed każdym requestem. Zastosowania: autoryzacja, redirecty, rewrite URLs, A/B testing, geolokacja, rate limiting. Działa na Edge Runtime - bardzo szybko.
Odpowiedź w 2 minuty:
Podstawowy middleware:
// middleware.ts (w root projektu)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Logika przed każdym requestem
console.log('Path:', request.nextUrl.pathname);
return NextResponse.next();
}
// Opcjonalnie: określ które ścieżki
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Autoryzacja:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
// Chronione ścieżki
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Zalogowani nie widzą /login
if (request.nextUrl.pathname === '/login' && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
Geolokacja i A/B testing:
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
// Redirect na lokalną wersję
if (country === 'PL' && !request.nextUrl.pathname.startsWith('/pl')) {
return NextResponse.redirect(new URL(`/pl${request.nextUrl.pathname}`, request.url));
}
// A/B testing
const bucket = request.cookies.get('ab-bucket')?.value ||
(Math.random() > 0.5 ? 'a' : 'b');
const response = NextResponse.next();
response.cookies.set('ab-bucket', bucket);
return response;
}
Matcher patterns:
export const config = {
matcher: [
// Match all paths except static files
'/((?!_next/static|_next/image|favicon.ico).*)',
// Or specific paths
'/dashboard/:path*',
'/api/:path*',
],
};
12. Czym są parallel routes i intercepting routes?
Odpowiedź w 30 sekund:
Parallel routes (@folder) renderują wiele stron jednocześnie w tym samym layoucie - np. dashboard z wieloma panelami. Intercepting routes ((.), (..)) przechwytują nawigację i pokazują inny content - np. modal ze zdjęciem zamiast pełnej strony.
Odpowiedź w 2 minuty:
Parallel Routes (@nazwa):
app/
├── layout.tsx
├── @dashboard/
│ └── page.tsx # Slot "dashboard"
├── @analytics/
│ └── page.tsx # Slot "analytics"
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
dashboard,
analytics,
}: {
children: React.ReactNode;
dashboard: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2">
<div>{dashboard}</div>
<div>{analytics}</div>
</div>
);
}
Intercepting Routes - Modal pattern:
app/
├── @modal/
│ └── (.)photos/[id]/
│ └── page.tsx # Intercepted route (modal)
├── photos/
│ └── [id]/
│ └── page.tsx # Prawdziwa strona (full page)
├── layout.tsx
└── page.tsx
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal} {/* Modal wyświetla się nad contentem */}
</>
);
}
// app/@modal/(.)photos/[id]/page.tsx
export default function PhotoModal({ params }) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-4 rounded">
<Image src={`/photos/${params.id}.jpg`} />
</div>
</div>
);
}
Intercepting patterns:
-
(.)- ten sam poziom -
(..)- jeden poziom wyżej -
(..)(..)- dwa poziomy wyżej -
(...)- od root
Renderowanie: SSR, SSG, ISR
13. Jakie strategie renderowania oferuje Next.js (SSR, SSG, ISR)?
Odpowiedź w 30 sekund:
SSG (Static Site Generation) - generuje HTML w build time. SSR (Server-Side Rendering) - generuje HTML na każdy request. ISR (Incremental Static Regeneration) - statyczny z rewalidacją w tle. W App Router kontrolujesz to przez opcje fetch i export const.
Odpowiedź w 2 minuty:
Static Site Generation (SSG):
// Domyślne zachowanie dla stron bez dynamicznych danych
// app/about/page.tsx
export default function AboutPage() {
return <div>O nas</div>;
}
// Generowane raz w build time
Server-Side Rendering (SSR):
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // Wymusza SSR
export default async function DashboardPage() {
const data = await fetchDashboardData();
return <Dashboard data={data} />;
}
// Świeże dane na każdy request
Incremental Static Regeneration (ISR):
// app/products/page.tsx
export const revalidate = 60; // Rewalidacja co 60 sekund
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // Lub per-fetch
});
return <ProductList products={products} />;
}
// Statyczne, ale odświeża się w tle
Porównanie:
| Strategia | Build Time | Request Time | Kiedy używać |
|---|---|---|---|
| SSG | HTML generowany | Serwowany z cache | Blog, dokumentacja |
| SSR | - | HTML generowany | Personalizacja, real-time |
| ISR | HTML generowany | Cache + rewalidacja | E-commerce, news |
Kontrola w App Router:
// Wymusz SSR
export const dynamic = 'force-dynamic';
// Wymusz SSG
export const dynamic = 'force-static';
// Rewalidacja czasowa
export const revalidate = 3600; // 1 godzina
// Brak cache
export const revalidate = 0;
14. Czym jest Incremental Static Regeneration (ISR) i jak go skonfigurować?
Odpowiedź w 30 sekund:
ISR pozwala regenerować statyczne strony bez pełnego rebuildu. Ustawiasz revalidate w sekundach - strona jest statyczna, ale po upływie czasu Next.js regeneruje ją w tle przy następnym requeście. Łączy szybkość SSG z aktualnością danych.
Odpowiedź w 2 minuty:
Jak działa ISR:
- Pierwszy request → serwuje statyczny HTML z build time
- Po upływie
revalidate→ następny request triggeruje regenerację w tle - Stary HTML serwowany dopóki nowy nie będzie gotowy
- Nowy HTML zastępuje stary w cache
Konfiguracja na poziomie strony:
// app/products/page.tsx
export const revalidate = 60; // Rewaliduj co 60 sekund
export default async function ProductsPage() {
const products = await getProducts();
return <ProductList products={products} />;
}
Konfiguracja per-fetch:
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
// Ten fetch rewaliduje co 60 sekund
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 },
});
// Ten fetch jest świeży na każdy request
const stock = await fetch(`https://api.example.com/stock/${params.id}`, {
cache: 'no-store',
});
return <Product product={product} stock={stock} />;
}
On-demand revalidation (rewalidacja na żądanie):
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid secret' }, { status: 401 });
}
if (path) {
revalidatePath(path); // Rewaliduj konkretną ścieżkę
}
if (tag) {
revalidateTag(tag); // Rewaliduj wszystkie z tym tagiem
}
return Response.json({ revalidated: true });
}
Tagowanie dla grupowej rewalidacji:
// Fetch z tagiem
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] },
});
// Rewaliduj wszystkie z tagiem 'products'
revalidateTag('products');
15. Jak działa streaming i Suspense w Next.js?
Odpowiedź w 30 sekund:
Streaming pozwala wysyłać HTML w kawałkach zamiast czekać na całą stronę. Next.js używa React Suspense - owija wolne komponenty w <Suspense> z fallbackiem. Serwer streamuje gotowe części, użytkownik widzi content wcześniej.
Odpowiedź w 2 minuty:
Tradycyjny SSR vs Streaming:
Tradycyjny SSR:
1. Fetch ALL data
2. Render ALL HTML
3. Send ALL HTML ← Użytkownik czeka
4. Load ALL JS
5. Hydrate ALL
Streaming:
1. Send shell immediately ← Użytkownik widzi layout
2. Stream components as ready
3. Hydrate incrementally
Implementacja z Suspense:
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Natychmiast widoczne */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats /> {/* Streamowane gdy gotowe */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<SlowChart /> {/* Niezależnie streamowane */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<SlowDataTable /> {/* Niezależnie streamowane */}
</Suspense>
</div>
);
}
// Wolny komponent - async Server Component
async function SlowStats() {
const stats = await fetch('https://api.example.com/stats'); // 3 sekundy
return <StatsDisplay stats={stats} />;
}
loading.tsx jako automatyczny Suspense:
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
// Automatycznie owija page.tsx w Suspense
// Równoważne:
<Suspense fallback={<Loading />}>
<DashboardPage />
</Suspense>
Nested Suspense dla granularnej kontroli:
export default async function ProductPage({ params }) {
const product = await getProduct(params.id); // Szybkie
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Recenzje mogą być wolne - nie blokują produktu */}
<Suspense fallback={<p>Ładowanie recenzji...</p>}>
<Reviews productId={params.id} />
</Suspense>
{/* Rekomendacje jeszcze wolniejsze */}
<Suspense fallback={<p>Ładowanie rekomendacji...</p>}>
<Recommendations productId={params.id} />
</Suspense>
</div>
);
}
16. Kiedy wybrać Static Generation a kiedy Server-side Rendering?
Odpowiedź w 30 sekund:
SSG gdy dane nie zmieniają się między requestami (blog, docs, marketing). SSR gdy potrzebujesz danych w czasie rzeczywistym lub personalizacji (dashboard, koszyk). ISR to kompromis - statyczne z okresową aktualizacją (e-commerce, news).
Odpowiedź w 2 minuty:
Decision tree:
Czy dane są takie same dla wszystkich użytkowników?
│
├── TAK → Czy często się zmieniają?
│ │
│ ├── NIE → SSG (blog, docs, landing pages)
│ │
│ └── TAK → Jak często?
│ │
│ ├── Co kilka minut/godzin → ISR
│ │
│ └── Real-time → SSR
│
└── NIE → SSR (personalizacja, auth-dependent content)
Przykłady zastosowań:
| Typ strony | Strategia | Powód |
|---|---|---|
| Blog post | SSG | Content nie zmienia się |
| Dokumentacja | SSG | Zmienia się tylko przy deploy |
| Strona produktu | ISR (60s) | Ceny/stock mogą się zmieniać |
| Lista produktów | ISR (300s) | Nowe produkty co jakiś czas |
| Dashboard użytkownika | SSR | Personalizowane dane |
| Koszyk zakupowy | SSR | Dane sesji |
| Feed social media | SSR | Real-time |
| Strona główna e-commerce | ISR (60s) | Promocje, ale nie real-time |
Implementacja hybrydowa:
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
// Statyczne dane produktu - ISR
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 3600 }, // 1 godzina
});
// Dynamiczny stan magazynowy - zawsze świeży
const stock = await fetch(`/api/stock/${params.id}`, {
cache: 'no-store',
});
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<StockBadge count={stock.available} /> {/* Real-time */}
</div>
);
}
17. Czym jest Partial Prerendering (PPR) w Next.js 14+?
Odpowiedź w 30 sekund:
PPR łączy statyczny shell strony z dynamicznymi "dziurami". Statyczny HTML jest serwowany natychmiast z edge cache, dynamiczne części (w Suspense) są streamowane. Nie musisz wybierać między SSG a SSR - masz oba w jednej stronie.
Odpowiedź w 2 minuty:
Tradycyjne podejście:
Strona jest ALBO statyczna ALBO dynamiczna
└── Jeśli cokolwiek jest dynamiczne → cała strona SSR
PPR (Partial Prerendering):
Statyczny shell (instant z edge)
├── Header ← statyczny
├── Hero ← statyczny
├── [Suspense: PersonalizedContent] ← dynamiczny, streamowany
├── Products ← statyczny
├── [Suspense: CartWidget] ← dynamiczny, streamowany
└── Footer ← statyczny
Włączenie PPR (experimental w Next.js 14):
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
Przykład implementacji:
// app/page.tsx
import { Suspense } from 'react';
export default function HomePage() {
return (
<div>
{/* Statyczny shell - prerendered */}
<Header />
<Hero />
{/* Dynamiczny slot - streamowany */}
<Suspense fallback={<WelcomeSkeleton />}>
<PersonalizedWelcome /> {/* Wymaga auth/cookies */}
</Suspense>
{/* Statyczny content */}
<FeaturedProducts />
{/* Dynamiczny slot */}
<Suspense fallback={<CartSkeleton />}>
<CartPreview /> {/* Dane użytkownika */}
</Suspense>
<Footer />
</div>
);
}
// Ten komponent triggeruje dynamiczne renderowanie
async function PersonalizedWelcome() {
const user = await getUser(); // Wymaga cookies
return <h2>Witaj, {user.name}!</h2>;
}
Korzyści PPR:
- Instant TTFB - statyczny shell z edge cache
- Personalizacja - dynamiczne części bez blokowania
- SEO - statyczny content jest od razu widoczny
- DX - nie musisz wybierać SSG vs SSR per-page
Pobieranie Danych
18. Jak działa fetch w Server Components i czym różni się od fetch po stronie klienta?
Odpowiedź w 30 sekund:
W Server Components fetch jest rozszerzony o opcje cache Next.js. Domyślnie cachuje wyniki (jak SSG), możesz kontrolować przez cache: 'no-store' lub next: { revalidate }. Po stronie klienta fetch działa standardowo - bez automatycznego cache Next.js.
Odpowiedź w 2 minuty:
Server Component fetch (rozszerzony):
// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
// Domyślnie: cache: 'force-cache' (statyczny)
const products = await fetch('https://api.example.com/products');
// Wyłączenie cache (SSR)
const stock = await fetch('https://api.example.com/stock', {
cache: 'no-store',
});
// ISR - rewalidacja co 60s
const featured = await fetch('https://api.example.com/featured', {
next: { revalidate: 60 },
});
// Z tagiem do on-demand revalidation
const categories = await fetch('https://api.example.com/categories', {
next: { tags: ['categories'] },
});
return <ProductList products={products} />;
}
Client Component fetch (standardowy):
'use client';
import { useState, useEffect } from 'react';
export function ClientProducts() {
const [products, setProducts] = useState([]);
useEffect(() => {
// Standardowy fetch - bez Next.js cache
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return <ProductList products={products} />;
}
Deduplikacja requestów:
// Next.js automatycznie deduplikuje te same requesty w jednym renderze
async function ProductPage({ params }) {
const product = await fetch(`/api/products/${params.id}`);
return (
<div>
<ProductDetails product={product} />
<RelatedProducts productId={params.id} />
</div>
);
}
async function RelatedProducts({ productId }) {
// Ten sam URL - Next.js nie wykona drugiego requestu!
const product = await fetch(`/api/products/${productId}`);
return <Related categories={product.categories} />;
}
Różnice:
| Cecha | Server Component | Client Component |
|---|---|---|
| Cache | Automatyczny (Next.js) | Brak (lub React Query) |
| Deduplikacja | Automatyczna | Ręczna |
| Wykonanie | Build/request time | Runtime (przeglądarka) |
| Secrets | Bezpieczne | Widoczne! |
19. Jak skonfigurować cache i rewalidację danych w Next.js?
Odpowiedź w 30 sekund:
Cache kontrolujesz przez: cache: 'force-cache' (domyślne, statyczne), cache: 'no-store' (dynamiczne), next: { revalidate: seconds } (ISR). Rewalidacja on-demand przez revalidatePath() lub revalidateTag(). Możesz też ustawić domyślne zachowanie per-segment.
Odpowiedź w 2 minuty:
Opcje cache w fetch:
// Statyczne (domyślne)
fetch(url, { cache: 'force-cache' });
// Dynamiczne - zawsze świeże
fetch(url, { cache: 'no-store' });
// ISR - rewalidacja czasowa
fetch(url, { next: { revalidate: 60 } });
// Z tagiem
fetch(url, { next: { tags: ['products', 'featured'] } });
Konfiguracja per-segment:
// app/products/page.tsx
// Cała strona dynamiczna
export const dynamic = 'force-dynamic';
// Cała strona statyczna
export const dynamic = 'force-static';
// ISR dla całej strony
export const revalidate = 60;
// Brak cache
export const revalidate = 0;
On-demand revalidation w Server Action:
// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(id: string, data: FormData) {
await db.product.update({ where: { id }, data });
// Rewaliduj konkretną ścieżkę
revalidatePath(`/products/${id}`);
// Rewaliduj listing
revalidatePath('/products');
// Lub wszystkie z tagiem
revalidateTag('products');
}
Revalidation via API route:
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const tag = request.nextUrl.searchParams.get('tag');
if (tag) {
revalidateTag(tag);
return Response.json({ revalidated: true, tag });
}
return Response.json({ error: 'Missing tag' }, { status: 400 });
}
// Webhook od CMS:
// POST /api/revalidate?tag=products
Cache layers w Next.js:
- Request Memoization - deduplikacja w jednym renderze
- Data Cache - persystentny cache fetch
- Full Route Cache - cache całych stron
- Router Cache - client-side cache nawigacji
20. Czym jest funkcja generateStaticParams i kiedy jej używać?
Odpowiedź w 30 sekund:
generateStaticParams generuje listę parametrów dla dynamicznych segmentów ([id], [slug]) w build time. Next.js pre-renderuje stronę dla każdego zestawu parametrów. Używaj dla SSG dynamicznych stron - blogi, produkty, dokumentacja.
Odpowiedź w 2 minuty:
Podstawowe użycie:
// app/blog/[slug]/page.tsx
// Generuje statyczne ścieżki w build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Wygeneruje: /blog/hello-world, /blog/next-js-guide, etc.
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
Zagnieżdżone parametry:
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({
category: product.category,
id: product.id,
}));
}
// Wygeneruje: /products/electronics/123, /products/books/456
Dynamiczne fallback:
// app/products/[id]/page.tsx
export async function generateStaticParams() {
// Generuj tylko top 10 produktów w build time
const topProducts = await getTopProducts(10);
return topProducts.map((p) => ({ id: p.id }));
}
// dynamicParams kontroluje co się dzieje z nieznanymi ID
export const dynamicParams = true; // (domyślne) generuj on-demand
// export const dynamicParams = false; // 404 dla nieznanych
Z parent params:
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams({
params: { locale },
}: {
params: { locale: string };
}) {
// Dostęp do parent params
const posts = await getPostsByLocale(locale);
return posts.map((post) => ({
slug: post.slug,
}));
}
Kiedy używać:
- Blog posts z znaną listą
- Strony produktów (top N w build, reszta on-demand)
- Dokumentacja (wszystkie strony)
- Multi-language sites
21. Jak obsługiwać równoległe zapytania o dane w Server Components?
Odpowiedź w 30 sekund:
Używaj Promise.all() dla niezależnych zapytań - wykonują się równolegle zamiast sekwencyjnie. Alternatywnie, przenieś fetch do osobnych komponentów i owij w Suspense - każdy streamuje niezależnie. Next.js automatycznie deduplikuje te same URL-e.
Odpowiedź w 2 minuty:
Problem: sekwencyjne zapytania (waterfall):
// ❌ Źle - każdy fetch czeka na poprzedni
export default async function Dashboard() {
const user = await getUser(); // 200ms
const posts = await getPosts(); // 300ms
const comments = await getComments(); // 200ms
// Łącznie: 700ms
return <DashboardUI user={user} posts={posts} comments={comments} />;
}
Rozwiązanie 1: Promise.all()
// ✅ Dobrze - zapytania równolegle
export default async function Dashboard() {
const [user, posts, comments] = await Promise.all([
getUser(), // 200ms
getPosts(), // 300ms ← wszystkie startują równocześnie
getComments(), // 200ms
]);
// Łącznie: 300ms (najdłuższy)
return <DashboardUI user={user} posts={posts} comments={comments} />;
}
Rozwiązanie 2: Parallel fetching z Suspense (streaming):
// ✅ Najlepsze - streaming, każdy komponent niezależny
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserInfo /> {/* Fetch wewnątrz */}
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsList /> {/* Fetch wewnątrz */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<CommentsSection /> {/* Fetch wewnątrz */}
</Suspense>
</div>
);
}
async function UserInfo() {
const user = await getUser(); // Streamuje gdy gotowe
return <UserCard user={user} />;
}
Promise.allSettled() dla fault tolerance:
export default async function Dashboard() {
const results = await Promise.allSettled([
getUser(),
getPosts(),
getAnalytics(), // Może failować
]);
const user = results[0].status === 'fulfilled' ? results[0].value : null;
const posts = results[1].status === 'fulfilled' ? results[1].value : [];
const analytics = results[2].status === 'fulfilled' ? results[2].value : null;
return (
<div>
{user && <UserCard user={user} />}
<PostsList posts={posts} />
{analytics && <Analytics data={analytics} />}
</div>
);
}
22. Czym są Server Actions i jak ich używać do mutacji danych?
Odpowiedź w 30 sekund:
Server Actions to async funkcje z dyrektywą 'use server' wywoływane z klienta, ale wykonywane na serwerze. Służą do mutacji: formularze, CRUD, walidacja. Automatycznie obsługują rewalidację cache i działają bez JavaScript (progressive enhancement).
Odpowiedź w 2 minuty:
Definiowanie Server Action:
// app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Walidacja
if (!title || title.length < 3) {
return { error: 'Tytuł musi mieć min. 3 znaki' };
}
// Zapis do DB
const post = await db.post.create({
data: { title, content },
});
// Rewalidacja cache
revalidatePath('/posts');
return { success: true, post };
}
Użycie w formularzu (bez JS!):
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Tytuł" required />
<textarea name="content" placeholder="Treść" />
<button type="submit">Opublikuj</button>
</form>
);
// Działa nawet z wyłączonym JavaScript!
}
Z obsługą stanu (Client Component):
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '../actions';
export function PostForm() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
{state?.error && <p className="text-red-500">{state.error}</p>}
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Zapisywanie...' : 'Zapisz'}
</button>
);
}
Server Action wywoływana programowo:
'use client';
import { deletePost } from '../actions';
export function DeleteButton({ postId }) {
const handleDelete = async () => {
if (confirm('Na pewno?')) {
await deletePost(postId);
}
};
return <button onClick={handleDelete}>Usuń</button>;
}
Inline Server Action:
// app/posts/[id]/page.tsx
export default function PostPage({ params }) {
async function likePost() {
'use server';
await db.post.update({
where: { id: params.id },
data: { likes: { increment: 1 } },
});
revalidatePath(`/posts/${params.id}`);
}
return (
<form action={likePost}>
<button>❤️ Like</button>
</form>
);
}
Optymalizacja i Wydajność
23. Jak działa komponent next/image i jakie oferuje optymalizacje?
Odpowiedź w 30 sekund:
next/image automatycznie optymalizuje obrazy: lazy loading, responsive srcset, konwersja do WebP/AVIF, cache na edge. Wymaga określenia width/height (lub fill) dla uniknięcia layout shift. Obrazy są optymalizowane on-demand, nie w build time.
Odpowiedź w 2 minuty:
Podstawowe użycie:
import Image from 'next/image';
// Lokalne obrazy (automatyczne wymiary)
import heroImage from '@/public/hero.jpg';
export function Hero() {
return (
<Image
src={heroImage}
alt="Hero image"
placeholder="blur" // Automatyczny blur placeholder
/>
);
}
// Remote images (wymagane wymiary)
export function Avatar({ src }) {
return (
<Image
src={src}
alt="Avatar"
width={100}
height={100}
className="rounded-full"
/>
);
}
Fill mode dla responsive:
export function ProductImage({ src }) {
return (
<div className="relative aspect-square">
<Image
src={src}
alt="Product"
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
);
}
Optymalizacje out-of-the-box:
| Funkcja | Opis |
|---|---|
| Lazy loading | Domyślne, loading="eager" wyłącza |
| Responsive | Automatyczny srcset |
| Format | WebP/AVIF z fallback |
| Size optimization | Na żądanie, cache na edge |
| Blur placeholder | Dla lokalnych placeholder="blur"
|
| Priority |
priority dla LCP images |
Konfiguracja dla external images:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
pathname: '/products/**',
},
],
formats: ['image/avif', 'image/webp'],
},
};
Priority dla LCP:
// Największy obraz above the fold
<Image
src="/hero.jpg"
alt="Hero"
priority // Preload, bez lazy loading
fill
/>
24. Czym jest next/font i jakie daje korzyści wydajnościowe?
Odpowiedź w 30 sekund:
next/font automatycznie hostuje fonty lokalnie (zero external requests), stosuje font-display: swap, eliminuje layout shift przez CSS size-adjust. Wspiera Google Fonts i lokalne fonty. Fonty są załadowane w build time - zero runtime cost.
Odpowiedź w 2 minuty:
Google Fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin', 'latin-ext'],
display: 'swap',
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
});
export default function RootLayout({ children }) {
return (
<html lang="pl" className={`${inter.variable} ${robotoMono.variable}`}>
<body className={inter.className}>{children}</body>
</html>
);
}
CSS variables:
/* globals.css */
:root {
--font-sans: var(--font-inter);
--font-mono: var(--font-roboto-mono);
}
body {
font-family: var(--font-sans);
}
code {
font-family: var(--font-mono);
}
Lokalne fonty:
import localFont from 'next/font/local';
const customFont = localFont({
src: [
{
path: './fonts/CustomFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/CustomFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
variable: '--font-custom',
});
Korzyści:
| Bez next/font | Z next/font |
|---|---|
| Request do Google Fonts | Self-hosted (zero external) |
| FOUT/FOIT | Automatyczny swap + size-adjust |
| Layout shift | Zero CLS |
| Runtime loading | Build time optimization |
| Blocking render | Non-blocking |
Tailwind integration:
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
};
25. Jak zoptymalizować ładowanie skryptów zewnętrznych w Next.js?
Odpowiedź w 30 sekund:
Używaj next/script ze strategiami: beforeInteractive (krytyczne, przed hydracją), afterInteractive (domyślne, po hydracii), lazyOnload (niski priorytet, po load). Dla analytics używaj afterInteractive, dla chat widgets lazyOnload.
Odpowiedź w 2 minuty:
Strategie ładowania:
import Script from 'next/script';
// Przed hydracją - blokuje, użyj oszczędnie
<Script
src="/critical-polyfill.js"
strategy="beforeInteractive"
/>
// Po hydracii - domyślne dla większości
// Po window.onload - niski priorytet
// Web Worker - off main thread
Google Analytics:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
strategy="afterInteractive"
/></Script>
</body>
</html>
);
}
Event handlers:
<Script
src="https://example.com/widget.js"
strategy="lazyOnload"
onLoad={() => {
console.log('Script loaded');
initWidget();
}}
onError={() => {
console.error('Script failed');
}}
/>
Inline scripts:
<Script id="show-banner" strategy="afterInteractive">
{`document.getElementById('banner').style.display = 'block'`}
</Script>
Best practices:
- Analytics:
afterInteractive - Chat widgets:
lazyOnload - Polyfills (jeśli potrzebne):
beforeInteractive - Heavy SDKs:
lazyOnloadlubworker
26. Jakie techniki optymalizacji bundle oferuje Next.js?
Odpowiedź w 30 sekund:
Next.js automatycznie: code splitting per route, tree shaking, minifikacja, dead code elimination. Dynamiczne importy z next/dynamic dla lazy loading komponentów. Bundle analyzer (@next/bundle-analyzer) pokazuje co zajmuje miejsce.
Odpowiedź w 2 minuty:
Automatyczne optymalizacje:
✅ Code splitting per route
✅ Tree shaking nieużywanego kodu
✅ Minifikacja (Terser/SWC)
✅ CSS minification
✅ Automatic polyfills (modern browsers)
Dynamic imports:
import dynamic from 'next/dynamic';
// Lazy load komponentu
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <p>Ładowanie wykresu...</p>,
ssr: false, // Tylko client-side (np. dla bibliotek bez SSR)
});
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<HeavyChart /> {/* Ładowany gdy widoczny */}
</div>
);
}
Named exports:
const Modal = dynamic(() =>
import('@/components/modals').then(mod => mod.ConfirmModal)
);
Bundle analyzer:
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// config
});
// Uruchomienie:
// ANALYZE=true npm run build
Import optimization:
// ❌ Importuje całą bibliotekę
import { format } from 'date-fns';
// ✅ Importuje tylko potrzebną funkcję
import format from 'date-fns/format';
// ✅ Lub z modularizeImports w next.config.js
module.exports = {
modularizeImports: {
'date-fns': {
transform: 'date-fns/{{member}}',
},
'lodash': {
transform: 'lodash/{{member}}',
},
},
};
Server Components = zero client JS:
// Ten komponent NIE trafia do bundle klienta
// app/products/page.tsx (Server Component)
import { marked } from 'marked'; // Duża lib, ale tylko na serwerze
export default async function Page() {
const content = marked(markdown);
return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
27. Jak skonfigurować lazy loading komponentów w Next.js?
Odpowiedź w 30 sekund:
Używaj next/dynamic dla komponentów i React lazy() + Suspense. Dynamic import dzieli bundle - komponent ładuje się gdy potrzebny. Opcja ssr: false dla komponentów client-only (np. wykresy bez SSR support).
Odpowiedź w 2 minuty:
next/dynamic (zalecane):
import dynamic from 'next/dynamic';
// Podstawowy lazy loading
const DynamicComponent = dynamic(() => import('@/components/Heavy'));
// Z loading state
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="animate-pulse h-64 bg-gray-200" />,
});
// Bez SSR (dla bibliotek browser-only)
const MapComponent = dynamic(() => import('@/components/Map'), {
ssr: false,
loading: () => <p>Ładowanie mapy...</p>,
});
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Chart />
<MapComponent />
</div>
);
}
Warunkowy lazy loading:
'use client';
import dynamic from 'next/dynamic';
import { useState } from 'react';
const Modal = dynamic(() => import('@/components/Modal'));
export function Page() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>
Otwórz modal
</button>
{/* Modal ładuje się dopiero gdy showModal = true */}
{showModal && <Modal onClose={() => setShowModal(false)} />}
</div>
);
}
React lazy() z Suspense (alternatywa):
'use client';
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('@/components/Heavy'));
export function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Preloading na hover:
'use client';
import dynamic from 'next/dynamic';
import { useState } from 'react';
const HeavyModal = dynamic(() => import('@/components/HeavyModal'));
// Preload funkcja
const preloadModal = () => {
import('@/components/HeavyModal');
};
export function Button() {
const [show, setShow] = useState(false);
return (
<>
<button
onMouseEnter={preloadModal} // Preload na hover
onClick={() => setShow(true)}
>
Otwórz
</button>
{show && <HeavyModal />}
</>
);
}
Metadata i SEO
28. Jak definiować metadata statyczne i dynamiczne w App Router?
Odpowiedź w 30 sekund:
Eksportuj metadata object dla statycznych wartości lub generateMetadata() async function dla dynamicznych. Metadata jest dziedziczone i mergowane z parent layoutów. App Router automatycznie generuje <head> z title, description, Open Graph, Twitter cards.
Odpowiedź w 2 minuty:
Statyczne metadata:
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
template: '%s | Moja Aplikacja',
default: 'Moja Aplikacja',
},
description: 'Opis aplikacji',
keywords: ['next.js', 'react', 'typescript'],
authors: [{ name: 'Jan Kowalski' }],
openGraph: {
type: 'website',
locale: 'pl_PL',
siteName: 'Moja Aplikacja',
},
twitter: {
card: 'summary_large_image',
creator: '@jankowalski',
},
};
Dynamiczne metadata:
// app/products/[id]/page.tsx
import type { Metadata } from 'next';
type Props = {
params: { id: string };
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
};
}
export default async function ProductPage({ params }: Props) {
const product = await getProduct(params.id);
return <ProductDetails product={product} />;
}
Dziedziczenie i merge:
// app/layout.tsx
export const metadata = {
title: {
template: '%s | Shop',
default: 'Shop',
},
};
// app/products/page.tsx
export const metadata = {
title: 'Produkty', // Wynik: "Produkty | Shop"
};
// app/products/[id]/page.tsx
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product.name, // Wynik: "iPhone 15 | Shop"
};
}
Pełny przykład:
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
title: 'Tytuł strony',
description: 'Opis strony',
robots: {
index: true,
follow: true,
},
openGraph: {
title: 'OG Title',
description: 'OG Description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'Twitter Title',
images: ['/twitter-image.jpg'],
},
alternates: {
canonical: '/products',
languages: {
'en': '/en/products',
'pl': '/pl/products',
},
},
};
29. Czym jest plik opengraph-image i jak go używać?
Odpowiedź w 30 sekund:
opengraph-image.tsx to specjalny plik w App Router generujący OG images dynamicznie za pomocą JSX. Next.js używa @vercel/og (Satori) do renderowania React do obrazu. Możesz też użyć statycznego pliku opengraph-image.png.
Odpowiedź w 2 minuty:
Statyczny OG image:
app/
├── opengraph-image.png # /opengraph-image
├── twitter-image.png # /twitter-image
├── products/
│ └── [id]/
│ └── opengraph-image.png # Per-product
Dynamiczny OG image (JSX):
// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
export default function OGImage() {
return new ImageResponse(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1a1a2e',
color: 'white',
}}
>
<h1 style={{ fontSize: 64 }}>Moja Aplikacja</h1>
<p style={{ fontSize: 32 }}>Najlepszy produkt na rynku</p>
</div>
),
{ ...size }
);
}
Dynamiczny per-product:
// app/products/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OGImage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return new ImageResponse(
(
<div
style={{
display: 'flex',
height: '100%',
width: '100%',
backgroundColor: 'white',
padding: 48,
}}
>
<img
src={product.image}
width={400}
height={400}
style={{ objectFit: 'cover' }}
/>
<div style={{ marginLeft: 48 }}>
<h1 style={{ fontSize: 48 }}>{product.name}</h1>
<p style={{ fontSize: 32, color: 'green' }}>{product.price} zł</p>
</div>
</div>
),
{ ...size }
);
}
Z custom fontem:
export default async function OGImage() {
const font = await fetch(
new URL('./Inter-Bold.ttf', import.meta.url)
).then((res) => res.arrayBuffer());
return new ImageResponse(
(<div style={{ fontFamily: 'Inter' }}>Hello</div>),
{
...size,
fonts: [{ name: 'Inter', data: font, weight: 700 }],
}
);
}
30. Jak wygenerować sitemap.xml i robots.txt w Next.js?
Odpowiedź w 30 sekund:
Utwórz sitemap.ts eksportujący async function zwracającą tablicę URL-i. Dla robots utwórz robots.ts eksportujący obiekt z rules i sitemap URL. Oba pliki w /app generują dynamicznie /sitemap.xml i /robots.txt.
Odpowiedź w 2 minuty:
Sitemap statyczny:
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
];
}
Sitemap dynamiczny (z DB):
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const products = await getProducts();
const posts = await getPosts();
const productUrls = products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
const postUrls = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
return [
{
url: 'https://example.com',
lastModified: new Date(),
priority: 1,
},
...productUrls,
...postUrls,
];
}
Multiple sitemaps (dla dużych stron):
// app/sitemap.ts
export async function generateSitemaps() {
const productCount = await getProductCount();
const numSitemaps = Math.ceil(productCount / 50000);
return Array.from({ length: numSitemaps }, (_, i) => ({ id: i }));
}
export default async function sitemap({ id }: { id: number }) {
const products = await getProductsByPage(id, 50000);
// ...
}
// Generuje: /sitemap/0.xml, /sitemap/1.xml, etc.
Robots.txt:
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/private/'],
},
{
userAgent: 'Googlebot',
allow: '/',
},
],
sitemap: 'https://example.com/sitemap.xml',
};
}
31. Jak zaimplementować canonical URLs i hreflang?
Odpowiedź w 30 sekund:
Canonical URL w metadata.alternates.canonical. Hreflang dla multi-language w metadata.alternates.languages. Next.js automatycznie generuje odpowiednie <link> tagi. Dynamicznie ustawiasz w generateMetadata().
Odpowiedź w 2 minuty:
Canonical URL:
// app/products/[slug]/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
alternates: {
canonical: '/products/iphone-15', // Relatywne do metadataBase
},
};
// Lub dynamicznie:
export async function generateMetadata({ params }): Promise<Metadata> {
return {
alternates: {
canonical: `/products/${params.slug}`,
},
};
}
Hreflang dla multi-language:
// app/[locale]/products/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale, slug } = params;
return {
alternates: {
canonical: `/${locale}/products/${slug}`,
languages: {
'en': `/en/products/${slug}`,
'pl': `/pl/products/${slug}`,
'de': `/de/products/${slug}`,
'x-default': `/en/products/${slug}`,
},
},
};
}
Wygenerowany HTML:
<link rel="canonical" href="https://example.com/pl/products/iphone-15" />
<link rel="alternate" hreflang="en" href="https://example.com/en/products/iphone-15" />
<link rel="alternate" hreflang="pl" href="https://example.com/pl/products/iphone-15" />
<link rel="alternate" hreflang="de" href="https://example.com/de/products/iphone-15" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/iphone-15" />
MetadataBase (wymagane dla relative URLs):
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
// Teraz canonical: '/products' → https://example.com/products
};
Z media alternates (RSS, JSON):
export const metadata: Metadata = {
alternates: {
canonical: '/blog',
types: {
'application/rss+xml': '/blog/feed.xml',
'application/json': '/blog/feed.json',
},
},
};
API i Backend
32. Jak tworzyć Route Handlers (API routes) w App Router?
Odpowiedź w 30 sekund:
Utwórz route.ts w folderze /app. Eksportuj funkcje nazwane po metodach HTTP: GET, POST, PUT, DELETE, PATCH. Funkcje otrzymują Request i zwracają Response. Route Handlers zastępują Pages Router /pages/api.
Odpowiedź w 2 minuty:
Podstawowy Route Handler:
// app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const products = await getProducts();
return NextResponse.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
const product = await createProduct(body);
return NextResponse.json(product, { status: 201 });
}
Dynamic route:
// app/api/products/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const product = await getProduct(params.id);
if (!product) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await deleteProduct(params.id);
return new Response(null, { status: 204 });
}
Query params i headers:
// app/api/search/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
// Headers
const authHeader = request.headers.get('authorization');
const results = await search(query, parseInt(page));
return NextResponse.json(results, {
headers: {
'Cache-Control': 'max-age=60',
},
});
}
Cookies:
// app/api/auth/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { token } = await request.json();
cookies().set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 dni
});
return NextResponse.json({ success: true });
}
export async function DELETE() {
cookies().delete('session');
return NextResponse.json({ success: true });
}
33. Czym różnią się Route Handlers od Server Actions?
Odpowiedź w 30 sekund:
Route Handlers to REST endpoints (GET/POST/PUT) wywoływane przez fetch, dostępne publicznie. Server Actions to RPC-style funkcje wywoływane bezpośrednio z komponentów, automatycznie obsługują formularz i rewalidację. Używaj Actions do mutacji, Handlers do REST API.
Odpowiedź w 2 minuty:
Porównanie:
| Cecha | Route Handler | Server Action |
|---|---|---|
| Wywołanie | fetch('/api/...') | Bezpośrednie z komponentu |
| HTTP methods | GET, POST, PUT, DELETE | Tylko POST (wewnętrznie) |
| URL | Publiczny endpoint | Brak publicznego URL |
| Użycie | REST API, webhooks | Formularze, mutacje UI |
| Rewalidacja | Ręczna | Automatyczna integracja |
| Progressive Enhancement | Nie | Tak (działa bez JS) |
Route Handler - REST API:
// app/api/products/route.ts
export async function GET() {
const products = await db.product.findMany();
return Response.json(products);
}
export async function POST(request: Request) {
const data = await request.json();
const product = await db.product.create({ data });
return Response.json(product, { status: 201 });
}
// Wywołanie:
// fetch('/api/products', { method: 'POST', body: JSON.stringify(data) })
Server Action - mutacja z UI:
// app/actions.ts
'use server';
export async function createProduct(formData: FormData) {
const name = formData.get('name');
const product = await db.product.create({ data: { name } });
revalidatePath('/products');
return product;
}
// app/products/new/page.tsx
import { createProduct } from '../actions';
export default function NewProduct() {
return (
<form action={createProduct}>
<input name="name" />
<button type="submit">Utwórz</button>
</form>
);
}
// Działa bez JavaScript!
Kiedy używać czego:
| Scenariusz | Wybór |
|---|---|
| Public REST API | Route Handler |
| Webhook od Stripe | Route Handler |
| Formularz kontaktowy | Server Action |
| Like/Unlike button | Server Action |
| Mobile app backend | Route Handler |
| Admin dashboard CRUD | Server Action |
34. Jak obsługiwać różne metody HTTP w Route Handlers?
Odpowiedź w 30 sekund:
Eksportuj osobne funkcje dla każdej metody: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Każda funkcja otrzymuje Request i opcjonalnie context z params. Nieobsługiwane metody automatycznie zwracają 405 Method Not Allowed.
Odpowiedź w 2 minuty:
Wszystkie metody w jednym pliku:
// app/api/products/[id]/route.ts
import { NextResponse } from 'next/server';
type Context = { params: { id: string } };
// GET /api/products/123
export async function GET(request: Request, { params }: Context) {
const product = await db.product.findUnique({
where: { id: params.id },
});
if (!product) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(product);
}
// PUT /api/products/123 (full update)
export async function PUT(request: Request, { params }: Context) {
const body = await request.json();
const product = await db.product.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(product);
}
// PATCH /api/products/123 (partial update)
export async function PATCH(request: Request, { params }: Context) {
const body = await request.json();
const product = await db.product.update({
where: { id: params.id },
data: body, // Tylko przekazane pola
});
return NextResponse.json(product);
}
// DELETE /api/products/123
export async function DELETE(request: Request, { params }: Context) {
await db.product.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
}
// OPTIONS (CORS preflight)
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
HEAD dla metadata:
// HEAD /api/products/123
export async function HEAD(request: Request, { params }: Context) {
const product = await db.product.findUnique({
where: { id: params.id },
select: { updatedAt: true },
});
return new Response(null, {
headers: {
'Last-Modified': product?.updatedAt.toUTCString() || '',
},
});
}
35. Jak zaimplementować API z walidacją i obsługą błędów?
Odpowiedź w 30 sekund:
Używaj Zod do walidacji body/params. Try-catch dla obsługi błędów. Zwracaj odpowiednie status codes (400 dla validation, 404 dla not found, 500 dla server errors). Stwórz helper do formatowania błędów.
Odpowiedź w 2 minuty:
Walidacja z Zod:
// lib/validations.ts
import { z } from 'zod';
export const createProductSchema = z.object({
name: z.string().min(2, 'Nazwa min. 2 znaki').max(100),
price: z.number().positive('Cena musi być dodatnia'),
description: z.string().optional(),
categoryId: z.string().uuid('Nieprawidłowe ID kategorii'),
});
export type CreateProductInput = z.infer<typeof createProductSchema>;
Route Handler z walidacją:
// app/api/products/route.ts
import { NextResponse } from 'next/server';
import { createProductSchema } from '@/lib/validations';
import { ZodError } from 'zod';
export async function POST(request: Request) {
try {
const body = await request.json();
// Walidacja
const validatedData = createProductSchema.parse(body);
// Tworzenie produktu
const product = await db.product.create({
data: validatedData,
});
return NextResponse.json(product, { status: 201 });
} catch (error) {
// Błąd walidacji Zod
if (error instanceof ZodError) {
return NextResponse.json(
{
error: 'Validation error',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
},
{ status: 400 }
);
}
// Błąd bazy danych
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return NextResponse.json(
{ error: 'Product already exists' },
{ status: 409 }
);
}
}
// Nieoczekiwany błąd
console.error('Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Helper do błędów:
// lib/api-response.ts
export function errorResponse(message: string, status: number) {
return NextResponse.json({ error: message }, { status });
}
export function validationError(errors: Array<{ field: string; message: string }>) {
return NextResponse.json({ error: 'Validation failed', details: errors }, { status: 400 });
}
export function notFound(resource: string) {
return NextResponse.json({ error: `${resource} not found` }, { status: 404 });
}
// Użycie:
if (!product) {
return notFound('Product');
}
Konfiguracja i Deployment
36. Jakie są kluczowe opcje w pliku next.config.js?
Odpowiedź w 30 sekund:
Kluczowe: images.remotePatterns (external images), redirects/rewrites (routing), env (environment variables), experimental (nowe features). Także output: 'standalone' dla Docker, transpilePackages dla ESM, modularizeImports dla tree-shaking.
Odpowiedź w 2 minuty:
Podstawowa konfiguracja:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Obrazy z external domains
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
},
{
protocol: 'https',
hostname: '**.cloudinary.com',
},
],
},
// Redirecty (301)
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
];
},
// Rewrites (proxy, bez zmiany URL)
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://backend.example.com/:path*',
},
];
},
// Environment variables (dostępne w runtime)
env: {
CUSTOM_VAR: 'value',
},
// Headers
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
];
},
};
module.exports = nextConfig;
Zaawansowane opcje:
const nextConfig = {
// Standalone output dla Docker
output: 'standalone',
// Transpilacja ESM packages
transpilePackages: ['some-esm-package'],
// Tree-shaking dla dużych bibliotek
modularizeImports: {
'lodash': {
transform: 'lodash/{{member}}',
},
'@mui/icons-material': {
transform: '@mui/icons-material/{{member}}',
},
},
// Experimental features
experimental: {
ppr: true, // Partial Prerendering
serverActions: {
bodySizeLimit: '2mb',
},
},
// Webpack customization
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = { fs: false };
}
return config;
},
};
37. Jak skonfigurować zmienne środowiskowe w Next.js?
Odpowiedź w 30 sekund:
.env.local dla local, .env.production dla prod. Zmienne z NEXT_PUBLIC_ są dostępne w przeglądarce. Bez prefixu - tylko server-side. Dostęp przez process.env.VAR_NAME. Waliduj zmienne na starcie aplikacji.
Odpowiedź w 2 minuty:
Pliki env (priorytet):
.env # Domyślne dla wszystkich
.env.local # Lokalne (gitignore!)
.env.development # Dev tylko
.env.production # Prod tylko
.env.test # Testy
Server-side vs Client-side:
# .env.local
# Server-side only (bezpieczne)
DATABASE_URL="postgresql://..."
API_SECRET_KEY="secret123"
STRIPE_SECRET_KEY="sk_..."
# Client-side (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_..."
Użycie:
// Server Component / API Route
const dbUrl = process.env.DATABASE_URL;
const secret = process.env.API_SECRET_KEY;
// Client Component
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// process.env.DATABASE_URL → undefined w kliencie!
Walidacja z Zod:
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET_KEY: z.string().min(32),
NEXT_PUBLIC_API_URL: z.string().url(),
});
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET_KEY: process.env.API_SECRET_KEY,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
});
// Użycie:
import { env } from '@/lib/env';
const url = env.DATABASE_URL; // Typed!
Runtime env (dla Docker):
// next.config.js
module.exports = {
// Dostępne w runtime, nie w build time
serverRuntimeConfig: {
apiSecret: process.env.API_SECRET,
},
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL,
},
};
// Użycie:
import getConfig from 'next/config';
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
38. Czym różni się deployment na Vercel od self-hosted?
Odpowiedź w 30 sekund:
Vercel: zero-config, automatyczny edge/serverless, preview deployments, analytics. Self-hosted: next build && next start, wymaga Node.js server, brak edge functions (lub dodatkowa konfiguracja). Docker z output: 'standalone' dla self-hosted.
Odpowiedź w 2 minuty:
Vercel (managed):
✅ Zero configuration
✅ Automatic edge functions
✅ Preview deployments (per PR)
✅ Automatic HTTPS, CDN
✅ ISR działa out-of-the-box
✅ Analytics, Speed Insights
✅ Image Optimization included
❌ Vendor lock-in
❌ Koszty przy dużym ruchu
Self-hosted (Node.js):
# Build
npm run build
# Start (wymaga Node.js server)
npm start
# lub
node .next/standalone/server.js
✅ Pełna kontrola
✅ Własna infrastruktura
✅ Brak vendor lock-in
❌ Musisz zarządzać: skalowanie, SSL, CDN
❌ Image Optimization wymaga konfiguracji
❌ ISR wymaga persystentnego cache
Docker deployment:
# Dockerfile
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
// next.config.js dla Docker
module.exports = {
output: 'standalone', // Minimalny output
};
Porównanie features:
| Feature | Vercel | Self-hosted |
|---|---|---|
| Edge Functions | ✅ Auto | ❌ / Custom |
| ISR | ✅ Auto | Wymaga setup |
| Image Optimization | ✅ | Wymaga loader |
| Preview Deploys | ✅ | CI/CD setup |
| Middleware | ✅ Edge | Node.js |
39. Jak skonfigurować Next.js do pracy z Docker?
Odpowiedź w 30 sekund:
Ustaw output: 'standalone' w next.config.js. Multi-stage Dockerfile: deps → build → runner. Kopiuj .next/standalone i .next/static. Uruchom node server.js. Dla Docker Compose dodaj health check i volumes dla cache.
Odpowiedź w 2 minuty:
next.config.js:
module.exports = {
output: 'standalone',
// Opcjonalnie: wyłącz telemetrię
// experimental: { outputFileTracingRoot: path.join(__dirname, '../') }
};
Dockerfile (optimized):
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Dependencies
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Build
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Standalone output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
docker-compose.yml:
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- NEXT_PUBLIC_API_URL=https://api.example.com
depends_on:
- db
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=app
volumes:
postgres_data:
.dockerignore:
node_modules
.next
.git
*.md
.env*.local
40. Jak monitorować wydajność aplikacji Next.js w produkcji?
Odpowiedź w 30 sekund:
Vercel Analytics i Speed Insights dla managed. Self-hosted: Web Vitals API z useReportWebVitals, custom analytics (Sentry, DataDog). Kluczowe metryki: LCP, FID/INP, CLS, TTFB. Next.js automatycznie raportuje Server Timing headers.
Odpowiedź w 2 minuty:
Vercel Analytics (managed):
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
Web Vitals API (self-hosted):
// app/web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
// Wyślij do swojego analytics
console.log(metric);
// Lub do Google Analytics
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
// Lub do custom endpoint
fetch('/api/analytics', {
method: 'POST',
body: JSON.stringify(metric),
});
});
return null;
}
Sentry dla error tracking:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
Custom performance logging:
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const start = Date.now();
const response = NextResponse.next();
response.headers.set('Server-Timing', `middleware;dur=${Date.now() - start}`);
return response;
}
Kluczowe metryki do monitorowania:
| Metryka | Cel | Co oznacza |
|---|---|---|
| LCP | < 2.5s | Largest Contentful Paint |
| FID/INP | < 100ms | First Input Delay / Interaction to Next Paint |
| CLS | < 0.1 | Cumulative Layout Shift |
| TTFB | < 600ms | Time to First Byte |
Podsumowanie
Next.js w 2026 to znacznie więcej niż "React z SSR". App Router, Server Components, Server Actions i Partial Prerendering zmieniają sposób budowania aplikacji webowych. Na rozmowie rekrutacyjnej musisz znać:
Fundamenty:
- Różnica Server Components vs Client Components
- App Router vs Pages Router
- Strategie renderowania (SSG, SSR, ISR)
Data fetching:
- Fetch w Server Components z cache
- Server Actions do mutacji
- Streaming z Suspense
Optymalizacja:
- next/image i next/font
- Dynamic imports i code splitting
- PPR (Partial Prerendering)
Infrastruktura:
- Route Handlers (API)
- Middleware
- Deployment (Vercel vs self-hosted)
Zobacz też
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik przygotowania do rozmowy frontend
- 15 Najtrudniejszych Pytań Rekrutacyjnych z React - fundament dla Next.js
- React 19 - Nowe Hooki - use(), Actions, Server Components
- TypeScript dla Początkujących - Next.js to TypeScript-first
Chcesz Więcej Pytań z Next.js?
Mamy kompletny zestaw 40 pytań z Next.js - każde z odpowiedzią w formacie "30 sekund / 2 minuty". Idealne do szybkiego przygotowania przed rozmową.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
