Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Edge Runtime i optymalizacja wydajności

W świecie nowoczesnych aplikacji webowych wydajność jest kluczowym czynnikiem wpływającym na doświadczenie użytkownika i sukces biznesowy. Next.js oferuje zaawansowane narzędzia do optymalizacji wydajności, wśród których Edge Runtime wysuwa się na pierwszy plan jako potężne rozwiązanie. W tym module omowimy Edge Runtime oraz inne strategie optymalizacji wydajności w aplikacjach Next.js.

Edge Runtime w Next.js

Czym jest Edge Runtime?

Edge Runtime to lekkie środowisko wykonawcze, które umożliwia uruchamianie kodu bliżej użytkownika końcowego - na "brzegu" sieci - zamiast w centralnym centrum danych. Next.js umożliwia uruchamianie Route Handlerów i stron w Edge Runtime, co zapewnia:

  1. Mniejsze opóźnienia - kod wykonuje się bliżej użytkownika
  2. Szybszy czas pierwszego bajtu (TTFB) - szybsze rozpoczęcie przesyłania odpowiedzi
  3. Globalne skalowanie - automatyczne rozproszenie na całym świecie
  4. Mniejsze zużycie pamięci - dzięki lżejszemu runtime'owi

Różnice między Edge Runtime a Node.js Runtime

Warto rozumieć różnice między Edge Runtime a tradycyjnym środowiskiem Node.js:

| Cecha | Edge Runtime | Node.js Runtime | |-------|--------------|----------------| | Czas zimnego startu | Bardzo szybki (milisekundy) | Wolniejszy (setki milisekund) | | Skalowalność | Automatyczne, globalne | Wymaga konfiguracji | | Dostęp do API | Ograniczony zestaw | Pełne API Node.js | | Rozmiar kodu | Limitowany (zazwyczaj 1-4 MB) | Praktycznie nieograniczony | | Dostęp do systemu plików | Brak | Pełny dostęp | | Czas wykonania funkcji | Ograniczony (np. 50-60 ms) | Dłuższy (minuty) | | Typowe zastosowania | API, strony z dynamicznym contentem | Skomplikowane operacje, dostęp do baz danych |

Konfiguracja Edge Runtime w Next.js

Aby uruchomić komponent strony lub Route Handler w Edge Runtime, używamy eksportu

runtime
w pliku:

1// app/api/edge-route/route.ts
2import { NextRequest } from 'next/server';
3
4export const runtime = 'edge'; // Deklaracja Edge Runtime
5
6export async function GET(request: NextRequest) {
7  const { searchParams } = new URL(request.url);
8  const query = searchParams.get('query') || 'Świat';
9  
10  // Obsługa żądania przy minimalnym opóźnieniu
11  return Response.json({ message: `Witaj, ${query}!` });
12}

Dla strony lub komponentu Layoutu:

1// app/edge-page/page.tsx
2import { cookies } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function EdgePage() {
7  const cookie = (await cookies()).get('theme')?.value || 'light';
8  
9  return (
10    <div>
11      <h1>Strona renderowana w Edge Runtime</h1>
12      <p>Twój motyw: {cookie}</p>
13    </div>
14  );
15}

Kiedy używać Edge Runtime?

Edge Runtime jest idealny dla:

  1. Dynamicznych personalizacji - np. geolokalizacji, preferencji użytkownika
  2. API proxy - przekazywanie żądań do innych serwisów z minimalnym opóźnieniem
  3. A/B testów - szybkie podejmowanie decyzji o wyświetlanej wersji
  4. Prostych dynamicznych stron - które nie wymagają złożonych operacji
  5. Międzynarodowych odbiorców - gdy opóźnienie sieci ma znaczenie

Przykład geolokalizacji w Edge Runtime:

1// app/edge-geo/page.tsx
2import { headers } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function GeoPage() {
7  const headersList = await headers();
8  const countryCode = headersList.get('x-vercel-ip-country') || 'Unknown';
9  const city = headersList.get('x-vercel-ip-city') || 'Unknown City';
10  
11  return (
12    <div>
13      <h1>Witaj, odwiedzający!</h1>
14      <p>Wykryliśmy, że jesteś z: {city}, {countryCode}</p>
15      {countryCode === 'PL' && (
16        <p>Specjalna treść dla odwiedzających z Polski!</p>
17      )}
18    </div>
19  );
20}

Optymalizacja obrazów i mediów

