Kurs Next.js · Moduł 8: Deployment i produkcja
Przygotowanie aplikacji do produkcji (build, analyze)
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:
- Kompiluje kod React i TypeScript do JavaScript
- Minifikuje kod CSS i JavaScript
- Optymalizuje obrazy
- Generuje statyczne strony (dla getStaticProps)
- Przygotowuje komponenty serwerowe (dla getServerSideProps)
- 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 buildPo 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 startTen 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 buildAby skorzystać z tej funkcji, należy zainstalować i skonfigurować narzędzie @next/bundle-analyzer:
1npm install --save-dev @next/bundle-analyzerNastę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:
- Duże zależności - biblioteki, które znacząco wpływają na rozmiar pakietu
- Duplikaty - te same pakiety załadowane kilkakrotnie
- Nieużywany kod - martwy kod, który można usunąć
- 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:
- Używaj importów ES Modules zamiast require
- Unikaj importów typu side-effect (np.
import 'library') - 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=trueZmienne 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:
- Nigdy nie umieszczaj sekretów w repozytorium
- Używaj zarządzania sekretami platformy hostingowej (Vercel, Netlify)
- 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:
- Testy wydajnościowe - użyj Lighthouse lub WebPageTest
- Testy funkcjonalne - sprawdź, czy wszystkie funkcje działają zgodnie z oczekiwaniami
- Testy responsywności - sprawdź różne rozmiary ekranu
- 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:
- Budowanie zoptymalizowanej wersji aplikacji
- Analizę pakietu w celu identyfikacji możliwości optymalizacji
- Implementację technik poprawiających wydajność
- Konfigurację zmiennych środowiskowych
- Bezpieczne zarządzanie sekretami
- 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. Komenda 'npm run build' w Next.js:
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