Kurs Next.js · Moduł 8: Deployment i produkcja

Przygotowanie aplikacji do produkcji (build, analyze)

5 min czytania
W tej lekcji6

Przygotowanie aplikacji Next.js do wdrożenia produkcyjnego to kluczowy etap procesu rozwoju, który wymaga szczególnej uwagi do optymalizacji, analizy i konfiguracji. Odpowiednio przygotowana aplikacja zapewni lepsze wrażenia użytkownika, wyższą wydajność i łatwiejsze utrzymanie.

Proces budowania aplikacji Next.js

Proces budowania (build) w Next.js przekształca kod źródłowy w zoptymalizowaną wersję gotową do wdrożenia. Podczas tego procesu Next.js:

  1. Kompiluje kod React i TypeScript do JavaScript
  2. Minifikuje kod CSS i JavaScript
  3. Optymalizuje obrazy
  4. Generuje statyczne strony (dla getStaticProps)
  5. Przygotowuje komponenty serwerowe (dla getServerSideProps)
  6. Tworzy pliki konieczne do nawigacji po stronie klienta

Podstawowy proces budowania

Aby zbudować aplikację Next.js, wykonaj następujące kroki:

1# Najpierw upewnij się, że wszystkie zależności są zainstalowane
2npm install
3
4# Następnie zbuduj aplikację
5npm run build

Po zakończeniu procesu budowania, Next.js wyświetli zestawienie stron, ich typów (statyczne, SSR, API) oraz rozmiaru każdej strony. To podsumowanie dostarcza cennych informacji o wydajności aplikacji.

Uruchamianie zbudowanej aplikacji

Po pomyślnym zbudowaniu aplikacji możesz uruchomić wersję produkcyjną lokalnie za pomocą:

1npm run start

Ten polecenie uruchamia serwer Node.js, który obsługuje twoją aplikację Next.js w trybie produkcyjnym, bez funkcji hot-reloading i z zoptymalizowanymi aktywami.

Analiza pakietu (bundle analysis)

Analiza rozmiaru pakietu jest niezbędna do zrozumienia, co dokładnie wpływa na wielkość twojej aplikacji. Next.js oferuje wbudowane narzędzie do analizy pakietu, które można włączyć za pomocą zmiennej środowiskowej:

1ANALYZE=true npm run build

Aby skorzystać z tej funkcji, należy zainstalować i skonfigurować narzędzie @next/bundle-analyzer:

1npm install --save-dev @next/bundle-analyzer

Następnie zaktualizuj plik next.config.js:

1const withBundleAnalyzer = require('@next/bundle-analyzer')({
2  enabled: process.env.ANALYZE === 'true',
3})
4
5module.exports = withBundleAnalyzer({
6  // Tutaj umieść pozostałe opcje konfiguracyjne Next.js
7})

Po uruchomieniu budowy z włączonym analizatorem, wygenerowane zostaną wizualne raporty pokazujące szczegółowy rozkład rozmiaru pakietu klienta i serwera.

Interpretacja wyników analizy pakietu

Analiza pakietu ujawnia:

  1. Duże zależności - biblioteki, które znacząco wpływają na rozmiar pakietu
  2. Duplikaty - te same pakiety załadowane kilkakrotnie
  3. Nieużywany kod - martwy kod, który można usunąć
  4. Rozkład modułów - które części aplikacji zajmują najwięcej miejsca

Na podstawie tych informacji możesz podjąć świadome decyzje dotyczące optymalizacji:

  • Zastąpić duże biblioteki lżejszymi alternatywami
  • Zaimplementować ładowanie leniwe dla dużych komponentów
  • Usunąć nieużywane zależności
  • Podzielić kod na mniejsze kawałki

Optymalizacja wydajności

Code splitting

Next.js automatycznie implementuje podział kodu, ale możesz dodatkowo zoptymalizować ten proces, używając dynamicznego importu:

1import dynamic from 'next/dynamic';
2
3// Zamiast importować bezpośrednio
4// import HeavyComponent from '../components/HeavyComponent';
5
6// Użyj dynamicznego importu
7const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
8  loading: () => <p>Ładowanie...</p>,
9  // SSR można wyłączyć dla komponentów, które nie są potrzebne na serwerze
10  ssr: false,
11});
12
13export default function HomePage() {
14  return (
15    <div>
16      <h1>Strona główna</h1>
17      <HeavyComponent />
18    </div>
19  );
20}