Next.js oferuje wbudowane rozwiązania do optymalizacji mediów, które znacząco wpływają na wydajność aplikacji.

Next.js Image Component

Komponent

Image
w Next.js automatycznie optymalizuje obrazy, co zmniejsza rozmiar przesyłanych danych i poprawia metryki Web Vitals:

1import Image from 'next/image';
2
3export default function OptimizedImages() {
4  return (
5    <div>
6      <h2>Optymalizowane obrazy w Next.js</h2>
7      
8      <Image
9        src="/hero-image.jpg" // Ścieżka względna w katalogu public
10        alt="Hero Image"
11        width={1200}
12        height={600}
13        priority // Priorytet ładowania dla obrazów nad zagięciem
14      />
15      
16      <Image
17        src="https://cdn.example.com/remote-image.jpg" // Zdalny obraz
18        alt="Remote Image"
19        width={800}
20        height={400}
21        quality={80} // Regulacja jakości (1-100)
22        placeholder="blur"
23        blurDataURL="data:image/svg+xml;base64,..." // Opcjonalny placeholder podczas ładowania
24      />
25    </div>
26  );
27}

Kluczowe funkcje komponentu

Image
:

  1. Automatyczne rozmiary responsywne - generowanie różnych rozmiarów obrazu
  2. Lazy loading - obrazy ładowane tylko gdy są widoczne
  3. Konwersja do nowoczesnych formatów - WebP, AVIF
  4. Optymalizacja jakości - redukcja rozmiaru pliku przy zachowaniu jakości
  5. Placeholder podczas ładowania - rozmycie (blur) zapobiegające przesunięciom układu (CLS)

Optymalizacja czcionek

Next.js App Router wprowadza nowy system ładowania czcionek, który eliminuje problemy z migotaniem tekstu (CLS):

1// app/layout.tsx
2import { Inter, Roboto_Mono } from 'next/font/google';
3
4// Konfiguracja czcionki zmiennej (variable font)
5const inter = Inter({
6  subsets: ['latin', 'latin-ext'],
7  display: 'swap',
8});
9
10// Konfiguracja czcionki o stałej grubości
11const robotoMono = Roboto_Mono({
12  subsets: ['latin'],
13  display: 'swap',
14  weight: ['400', '700'],
15});
16
17export default function RootLayout({ children }) {
18  return (
19    <html lang="pl" className={`${inter.className} ${robotoMono.variable}`}>
20      <body>{children}</body>
21    </html>
22  );
23}

W komponencie możemy użyć zdefiniowanej czcionki:

1// app/page.tsx
2export default function Home() {
3  return (
4    <main>
5      <h1 className={inter.className}>Nagłówek w czcionce Inter</h1>
6      <code className="font-mono">// To będzie w Roboto Mono dzięki zmiennej CSS</code>
7    </main>
8  );
9}

Strategie optymalizacji danych

React Server Components i redukcja bundle size

React Server Components (RSC) pozwalają znacząco zmniejszyć ilość JavaScript wysyłanego do przeglądarki:

1// app/products/page.tsx - Server Component (domyślnie)
2async function getProducts() {
3  const res = await fetch('https://api.example.com/products');
4  return res.json();
5}
6
7export default async function ProductsPage() {
8  // Ten kod NIGDY nie jest wysyłany do klienta
9  const products = await getProducts();
10  const totalValue = products.reduce((sum, product) => sum + product.price, 0);
11  
12  return (
13    <div>
14      <h1>Produkty (wartość: {totalValue})</h1>
15      <ProductList products={products} />
16    </div>
17  );
18}

Streaming i Suspense

Streaming umożliwia stopniowe przesyłanie UI, co przyspiesza Time to First Byte i poprawia interaktywność:

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import Loading from './loading';
4
5// Komponenty ładujące dane niezależnie od siebie
6import UserProfile from './UserProfile';
7import RecentOrders from './RecentOrders';
8import Analytics from './Analytics';
9
10export default function Dashboard() {
11  return (
12    <div className="dashboard">
13      <h1>Panel kontrolny</h1>
14      
15      <div className="dashboard-grid">
16        {/* Szybciej ładujące się elementy będą widoczne od razu */}
17        <Suspense fallback={<Loading section="profile" />}>
18          <UserProfile />
19        </Suspense>
20        
21        <Suspense fallback={<Loading section="orders" />}>
22          <RecentOrders />
23        </Suspense>
24        
25        <Suspense fallback={<Loading section="analytics" />}>
26          <Analytics />
27        </Suspense>
28      </div>
29    </div>
30  );
31}

