W Metropolii Quantum, inżynierowie nieustannie monitorują i optymalizują wydajność wszystkich systemów miejskich - od sieci energetycznych po transportowe. Każda decyzja architektoniczna ma wpływ na szybkość i efektywność całego miasta. Podobnie, w Next.js 15, wybór odpowiedniej strategii renderowania ma fundamentalny wpływ na wydajność aplikacji.
W tym rozdziale poznasz szczegółowe porównanie strategii renderowania, metryki wydajności i praktyczne przykłady, które pomogą ci podejmować świadome decyzje architektoniczne.
Jak działa: Każde żądanie użytkownika powoduje renderowanie strony na serwerze w czasie rzeczywistym.
1// app/dashboard/page.tsx
2// Domyślnie Next.js 15 używa cache, więc musimy jawnie wyłączyć
3export const dynamic = 'force-dynamic';
4// LUB
5// fetch(..., { cache: 'no-store' })
6
7async function getDashboardData() {
8 const res = await fetch('https://api.quantum.com/dashboard', {
9 cache: 'no-store' // Wymusza SSR
10 });
11 return res.json();
12}
13
14export default async function Dashboard() {
15 const data = await getDashboardData();
16
17 return (
18 <div>
19 <h1>Panel kontrolny</h1>
20 <p>Ostatnia aktualizacja: {new Date().toLocaleTimeString()}</p>
21 <DashboardContent data={data} />
22 </div>
23 );
24}Zalety:
Wady:
Jak działa: Strony są generowane raz podczas build time i serwowane jako statyczne pliki HTML.
1// app/blog/[slug]/page.tsx
2
3async function getPost(slug: string) {
4 const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
5 cache: 'force-cache' // Wymusza SSG
6 });
7 return res.json();
8}
9
10export async function generateStaticParams() {
11 const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());
12
13 return posts.map((post) => ({
14 slug: post.slug,
15 }));
16}
17
18export default async function BlogPost({
19 params,
20}: {
21 params: Promise<{ slug: string }>;
22}) {
23 const post = await getPost((await params).slug);
24
25 return (
26 <article>
27 <h1>{post.title}</h1>
28 <time>{post.publishedAt}</time>
29 <div dangerouslySetInnerHTML={{ __html: post.content }} />
30 </article>
31 );
32}Zalety:
Wady:
Jak działa: Strony są generowane statycznie, ale mogą być odświeżane w tle po określonym czasie.
1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4 const res = await fetch(`https://api.quantum.com/products/${id}`, {
5 next: { revalidate: 3600 } // Rewalidacja co godzinę
6 });
7 return res.json();
8}
9
10export async function generateStaticParams() {
11 // Generuj tylko najpopularniejsze produkty podczas build
12 const popularProducts = await fetch('https://api.quantum.com/products/popular')
13 .then(res => res.json());
14
15 return popularProducts.map((product) => ({
16 id: product.id.toString(),
17 }));
18}
19
20export default async function ProductPage({
21 params,
22}: {
23 params: Promise<{ id: string }>;
24}) {
25 const product = await getProduct((await params).id);
26
27 return (
28 <div>
29 <h1>{product.name}</h1>
30 <p>Cena: {product.price} QC</p>
31 <p>Dostępność: {product.stock} sztuk</p>
32 <button>Dodaj do koszyka</button>
33 </div>
34 );
35}
36
37export const revalidate = 3600; // Globalny czas rewalidacjiZalety:
Wady:
TTFB to czas od wysłania żądania do otrzymania pierwszego bajta odpowiedzi. W Metropolii Quantum to jak czas reakcji systemu na polecenie - im szybszy, tym lepiej.
Benchmark (przykładowe wartości):
| Strategia | TTFB | Ocena | |-----------|------|-------| | SSG | 50-100ms | ⭐⭐⭐⭐⭐ Doskonały | | ISR (cached) | 50-150ms | ⭐⭐⭐⭐ Bardzo dobry | | ISR (revalidating) | 200-500ms | ⭐⭐⭐ Dobry | | SSR | 300-800ms | ⭐⭐ Średni | | SSR (slow API) | 1000ms+ | ⭐ Słaby |
1// Pomiar TTFB w przeglądarce
2performance.getEntriesByType('navigation')[0].responseStart;LCP mierzy czas do wyświetlenia największego elementu treści. To jak szybkość pojawiania się głównego hologramu w Metropolii Quantum.
Benchmark:
| Strategia | LCP | Ocena | |-----------|-----|-------| | SSG | 0.5-1.5s | ⭐⭐⭐⭐⭐ Doskonały | | ISR | 0.8-2s | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 1.5-3s | ⭐⭐⭐ Dobry | | SSR (heavy) | 3s+ | ⭐⭐ Wymaga optymalizacji |
Rekomendacje Google:
1// Monitoring LCP
2import { onLCP } from 'web-vitals';
3
4onLCP((metric) => {
5 console.log('LCP:', metric.value);
6 // Wysłanie do analytics
7 analytics.track('Web Vitals', {
8 metric: 'LCP',
9 value: metric.value,
10 rating: metric.rating
11 });
12});CLS mierzy wizualną stabilność strony. To jak stabilność holograficznych interfejsów w Metropolii Quantum - nie powinny "skakać".
Benchmark:
| Strategia | CLS | Ocena | |-----------|-----|-------| | SSG | 0-0.05 | ⭐⭐⭐⭐⭐ Doskonały | | ISR | 0-0.1 | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 0.1-0.25 | ⭐⭐⭐ Dobry (jeśli dobrze zoptymalizowane) |
Rekomendacje Google:
1// Monitoring CLS
2import { onCLS } from 'web-vitals';
3
4onCLS((metric) => {
5 console.log('CLS:', metric.value);
6 analytics.track('Web Vitals', {
7 metric: 'CLS',
8 value: metric.value,
9 rating: metric.rating
10 });
11});✅ Blog lub strona marketingowa
1// app/blog/[slug]/page.tsx - Idealny dla SSG
2export async function generateStaticParams() {
3 const posts = await getPosts();
4 return posts.map(post => ({ slug: post.slug }));
5}
6
7export default async function BlogPost({ params }) {
8 const post = await getPost((await params).slug);
9 return <Article post={post} />;
10}✅ Strona dokumentacji
✅ Landing pages
✅ E-commerce (strony produktów)
1// app/products/[id]/page.tsx - Idealny dla ISR
2export const revalidate = 300; // 5 minut
3
4export default async function Product({ params }) {
5 const product = await getProduct((await params).id);
6
7 return (
8 <div>
9 <h1>{product.name}</h1>
10 <p>Cena: {product.price}</p>
11 <p>W magazynie: {product.stock}</p>
12 </div>
13 );
14}✅ Newsroom / Media
✅ Profile użytkowników (publiczne)
✅ Dashboard / Panel administracyjny
1// app/dashboard/page.tsx - Idealny dla SSR
2export const dynamic = 'force-dynamic';
3
4export default async function Dashboard() {
5 const session = await getServerSession();
6 const userData = await getUserData(session.userId);
7
8 return (
9 <div>
10 <h1>Witaj, {userData.name}</h1>
11 <RealTimeAnalytics userId={session.userId} />
12 </div>
13 );
14}✅ Search results
✅ User-specific content
1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5 const post = await getPost((await params).slug);
6
7 return {
8 title: post.title,
9 description: post.excerpt,
10 openGraph: {
11 images: [post.coverImage],
12 },
13 };
14}
15
16export async function generateStaticParams() {
17 const posts = await getAllPosts();
18
19 return posts.map((post) => ({
20 slug: post.slug,
21 }));
22}
23
24async function getPost(slug: string) {
25 const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
26 cache: 'force-cache'
27 });
28 return res.json();
29}
30
31export default async function BlogPost({ params }) {
32 const post = await getPost((await params).slug);
33
34 return (
35 <article>
36 <h1>{post.title}</h1>
37 <time>{post.publishedAt}</time>
38 <Image
39 src={post.coverImage}
40 alt={post.title}
41 width={1200}
42 height={630}
43 priority
44 />
45 <div dangerouslySetInnerHTML={{ __html: post.content }} />
46 </article>
47 );
48}Metryki:
1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4 const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
5 next: { revalidate: 600 } // Rewalidacja co 10 minut
6 });
7
8 if (!res.ok) {
9 notFound();
10 }
11
12 return res.json();
13}
14
15export async function generateStaticParams() {
16 // Generuj tylko top 100 produktów podczas build
17 const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
18 .then(res => res.json());
19
20 return topProducts.map((product) => ({
21 id: product.id.toString(),
22 }));
23}
24
25export default async function ProductPage({ params }) {
26 const product = await getProduct((await params).id);
27
28 return (
29 <div className="product-page">
30 <ProductGallery images={product.images} />
31
32 <div className="product-info">
33 <h1>{product.name}</h1>
34
35 <div className="price-stock">
36 <span className="price">{product.price} QC</span>
37 <StockIndicator stock={product.stock} />
38 </div>
39
40 <div className="description">
41 {product.description}
42 </div>
43
44 <AddToCartButton productId={product.id} />
45
46 <ProductReviews productId={product.id} />
47 </div>
48
49 <RelatedProducts categoryId={product.categoryId} />
50 </div>
51 );
52}
53
54export const revalidate = 600; // 10 minutMetryki:
1// app/dashboard/analytics/page.tsx
2
3export const dynamic = 'force-dynamic';
4export const fetchCache = 'force-no-store';
5
6async function getRealTimeAnalytics(userId: string) {
7 const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
8 cache: 'no-store',
9 headers: {
10 Authorization: `Bearer ${process.env.API_TOKEN}`
11 }
12 });
13
14 return res.json();
15}
16
17export default async function AnalyticsDashboard() {
18 const session = await getServerSession();
19
20 if (!session) {
21 redirect('/login');
22 }
23
24 const analytics = await getRealTimeAnalytics(session.user.id);
25
26 return (
27 <div className="analytics-dashboard">
28 <h1>Analityka w czasie rzeczywistym</h1>
29
30 <div className="stats-grid">
31 <StatCard
32 title="Aktywni użytkownicy"
33 value={analytics.activeUsers}
34 change={analytics.activeUsersChange}
35 />
36
37 <StatCard
38 title="Przychody dzisiaj"
39 value={`${analytics.todayRevenue} QC`}
40 change={analytics.revenueChange}
41 />
42
43 <StatCard
44 title="Konwersje"
45 value={`${analytics.conversionRate}%`}
46 change={analytics.conversionChange}
47 />
48 </div>
49
50 <RevenueChart data={analytics.revenueChart} />
51 <UserActivityFeed data={analytics.recentActivity} />
52 </div>
53 );
54}Metryki:
1// app/news/page.tsx
2
3async function getLatestNews() {
4 const res = await fetch('https://api.quantum-news.com/latest', {
5 next: { revalidate: 60 } // Rewalidacja co minutę
6 });
7
8 return res.json();
9}
10
11export default async function NewsFeed() {
12 const initialNews = await getLatestNews();
13
14 return (
15 <div className="news-feed">
16 <h1>Wiadomości z Metropolii Quantum</h1>
17
18 {/* Server-rendered initial content (ISR) */}
19 <NewsList initialNews={initialNews} />
20
21 {/* Client-side updates co 30 sekund */}
22 <ClientNewsUpdater />
23 </div>
24 );
25}
26
27export const revalidate = 60;1// components/ClientNewsUpdater.tsx
2'use client';
3
4import { useEffect, useState } from 'react';
5
6export function ClientNewsUpdater() {
7 const [latestNews, setLatestNews] = useState([]);
8
9 useEffect(() => {
10 const interval = setInterval(async () => {
11 const res = await fetch('/api/news/latest');
12 const data = await res.json();
13 setLatestNews(data);
14 }, 30000); // 30 sekund
15
16 return () => clearInterval(interval);
17 }, []);
18
19 if (latestNews.length === 0) return null;
20
21 return (
22 <div className="breaking-news">
23 <h2>Najnowsze wiadomości</h2>
24 {latestNews.map(news => (
25 <NewsCard key={news.id} news={news} />
26 ))}
27 </div>
28 );
29}Metryki:
1// app/layout.tsx
2import { SpeedInsights } from '@vercel/speed-insights/next';
3import { Analytics } from '@vercel/analytics/react';
4
5export default function RootLayout({ children }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <SpeedInsights />
11 <Analytics />
12 </body>
13 </html>
14 );
15}1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7 useReportWebVitals((metric) => {
8 // Wysyłanie do własnego analytics
9 const body = JSON.stringify({
10 name: metric.name,
11 value: metric.value,
12 rating: metric.rating,
13 delta: metric.delta,
14 id: metric.id,
15 });
16
17 fetch('/api/analytics/web-vitals', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/json' },
20 body,
21 });
22 });
23
24 return null;
25}1# .github/workflows/lighthouse.yml
2name: Lighthouse CI
3
4on:
5 pull_request:
6 branches: [main]
7
8jobs:
9 lighthouse:
10 runs-on: ubuntu-latest
11 steps:
12 - uses: actions/checkout@v3
13 - uses: actions/setup-node@v3
14
15 - name: Install dependencies
16 run: npm ci
17
18 - name: Build
19 run: npm run build
20
21 - name: Run Lighthouse CI
22 run: |
23 npm install -g @lhci/cli
24 lhci autorun
25
26 - name: Upload results
27 uses: actions/upload-artifact@v3
28 with:
29 name: lighthouse-results
30 path: .lighthouseci1// next.config.js
2module.exports = {
3 images: {
4 formats: ['image/avif', 'image/webp'],
5 deviceSizes: [640, 750, 828, 1080, 1200],
6 },
7 compress: true,
8 poweredByHeader: false,
9 generateEtags: true,
10};1// Użyj on-demand revalidation dla ważnych aktualizacji
2// app/api/revalidate/route.ts
3import { revalidatePath } from 'next/cache';
4import { NextRequest } from 'next/server';
5
6export async function POST(request: NextRequest) {
7 const secret = request.nextUrl.searchParams.get('secret');
8
9 if (secret !== process.env.REVALIDATION_TOKEN) {
10 return Response.json({ message: 'Invalid token' }, { status: 401 });
11 }
12
13 const path = request.nextUrl.searchParams.get('path');
14
15 if (path) {
16 revalidatePath(path);
17 return Response.json({ revalidated: true });
18 }
19
20 return Response.json({ message: 'Missing path' }, { status: 400 });
21}1// Użyj streaming dla szybszego TTFB
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4
5export default function Dashboard() {
6 return (
7 <div>
8 <h1>Dashboard</h1>
9
10 <Suspense fallback={<AnalyticsSkeleton />}>
11 <Analytics />
12 </Suspense>
13
14 <Suspense fallback={<ChartSkeleton />}>
15 <RevenueChart />
16 </Suspense>
17 </div>
18 );
19}1Czy dane muszą być w czasie rzeczywistym?
2├─ TAK → SSR
3│ └─ Rozważ streaming i Suspense dla lepszej wydajności
4│
5└─ NIE
6 │
7 ├─ Czy dane zmieniają się częściej niż raz na godzinę?
8 │ ├─ TAK → ISR (revalidate: 60-3600)
9 │ │ └─ Rozważ on-demand revalidation dla ważnych aktualizacji
10 │ │
11 │ └─ NIE → SSG
12 │ └─ Użyj generateStaticParams dla wszystkich możliwych ścieżek
13 │
14 └─ Czy potrzebujesz personalizacji?
15 ├─ TAK → SSR lub ISR + Client Components
16 └─ NIE → SSG lub ISRKluczowe wnioski:
Podobnie jak inżynierowie Metropolii Quantum optymalizują każdy system dla maksymalnej wydajności, tak i ty powinieneś świadomie wybierać strategię renderowania dla każdej części aplikacji. Nie ma jednego "najlepszego" rozwiązania - klucz to dopasowanie strategii do konkretnego przypadku użycia.
Monitoruj Web Vitals, testuj różne podejścia i zawsze stawiaj doświadczenie użytkownika na pierwszym miejscu. W erze kwantowych technologii, każda millisekunda ma znaczenie!