Optymalizacja obrazów

Next.js oferuje komponent Image, który automatycznie optymalizuje obrazy, ale warto dodatkowo skonfigurować opcje:

1import Image from 'next/image';
2
3export default function ProductCard({ product }) {
4  return (
5    <div>
6      <Image
7        src={product.imageUrl}
8        alt={product.name}
9        width={300}
10        height={200}
11        placeholder="blur"
12        blurDataURL={product.thumbnailUrl}
13        priority={product.featured} // Ładuje obraz z wysokim priorytetem, jeśli jest wyróżniony
14      />
15      <h2>{product.name}</h2>
16    </div>
17  );
18}

Tree shaking

Tree shaking to technika eliminacji martwego kodu podczas budowania. Next.js korzysta z Webpack, który wykonuje tree shaking automatycznie. Aby maksymalizować korzyści z tree shaking:

  1. Używaj importów ES Modules zamiast require
  2. Unikaj importów typu side-effect (np. import 'library')
  3. Preferuj importy nazwane zamiast domyślnych, gdy potrzebujesz tylko części biblioteki:
1// Źle: importuje całą bibliotekę
2import _ from 'lodash';
3const sortedItems = _.sortBy(items, 'name');
4
5// Dobrze: importuje tylko potrzebną funkcję
6import { sortBy } from 'lodash-es';
7const sortedItems = sortBy(items, 'name');

Zmienne środowiskowe

Next.js obsługuje zmienne środowiskowe poprzez pliki .env, .env.local, .env.development i .env.production. Są one kluczowe do konfiguracji aplikacji w różnych środowiskach.

1# .env.production
2NEXT_PUBLIC_API_URL=https://api.twojadomena.com
3DATABASE_URL=postgresql://user:password@production-db:5432/mydb
4FEATURE_FLAGS_ENABLED=true

Zmienne prefixowane NEXT_PUBLIC_ są dostępne w przeglądarce, podczas gdy pozostałe są widoczne tylko na serwerze.

Bezpieczne zarządzanie sekretami

Dla sekretów produkcyjnych:

  1. Nigdy nie umieszczaj sekretów w repozytorium
  2. Używaj zarządzania sekretami platformy hostingowej (Vercel, Netlify)
  3. Dla wrażliwych operacji, preferuj kod po stronie serwera (API Routes)
1// pages/api/secure-operation.ts
2export default async function handler(req, res) {
3  // API_SECRET jest niedostępny w kodzie klienta
4  const apiSecret = process.env.API_SECRET;
5
6  // Bezpieczne wykonanie operacji z sekretem
7  const result = await performSecureOperation(apiSecret);
8
9  // Zwróć tylko bezpieczne dane
10  res.status(200).json({ success: true, data: result.safeData });
11}

Testowanie produkcyjne

Przed wdrożeniem, przeprowadź testy wersji produkcyjnej:

  1. Testy wydajnościowe - użyj Lighthouse lub WebPageTest
  2. Testy funkcjonalne - sprawdź, czy wszystkie funkcje działają zgodnie z oczekiwaniami
  3. Testy responsywności - sprawdź różne rozmiary ekranu
  4. Testy przeglądarek - sprawdź kompatybilność z różnymi przeglądarkami

Automatyzacja testów przedprodukcyjnych

Skonfiguruj skrypt, który przeprowadzi wszystkie niezbędne testy:

1// scripts/pre-deploy-checks.js
2const { execSync } = require('child_process');
3
4// Uruchom testy jednostkowe
5console.log('Uruchamianie testów jednostkowych...');
6execSync('npm run test', { stdio: 'inherit' });
7
8// Uruchom testy end-to-end
9console.log('Uruchamianie testów E2E...');
10execSync('npm run test:e2e', { stdio: 'inherit' });
11
12// Sprawdź typy
13console.log('Sprawdzanie typów...');
14execSync('npm run typecheck', { stdio: 'inherit' });
15
16// Sprawdź lintowanie
17console.log('Sprawdzanie lintingu...');
18execSync('npm run lint', { stdio: 'inherit' });
19
20// Zbuduj aplikację
21console.log('Budowanie aplikacji...');
22execSync('npm run build', { stdio: 'inherit' });
23
24console.log('Wszystkie sprawdzenia przedwdrożeniowe przeszły pomyślnie!');