Optymalizacja routingu i nawigacji

Prefetching i Parallel Routes

Next.js App Router automatycznie prefetchuje linki widoczne w widoku, co można dodatkowo kontrolować:

1import Link from 'next/link';
2
3export default function Navigation() {
4  return (
5    <nav>
6      {/* Domyślny prefetch */}
7      <Link href="/products">Produkty</Link>
8      
9      {/* Wyłączony prefetch */}
10      <Link href="/heavy-page" prefetch={false}>Ciężka strona</Link>
11      
12      {/* Prefetch tylko nagłówków (dla dynamicznych stron) */}
13      <Link href="/blog/article-123">Artykuł</Link>
14    </nav>
15  );
16}

Parallel Routes umożliwiają ładowanie wielu sekcji strony niezależnie od siebie:

1// app/layout.tsx
2export default function Layout({ children, sidebar }) {
3  return (
4    <div className="layout">
5      <aside className="sidebar">
6        {sidebar}
7      </aside>
8      <main>
9        {children}
10      </main>
11    </div>
12  );
13}
14
15// app/@sidebar/page.tsx - ładowany niezależnie
16export default function Sidebar() {
17  return <div>Zawartość sidebara</div>;
18}
19
20// app/page.tsx - główna zawartość
21export default function Home() {
22  return <div>Główna zawartość</div>;
23}

Route Handlers i optymalizacja API

Route Handlers mogą być zoptymalizowane pod kątem wydajności, szczególnie w połączeniu z Edge Runtime:

1// app/api/optimized/route.ts
2import { NextResponse } from 'next/server';
3
4export const runtime = 'edge';
5
6export async function GET() {
7  // Stosowanie CORS dla optymalizacji preflightów
8  const response = NextResponse.json({ data: 'wartość' });
9  
10  response.headers.set('Access-Control-Allow-Origin', '*');
11  response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
12  response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
13  
14  // Ustawienie Cache-Control dla browsera
15  response.headers.set(
16    'Cache-Control',
17    'public, max-age=60, s-maxage=3600, stale-while-revalidate=600'
18  );
19  
20  return response;
21}

Zaawansowane techniki optymalizacji

Dobre praktyki dla funkcji generateStaticParams

Funkcja

generateStaticParams
służy do generowania statycznych wersji stron z parametrami:

1// app/products/[category]/[id]/page.tsx
2export async function generateStaticParams() {
3  // Pobierz tylko niezbędne dane dla generowania ścieżek
4  const categories = await fetch('https://api.example.com/categories').then(r => r.json());
5  
6  // Generuj kombinacje parametrów dla najważniejszych stron
7  const paths = [];
8  
9  for (const category of categories) {
10    // Pobierz tylko top produkty dla każdej kategorii
11    const topProducts = await fetch(
12      `https://api.example.com/categories/${category.id}/top-products`
13    ).then(r => r.json());
14    
15    for (const product of topProducts) {
16      paths.push({
17        category: category.slug,
18        id: product.id.toString(),
19      });
20    }
21  }
22  
23  return paths;
24}
25
26export default async function ProductPage({ params }) {
27  const { category, id } = await params;
28  // Strona produktu...
29}

Optymalizacja polega na:

  1. Generowaniu tylko najważniejszych parametrów (np. najpopularniejsze produkty)
  2. Minimalizacji liczby zapytań podczas budowania
  3. Odpowiednim cachowaniu danych używanych do generowania parametrów

Optymalizacja stylów i CSS

Next.js obsługuje różne strategie ładowania CSS, które można optymalizować:

1// app/products/[id]/page.tsx
2import './product-page.css'; // Globalny CSS dla tej trasy
3
4// CSS Modules dla komponentu
5import styles from './ProductDetails.module.css';
6
7// Styled JSX (wbudowana opcja)
8function ProductColorChoice() {
9  return (
10    <div className="color-selector">
11      <style jsx>{`
12        .color-selector {
13          display: flex;
14          gap: 8px;
15        }
16        .color-option {
17          width: 24px;
18          height: 24px;
19          border-radius: 50%;
20          cursor: pointer;
21        }
22      `}</style>
23      <div className="color-option" style={{ backgroundColor: 'red' }} />
24      <div className="color-option" style={{ backgroundColor: 'blue' }} />
25    </div>
26  );
27}
28
29export default async function ProductPage({ params }) {
30  return (
31    <div className={styles.productContainer}>
32      <h1 className={styles.productTitle}>Szczegóły produktu {(await params).id}</h1>
33      <ProductColorChoice />
34    </div>
35  );
36}

Optymalizacja CSS:

  1. CSS Modules dla izolacji stylów komponentów
  2. Automatyczne usuwanie nieużywanego CSS (tree-shaking)
  3. Grupowanie CSS w chunki zgodnie z trasami aplikacji

Wykorzystanie HTTP/2 i HTTP/3

Hosting Next.js na platformach obsługujących nowoczesne protokoły HTTP/2 i HTTP/3 zapewnia lepszą wydajność dzięki:

  1. Multipleksowaniu zapytań - wiele zapytań przez jedno połączenie
  2. Kompresji nagłówków - mniejsze narzuty komunikacji
  3. Server Push - serwer może wysyłać zasoby zanim klient o nie poprosi

Nie wymaga to zmian w kodzie, ale odpowiedniej konfiguracji serwera lub wyboru platformy hostingowej (np. Vercel, Netlify, Cloudflare Pages).

Narzędzia analizy i poprawy wydajności

Next.js Analytics i Web Vitals

Next.js oferuje wbudowane metryki Web Vitals, które możemy śledzić:

1// instrumentation.ts (w katalogu głównym)
2export function register() {
3  if (typeof window !== 'undefined') {
4    // Rozwiązanie działa tylko po stronie klienta
5    import('web-vitals').then(({ onCLS, onFID, onLCP, onTTFB }) => {
6      onCLS(metric => sendAnalyticsEvent('CLS', metric));
7      onFID(metric => sendAnalyticsEvent('FID', metric));
8      onLCP(metric => sendAnalyticsEvent('LCP', metric));
9      onTTFB(metric => sendAnalyticsEvent('TTFB', metric));
10    });
11  }
12}
13
14function sendAnalyticsEvent(name, metric) {
15  const body = JSON.stringify({
16    name,
17    value: metric.value,
18    page: window.location.pathname,
19    id: metric.id,
20  });
21  
22  // Wysyłka do własnego API
23  navigator.sendBeacon('/api/analytics', body);
24}

Audyt i analiza bundle size

Analiza rozmiaru bundla jest kluczowa dla wydajności. Next.js oferuje wbudowaną analizę bundla:

1next build --analyze

Możemy też użyć bardziej zaawansowanych narzędzi jak

@next/bundle-analyzer
:

1// next.config.js
2const withBundleAnalyzer = require('@next/bundle-analyzer')({
3  enabled: process.env.ANALYZE === 'true',
4});
5
6module.exports = withBundleAnalyzer({
7  // Konfiguracja Next.js
8});

Następnie uruchamiamy:

1ANALYZE=true npm run build

Security Best Practices w Next.js

W Metropolii Quantum roku 2150, bezpieczeństwo to nie opcja - to konieczność. Każda luka w systemie może być wykorzystana przez cyberprzestępców czy wrogie korporacje. W tej sekcji omówimy najważniejsze praktyki zabezpieczania aplikacji Next.js, które chronią zarówno dane użytkowników, jak i integralność całego systemu.

CSRF Protection (Cross-Site Request Forgery)

CSRF to atak, w którym złośliwa strona wykorzystuje uwierzytelnioną sesję użytkownika do wykonania nieautoryzowanych akcji. Next.js oferuje wbudowane mechanizmy ochrony, szczególnie w Server Actions.

Implementacja CSRF Protection

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Weryfikacja Origin header dla mutacji
7  if (request.method !== 'GET' && request.method !== 'HEAD') {
8    const origin = request.headers.get('origin');
9    const host = request.headers.get('host');
10
11    // Sprawdź czy Origin odpowiada hostowi
12    if (origin && !origin.includes(host || '')) {
13      return new NextResponse('Forbidden', { status: 403 });
14    }
15  }
16
17  return NextResponse.next();
18}
19
20export const config = {
21  matcher: '/api/:path*'
22};

CSRF Token dla formularzy