Podsumowanie

Przygotowanie aplikacji Next.js do produkcji to wieloetapowy proces, który obejmuje:

  1. Budowanie zoptymalizowanej wersji aplikacji
  2. Analizę pakietu w celu identyfikacji możliwości optymalizacji
  3. Implementację technik poprawiających wydajność
  4. Konfigurację zmiennych środowiskowych
  5. Bezpieczne zarządzanie sekretami
  6. Testowanie produkcyjne

Starannie przeprowadzony proces przygotowania aplikacji do produkcji zapewnia lepszą wydajność, bezpieczeństwo i doświadczenie użytkownika, co przekłada się na sukces biznesowy projektu.

Pamiętaj, że optymalizacja jest procesem ciągłym - regularne monitorowanie wydajności i aktualizowanie aplikacji zgodnie z nowymi najlepszymi praktykami to klucz do utrzymania wydajnej aplikacji.

Kod do tej lekcji: App.tsx
1// Demo: Przygotowanie aplikacji Next.js do produkcji
2// Build process, optymalizacja, checklist deployment
3import React, { useState } from 'react';
4
5// Proces budowania Next.js:
6// 1. npm run build → next build
7//    - Kompiluje TypeScript
8//    - Generuje statyczne strony (SSG)
9//    - Optymalizuje bundle (tree shaking, code splitting)
10//    - Generuje sitemap, robots.txt
11// 2. npm run start → next start
12//    - Uruchamia production server
13//    - Serwuje statyczne pliki z .next/
14//    - Obsługuje SSR i ISR
15
16interface ChecklistItem {
17  id: string;
18  category: string;
19  title: string;
20  description: string;
21  impact: 'critical' | 'high' | 'medium';
22  command?: string;
23}
24
25const checklist: ChecklistItem[] = [
26  {
27    id: '1', category: 'Build', title: 'Build bez błędów',
28    description: 'npm run build musi zakończyć się sukcesem',
29    impact: 'critical', command: 'npm run build',
30  },
31  {
32    id: '2', category: 'Build', title: 'TypeScript - brak błędów typów',
33    description: 'Strict mode, zero any warnings',
34    impact: 'critical', command: 'npx tsc --noEmit',
35  },
36  {
37    id: '3', category: 'Build', title: 'Linting',
38    description: 'ESLint bez ostrzeżeń i błędów',
39    impact: 'high', command: 'npm run lint',
40  },
41  {
42    id: '4', category: 'Performance', title: 'Bundle analysis',
43    description: 'Sprawdź rozmiar bundli, usuń nieużywane zależności',
44    impact: 'high', command: 'ANALYZE=true npm run build',
45  },
46  {
47    id: '5', category: 'Performance', title: 'Image optimization',
48    description: 'Użyj next/image, WebP/AVIF, lazy loading',
49    impact: 'high',
50  },
51  {
52    id: '6', category: 'Performance', title: 'Font optimization',
53    description: 'Użyj next/font, self-hosting, font-display: swap',
54    impact: 'medium',
55  },
56  {
57    id: '7', category: 'SEO', title: 'Metadata na każdej stronie',
58    description: 'title, description, Open Graph dla social sharing',
59    impact: 'high',
60  },
61  {
62    id: '8', category: 'SEO', title: 'sitemap.xml i robots.txt',
63    description: 'Automatyczna generacja w app/sitemap.ts i app/robots.ts',
64    impact: 'medium',
65  },
66  {
67    id: '9', category: 'Security', title: 'Environment variables',
68    description: 'Sekrety w .env.local, NEXT_PUBLIC_ tylko dla publicznych',
69    impact: 'critical',
70  },
71  {
72    id: '10', category: 'Security', title: 'Headers bezpieczeństwa',
73    description: 'CSP, X-Frame-Options, HSTS w next.config.js',
74    impact: 'high',
75  },
76  {
77    id: '11', category: 'Deploy', title: 'Wybór platformy',
78    description: 'Vercel (najlepsze), Netlify, AWS, Docker',
79    impact: 'high',
80  },
81  {
82    id: '12', category: 'Deploy', title: 'CI/CD pipeline',
83    description: 'GitHub Actions: build → test → deploy',
84    impact: 'medium',
85  },
86];
87
88const impactColors = { critical: '#f44336', high: '#ff9800', medium: '#64ffda' };
89const categories = [...new Set(checklist.map(c => c.category))];
90
91function BuildOutput() {
92  const routes = [
93    { path: '/', type: 'Static', size: '5.2 kB' },
94    { path: '/products', type: 'SSG', size: '12.4 kB' },
95    { path: '/products/[id]', type: 'SSG', size: '8.7 kB' },
96    { path: '/dashboard', type: 'SSR', size: '15.1 kB' },
97    { path: '/api/health', type: 'API', size: '1.2 kB' },
98  ];
99
100  const typeIcons: Record<string, string> = {
101    Static: 'S', SSG: 'G', SSR: 'R', API: 'A',
102  };
103  const typeColors: Record<string, string> = {
104    Static: '#7c4dff', SSG: '#64ffda', SSR: '#ff9800', API: '#2196f3',
105  };
106
107  return (
108    <div style={{
109      background: 'rgba(0,0,0,0.3)', borderRadius: '8px', padding: '16px',
110    }}>
111      <h4 style={{ color: '#64ffda', marginBottom: '8px', fontSize: '0.85rem' }}>
112        $ next build
113      </h4>
114      <div style={{ display: 'grid', gap: '4px' }}>
115        {routes.map(r => (
116          <div key={r.path} style={{
117            display: 'flex', alignItems: 'center', gap: '10px',
118            padding: '6px 8px', fontFamily: 'monospace', fontSize: '0.8rem',
119          }}>
120            <span style={{
121              width: '20px', height: '20px', borderRadius: '4px',
122              background: typeColors[r.type], color: '#0f0f23',
123              display: 'flex', alignItems: 'center', justifyContent: 'center',
124              fontSize: '0.7rem', fontWeight: 'bold', flexShrink: 0,
125            }}>{typeIcons[r.type]}</span>
126            <span style={{ color: '#b0bec5', flex: 1 }}>{r.path}</span>
127            <span style={{ color: '#78909c' }}>{r.size}</span>
128          </div>
129        ))}
130      </div>
131      <div style={{
132        marginTop: '12px', padding: '8px', borderTop: '1px solid rgba(255,255,255,0.05)',
133        display: 'flex', justifyContent: 'space-between',
134      }}>
135        <span style={{ color: '#4caf50', fontSize: '0.8rem' }}>Build completed in 12.4s</span>
136        <span style={{ color: '#78909c', fontSize: '0.8rem' }}>Total: 42.6 kB</span>
137      </div>
138    </div>
139  );
140}
141
142export default function ProductionChecklist() {
143  const [completed, setCompleted] = useState<Set<string>>(new Set());
144  const [activeCategory, setActiveCategory] = useState('all');
145
146  const toggle = (id: string) => {
147    setCompleted(prev => {
148      const next = new Set(prev);
149      if (next.has(id)) next.delete(id); else next.add(id);
150      return next;
151    });
152  };
153
154  const filtered = activeCategory === 'all'
155    ? checklist
156    : checklist.filter(c => c.category === activeCategory);
157
158  const progress = Math.round((completed.size / checklist.length) * 100);
159
160  return (
161    <div style={{
162      background: '#0f0f23', minHeight: '100vh', padding: '24px',
163      color: '#fff', fontFamily: 'system-ui, sans-serif',
164    }}>
165      <h1 style={{ color: '#64ffda', marginBottom: '4px' }}>Production Checklist</h1>
166      <p style={{ color: '#b0bec5', marginBottom: '16px' }}>
167        Przygotowanie aplikacji Next.js do wdrożenia
168      </p>
169
170      <div style={{
171        background: 'rgba(100,255,218,0.05)', borderRadius: '10px',
172        padding: '12px 16px', marginBottom: '20px',
173        display: 'flex', alignItems: 'center', gap: '16px',
174      }}>
175        <div style={{
176          width: '48px', height: '48px', borderRadius: '50%',
177          background: `conic-gradient(#64ffda ${progress * 3.6}deg, rgba(255,255,255,0.05) 0deg)`,
178          display: 'flex', alignItems: 'center', justifyContent: 'center',
179        }}>
180          <span style={{ color: '#64ffda', fontWeight: 'bold', fontSize: '0.8rem' }}>{progress}%</span>
181        </div>
182        <div>
183          <p style={{ color: '#fff', margin: 0, fontWeight: 600 }}>{completed.size}/{checklist.length} completed</p>
184          <p style={{ color: '#78909c', margin: 0, fontSize: '0.8rem' }}>
185            {progress === 100 ? 'Ready to deploy!' : 'Complete all checks before deploying'}
186          </p>
187        </div>
188      </div>
189
190      <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
191        <button onClick={() => setActiveCategory('all')} style={{
192          background: activeCategory === 'all' ? '#64ffda' : 'transparent',
193          color: activeCategory === 'all' ? '#0f0f23' : '#b0bec5',
194          border: '1px solid rgba(100,255,218,0.3)',
195          padding: '6px 12px', borderRadius: '20px', cursor: 'pointer',
196        }}>All</button>
197        {categories.map(c => (
198          <button key={c} onClick={() => setActiveCategory(c)} style={{
199            background: activeCategory === c ? '#64ffda' : 'transparent',
200            color: activeCategory === c ? '#0f0f23' : '#b0bec5',
201            border: '1px solid rgba(100,255,218,0.3)',
202            padding: '6px 12px', borderRadius: '20px', cursor: 'pointer',
203          }}>{c}</button>
204        ))}
205      </div>
206
207      <div style={{ display: 'grid', gap: '8px', marginBottom: '20px' }}>
208        {filtered.map(item => (
209          <div key={item.id} onClick={() => toggle(item.id)} style={{
210            display: 'flex', alignItems: 'center', gap: '12px',
211            background: completed.has(item.id) ? 'rgba(76,175,80,0.05)' : 'rgba(255,255,255,0.02)',
212            border: `1px solid ${completed.has(item.id) ? 'rgba(76,175,80,0.2)' : 'rgba(255,255,255,0.06)'}`,
213            borderRadius: '8px', padding: '12px', cursor: 'pointer',
214          }}>
215            <div style={{
216              width: '22px', height: '22px', borderRadius: '6px',
217              border: `2px solid ${completed.has(item.id) ? '#4caf50' : 'rgba(255,255,255,0.2)'}`,
218              background: completed.has(item.id) ? '#4caf50' : 'transparent',
219              display: 'flex', alignItems: 'center', justifyContent: 'center',
220              color: '#fff', fontSize: '0.7rem', flexShrink: 0,
221            }}>
222              {completed.has(item.id) ? 'V' : ''}
223            </div>
224            <div style={{ flex: 1 }}>
225              <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
226                <span style={{
227                  color: completed.has(item.id) ? '#78909c' : '#fff',
228                  textDecoration: completed.has(item.id) ? 'line-through' : 'none',
229                }}>{item.title}</span>
230                <span style={{
231                  background: `${impactColors[item.impact]}15`,
232                  color: impactColors[item.impact],
233                  padding: '1px 8px', borderRadius: '10px', fontSize: '0.65rem',
234                }}>{item.impact}</span>
235              </div>
236              <p style={{ color: '#78909c', fontSize: '0.8rem', margin: '2px 0 0' }}>{item.description}</p>
237            </div>
238          </div>
239        ))}
240      </div>
241
242      <BuildOutput />
243    </div>
244  );
245}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Komenda 'npm run build' w Next.js:

  2. 2. Bundle analyzer w Next.js służy do:

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zbuduj interfejs z optymalizacją bundle size i lazy loading

  • Klikanie w kolejności

    Ułóż elementy w prawidłowej kolejności: npm → install → @next/bundle-analyzer

Przydatne artykuły