1// lib/csrf.ts
2import { cookies } from 'next/headers';
3import crypto from 'crypto';
4
5export async function generateCSRFToken(): Promise<string> {
6  const token = crypto.randomBytes(32).toString('hex');
7  const cookieStore = await cookies();
8
9  cookieStore.set('csrf-token', token, {
10    httpOnly: true,
11    secure: process.env.NODE_ENV === 'production',
12    sameSite: 'strict',
13    maxAge: 60 * 60 // 1 godzina
14  });
15
16  return token;
17}
18
19export async function verifyCSRFToken(token: string): Promise<boolean> {
20  const cookieStore = await cookies();
21  const storedToken = cookieStore.get('csrf-token')?.value;
22
23  return storedToken === token;
24}
25
26// app/actions.ts
27'use server';
28
29import { verifyCSRFToken } from '@/lib/csrf';
30
31export async function submitForm(formData: FormData) {
32  const csrfToken = formData.get('csrf-token') as string;
33
34  if (!await verifyCSRFToken(csrfToken)) {
35    throw new Error('Invalid CSRF token');
36  }
37
38  // Przetwarzanie formularza...
39}
40
41// app/my-form/page.tsx
42import { generateCSRFToken } from '@/lib/csrf';
43import { submitForm } from '@/app/actions';
44
45export default async function MyFormPage() {
46  const csrfToken = await generateCSRFToken();
47
48  return (
49    <form action={submitForm}>
50      <input type="hidden" name="csrf-token" value={csrfToken} />
51      {/* Pozostałe pola formularza */}
52      <button type="submit">Wyślij</button>
53    </form>
54  );
55}

XSS Prevention (Cross-Site Scripting)

XSS to atak, w którym napastnik wstrzykuje złośliwy kod JavaScript do aplikacji. Next.js i React oferują wbudowaną ochronę, ale warto znać dodatkowe techniki.

Domyślna ochrona React

React automatycznie escapuje wszystkie wartości renderowane w JSX:

1// Bezpieczne - React automatycznie escapuje
2function UserProfile({ username }: { username: string }) {
3  // Nawet jeśli username = "<script>alert('xss')</script>"
4  // React wyświetli to jako tekst, nie wykona
5  return <div>Witaj, {username}!</div>;
6}

Uważaj na dangerouslySetInnerHTML

1// NIEBEZPIECZNE - może wykonać złośliwy kod
2function DangerousComponent({ html }: { html: string }) {
3  return <div dangerouslySetInnerHTML={{ __html: html }} />;
4}
5
6// BEZPIECZNE - użyj biblioteki do sanityzacji
7import DOMPurify from 'isomorphic-dompurify';
8
9function SafeComponent({ html }: { html: string }) {
10  const sanitized = DOMPurify.sanitize(html, {
11    ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
12    ALLOWED_ATTR: ['href', 'target']
13  });
14
15  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
16}

Content Security Policy (CSP)

CSP to mechanizm zabezpieczający przed XSS poprzez określenie, jakie źródła kodu są zaufane:

1// next.config.js
2const ContentSecurityPolicy = `
3  default-src 'self';
4  script-src 'self' 'unsafe-eval' 'unsafe-inline' https://trusted-cdn.com;
5  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
6  img-src 'self' data: https: blob:;
7  font-src 'self' https://fonts.gstatic.com;
8  connect-src 'self' https://api.example.com;
9  frame-ancestors 'none';
10`;
11
12const securityHeaders = [
13  {
14    key: 'Content-Security-Policy',
15    value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
16  }
17];
18
19module.exports = {
20  async headers() {
21    return [
22      {
23        source: '/:path*',
24        headers: securityHeaders
25      }
26    ];
27  }
28};

Lub przez middleware:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7
8  const csp = [
9    "default-src 'self'",
10    "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
11    "style-src 'self' 'unsafe-inline'",
12    "img-src 'self' data: https:",
13    "font-src 'self' data:",
14    "connect-src 'self' https://api.example.com"
15  ].join('; ');
16
17  response.headers.set('Content-Security-Policy', csp);
18
19  return response;
20}

Rate Limiting - ochrona przed nadużyciami

Rate limiting chroni aplikację przed atakami typu DDoS i nadużyciami API. Możemy zaimplementować to w Next.js Middleware:

1// lib/rate-limit.ts
2import { LRUCache } from 'lru-cache';
3
4type RateLimitOptions = {
5  interval: number; // czas w milisekundach
6  uniqueTokenPerInterval: number; // maksymalna liczba unikalnych tokenów
7};
8
9export function rateLimit(options: RateLimitOptions) {
10  const tokenCache = new LRUCache({
11    max: options.uniqueTokenPerInterval || 500,
12    ttl: options.interval || 60000
13  });
14
15  return {
16    check: (limit: number, token: string): { success: boolean; remaining: number } => {
17      const tokenCount = (tokenCache.get(token) as number[]) || [0];
18
19      if (tokenCount[0] === 0) {
20        tokenCache.set(token, [1]);
21        return { success: true, remaining: limit - 1 };
22      }
23
24      const currentCount = tokenCount[0];
25
26      if (currentCount >= limit) {
27        return { success: false, remaining: 0 };
28      }
29
30      tokenCache.set(token, [currentCount + 1]);
31      return { success: true, remaining: limit - currentCount - 1 };
32    }
33  };
34}
35
36// middleware.ts
37import { NextResponse } from 'next/server';
38import type { NextRequest } from 'next/server';
39import { rateLimit } from '@/lib/rate-limit';
40
41const limiter = rateLimit({
42  interval: 60 * 1000, // 1 minuta
43  uniqueTokenPerInterval: 500
44});
45
46export async function middleware(request: NextRequest) {
47  // Użyj IP jako identyfikatora
48  const ip = request.ip ?? 'anonymous';
49
50  try {
51    const { success, remaining } = limiter.check(10, ip); // 10 requestów na minutę
52
53    if (!success) {
54      return new NextResponse('Too Many Requests', {
55        status: 429,
56        headers: {
57          'X-RateLimit-Limit': '10',
58          'X-RateLimit-Remaining': '0',
59          'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
60        }
61      });
62    }
63
64    const response = NextResponse.next();
65    response.headers.set('X-RateLimit-Limit', '10');
66    response.headers.set('X-RateLimit-Remaining', remaining.toString());
67
68    return response;
69  } catch (error) {
70    return new NextResponse('Internal Server Error', { status: 500 });
71  }
72}
73
74export const config = {
75  matcher: '/api/:path*'
76};

Rate Limiting dla Server Actions

1// lib/rate-limit-server-action.ts
2import { cookies } from 'next/headers';
3
4const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
5
6export async function checkRateLimit(
7  action: string,
8  limit: number = 10,
9  windowMs: number = 60000
10): Promise<boolean> {
11  const cookieStore = await cookies();
12  const sessionId = cookieStore.get('session-id')?.value || 'anonymous';
13  const key = `${sessionId}:${action}`;
14
15  const now = Date.now();
16  const record = rateLimitStore.get(key);
17
18  if (!record || now > record.resetAt) {
19    rateLimitStore.set(key, {
20      count: 1,
21      resetAt: now + windowMs
22    });
23    return true;
24  }
25
26  if (record.count >= limit) {
27    return false;
28  }
29
30  record.count++;
31  return true;
32}
33
34// app/actions.ts
35'use server';
36
37import { checkRateLimit } from '@/lib/rate-limit-server-action';
38
39export async function sensitiveAction(data: FormData) {
40  const allowed = await checkRateLimit('sensitive-action', 5, 60000);
41
42  if (!allowed) {
43    throw new Error('Rate limit exceeded. Please try again later.');
44  }
45
46  // Wykonaj akcję...
47}

Secure Headers (HSTS, X-Frame-Options, etc.)

Odpowiednie nagłówki HTTP to fundament bezpieczeństwa aplikacji webowych:

1// next.config.js
2const securityHeaders = [
3  {
4    key: 'X-DNS-Prefetch-Control',
5    value: 'on'
6  },
7  {
8    key: 'Strict-Transport-Security',
9    value: 'max-age=63072000; includeSubDomains; preload'
10  },
11  {
12    key: 'X-Frame-Options',
13    value: 'SAMEORIGIN'
14  },
15  {
16    key: 'X-Content-Type-Options',
17    value: 'nosniff'
18  },
19  {
20    key: 'X-XSS-Protection',
21    value: '1; mode=block'
22  },
23  {
24    key: 'Referrer-Policy',
25    value: 'strict-origin-when-cross-origin'
26  },
27  {
28    key: 'Permissions-Policy',
29    value: 'camera=(), microphone=(), geolocation=()'
30  }
31];
32
33module.exports = {
34  async headers() {
35    return [
36      {
37        source: '/:path*',
38        headers: securityHeaders
39      }
40    ];
41  }
42};

Lub przez middleware dla większej kontroli:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7
8  // HSTS - wymusza HTTPS
9  response.headers.set(
10    'Strict-Transport-Security',
11    'max-age=31536000; includeSubDomains'
12  );
13
14  // X-Frame-Options - ochrona przed clickjacking
15  response.headers.set('X-Frame-Options', 'SAMEORIGIN');
16
17  // X-Content-Type-Options - zapobiega MIME sniffing
18  response.headers.set('X-Content-Type-Options', 'nosniff');
19
20  // X-XSS-Protection - dodatkowa ochrona przed XSS
21  response.headers.set('X-XSS-Protection', '1; mode=block');
22
23  // Referrer-Policy - kontrola informacji w nagłówku Referer
24  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
25
26  // Permissions-Policy - kontrola dostępu do API przeglądarki
27  response.headers.set(
28    'Permissions-Policy',
29    'camera=(), microphone=(), geolocation=(), payment=()'
30  );
31
32  return response;
33}

Bezpieczeństwo API Routes i Server Actions

Walidacja input w Server Actions

1// app/actions.ts
2'use server';
3
4import { z } from 'zod';
5import { revalidatePath } from 'next/cache';
6
7const CreatePostSchema = z.object({
8  title: z.string().min(3).max(100),
9  content: z.string().min(10).max(5000),
10  tags: z.array(z.string()).max(5).optional()
11});
12
13export async function createPost(formData: FormData) {
14  // Walidacja danych wejściowych
15  const rawData = {
16    title: formData.get('title'),
17    content: formData.get('content'),
18    tags: formData.getAll('tags')
19  };
20
21  const validation = CreatePostSchema.safeParse(rawData);
22
23  if (!validation.success) {
24    return {
25      error: 'Validation failed',
26      issues: validation.error.issues
27    };
28  }
29
30  const { title, content, tags } = validation.data;
31
32  // Weryfikacja autentykacji
33  const session = await getServerSession();
34  if (!session?.user) {
35    return { error: 'Unauthorized' };
36  }
37
38  // Sanityzacja HTML (jeśli pozwalamy na formatowanie)
39  const sanitizedContent = sanitizeHtml(content);
40
41  try {
42    const post = await db.post.create({
43      data: {
44        title,
45        content: sanitizedContent,
46        tags,
47        authorId: session.user.id
48      }
49    });
50
51    revalidatePath('/blog');
52
53    return { success: true, post };
54  } catch (error) {
55    console.error('Failed to create post:', error);
56    return { error: 'Failed to create post' };
57  }
58}

Bezpieczne API Routes

1// app/api/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth';
4import { z } from 'zod';
5
6const QuerySchema = z.object({
7  page: z.coerce.number().min(1).default(1),
8  limit: z.coerce.number().min(1).max(100).default(10)
9});
10
11export async function GET(request: NextRequest) {
12  try {
13    // Weryfikacja autentykacji
14    const session = await getServerSession();
15    if (!session) {
16      return NextResponse.json(
17        { error: 'Unauthorized' },
18        { status: 401 }
19      );
20    }
21
22    // Walidacja query parameters
23    const { searchParams } = new URL(request.url);
24    const validation = QuerySchema.safeParse({
25      page: searchParams.get('page'),
26      limit: searchParams.get('limit')
27    });
28
29    if (!validation.success) {
30      return NextResponse.json(
31        { error: 'Invalid parameters', issues: validation.error.issues },
32        { status: 400 }
33      );
34    }
35
36    const { page, limit } = validation.data;
37
38    // Bezpieczne query do bazy danych
39    const users = await db.user.findMany({
40      skip: (page - 1) * limit,
41      take: limit,
42      select: {
43        id: true,
44        name: true,
45        email: true,
46        // NIE zwracaj hasła ani innych wrażliwych danych
47      }
48    });
49
50    return NextResponse.json({ users });
51  } catch (error) {
52    console.error('API Error:', error);
53    // NIE zwracaj szczegółów błędów w produkcji
54    return NextResponse.json(
55      { error: 'Internal server error' },
56      { status: 500 }
57    );
58  }
59}

Dodatkowe best practices

1. Bezpieczne przechowywanie sekretów

1// .env.local (NIGDY nie commituj do repo!)
2DATABASE_URL="postgresql://..."
3NEXTAUTH_SECRET="super-secret-key"
4API_KEY="secret-api-key"
5
6// app/config.ts
7const requiredEnvVars = [
8  'DATABASE_URL',
9  'NEXTAUTH_SECRET',
10  'API_KEY'
11] as const;
12
13// Walidacja obecności zmiennych środowiskowych przy starcie
14requiredEnvVars.forEach((varName) => {
15  if (!process.env[varName]) {
16    throw new Error(`Missing required environment variable: ${varName}`);
17  }
18});
19
20export const config = {
21  database: {
22    url: process.env.DATABASE_URL!
23  },
24  auth: {
25    secret: process.env.NEXTAUTH_SECRET!
26  },
27  api: {
28    key: process.env.API_KEY!
29  }
30} as const;

2. Bezpieczne cookies

1// lib/cookies.ts
2import { cookies } from 'next/headers';
3
4export async function setSecureCookie(name: string, value: string) {
5  const cookieStore = await cookies();
6
7  cookieStore.set(name, value, {
8    httpOnly: true, // Niedostępne dla JavaScript
9    secure: process.env.NODE_ENV === 'production', // Tylko HTTPS w produkcji
10    sameSite: 'strict', // Ochrona przed CSRF
11    maxAge: 60 * 60 * 24 * 7, // 7 dni
12    path: '/'
13  });
14}

3. Logging i monitoring

1// lib/logger.ts
2export function logSecurityEvent(event: {
3  type: 'auth_failure' | 'rate_limit' | 'csrf_violation' | 'xss_attempt';
4  ip: string;
5  userAgent: string;
6  details?: any;
7}) {
8  const logEntry = {
9    timestamp: new Date().toISOString(),
10    ...event
11  };
12
13  // W produkcji wyślij do systemu monitoringu (Sentry, LogRocket, etc.)
14  if (process.env.NODE_ENV === 'production') {
15    // sendToMonitoring(logEntry);
16  }
17
18  console.warn('Security Event:', logEntry);
19}
20
21// middleware.ts
22export function middleware(request: NextRequest) {
23  const ip = request.ip ?? 'unknown';
24  const userAgent = request.headers.get('user-agent') ?? 'unknown';
25
26  // Loguj podejrzane aktywności
27  if (isSuspicious(request)) {
28    logSecurityEvent({
29      type: 'csrf_violation',
30      ip,
31      userAgent,
32      details: { url: request.url }
33    });
34  }
35
36  return NextResponse.next();
37}

Podsumowanie security best practices

W Metropolii Quantum bezpieczeństwo to proces ciągły, nie jednorazowa akcja. Kluczowe zasady:

  1. Nigdy nie ufaj inputowi użytkownika - zawsze waliduj i sanityzuj
  2. Używaj HTTPS wszędzie - wymuś przez HSTS
  3. Implementuj rate limiting - chroń przed nadużyciami
  4. Ustawiaj bezpieczne nagłówki - CSP, X-Frame-Options, etc.
  5. Chroń przed CSRF - weryfikuj Origin, używaj tokenów
  6. Zapobiegaj XSS - sanityzuj HTML, używaj CSP
  7. Bezpiecznie przechowuj sekrety - używaj zmiennych środowiskowych
  8. Loguj zdarzenia bezpieczeństwa - monitoruj podejrzane aktywności
  9. Regularnie aktualizuj zależności - łataj znane luki
  10. Stosuj principle of least privilege - minimalne uprawnienia

Podsumowanie: Najlepsze praktyki optymalizacji wydajności

Podsumowując, oto kluczowe zasady optymalizacji wydajności w Next.js:

  1. Używaj Edge Runtime dla dynamicznych, globalnych aplikacji wymagających niskiego opóźnienia
  2. Wykorzystuj React Server Components aby zmniejszyć ilość JS wysyłanego do klienta
  3. Optymalizuj obrazy za pomocą komponentu
    Image
    z Next.js
  4. Implementuj streaming z komponentem
    Suspense
    dla lepszej responsywności UI
  5. Zarządzaj cachowaniem na różnych poziomach (przeglądarka, CDN, serwer)
  6. Wykorzystuj prefetching dla szybszej nawigacji między stronami
  7. Monitoruj Web Vitals i ciągle optymalizuj krytyczne ścieżki renderowania
  8. Optymalizuj ładowanie fontów używając wbudowanego systemu fontów Next.js
  9. Eliminuj zbędny JavaScript poprzez analizę bundle size i usuwanie nieużywanych zależności
  10. Testuj na realnych urządzeniach i wolniejszych połączeniach, aby zidentyfikować problemy z wydajnością

Stosując te praktyki, możesz tworzyć aplikacje Next.js, które są zarówno funkcjonalne, jak i błyskawicznie szybkie, zapewniając użytkownikom doskonałe doświadczenia niezależnie od ich lokalizacji czy używanego urządzenia.

Vai a CodeWorlds