Ein häufiges Missverständnis: “React ist eine Node.js-Anwendung”.
Falsch. React läuft im Browser – komplett. Node.js ist nur das
Build-Tool, nicht die Runtime. Nach npm run build entsteht
statischer Web-Content: HTML, JavaScript, CSS. Kein Server-Prozess,
keine Node-Abhängigkeiten, kein npm start in
Production.
Das hat fundamentale Konsequenzen: Eine React-App kann auf jedem Webserver gehostet werden, der statische Dateien ausliefern kann. Apache, Nginx, S3 Bucket, CDN – alles funktioniert. Kein Express-Server nötig, keine Port-Konfiguration, keine Process-Manager.
Development-Modus mit Vite:
npm run dev
# Startet Development-Server auf http://localhost:5173
# Hot Module Replacement (HMR)
# Source Maps für Debugging
# Unminified CodeDas ist nicht für Production. Es ist ein Development-Server mit Live-Reload.
Production Build:
npm run build
# Output: dist/ VerzeichnisWas passiert beim Build?
main-a3f2d8b9.js).css FilesVorher (Source):
src/
├── App.tsx
├── components/
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── Dashboard.tsx
├── utils/
│ ├── api.ts
│ └── helpers.ts
├── styles/
│ └── global.css
└── main.tsx
Nachher (Build Output):
dist/
├── index.html
├── assets/
│ ├── index-a3f2d8b9.js # Main bundle
│ ├── vendor-c9e4f1a2.js # Dependencies (React, etc.)
│ ├── Dashboard-b7d3e5f8.js # Lazy-loaded chunk
│ ├── index-f4e8a2c1.css # Extracted CSS
│ └── logo-d4f7a9b3.svg # Static assets
└── favicon.ico
index.html (generiert):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My React App</title>
<link rel="stylesheet" href="/assets/index-f4e8a2c1.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/index-a3f2d8b9.js"></script>
</body>
</html>Das ist alles. Keine Server-Logic, keine Templates, kein Rendering. Browser lädt HTML, lädt JS, führt React aus.
React ist eine Client-Side-Bibliothek. Der gesamte Code läuft im Browser:
https://example.com/index.html<script src="/assets/index-*.js"><div id="root">Kein Backend-Code läuft. API-Calls gehen an separate Backend-Services, aber die React-App selbst ist purer Frontend-Code.
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name example.com;
root /var/www/myapp/dist;
index index.html;
# Client-Side Routing Support
location / {
try_files $uri $uri/ /index.html;
}
# Cache Static Assets
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Gzip Compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
Wichtig:
try_files $uri $uri/ /index.html;
Das ist kritisch für Client-Side Routing. User navigiert zu
/products/123 → Server hat keine
products/123.html → Ohne try_files gibt’s 404.
Mit try_files → Server sendet index.html →
React Router übernimmt.
Deploy:
# Build
npm run build
# Upload zu Server
scp -r dist/* user@server:/var/www/myapp/dist/
# Nginx reload
ssh user@server 'sudo systemctl reload nginx'# dist/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Wenn File existiert, liefere es aus
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Sonst: index.html
RewriteRule . /index.html [L]
</IfModule>
# Cache Headers
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>Deploy auf Shared Hosting:
npm run build
# FTP Upload von dist/ zu public_html/# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200Deploy:
# Via Netlify CLI
netlify deploy --prod
# Oder: Git Push → Auto-Deploy
git push origin mainNetlify regelt automatisch: - HTTPS - CDN - Client-Side Routing - Atomic Deploys - Rollbacks
// vercel.json
{
"rewrites": [
{ "source": "/(.*)", "destination": "/" }
]
}Deploy:
vercel --prod# Build
npm run build
# Upload zu S3
aws s3 sync dist/ s3://my-react-app-bucket --delete
# Invalidate CloudFront Cache
aws cloudfront create-invalidation --distribution-id E1234567890 --paths "/*"S3 Bucket Configuration:
index.htmlindex.html (für Client-Side
Routing!)CloudFront Distribution:
/index.html (Status
200)npm install --save-dev gh-pages// package.json
{
"homepage": "https://username.github.io/repo-name",
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d dist"
}
}npm run deployVite Config für GitHub Pages:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
base: '/repo-name/', // Wichtig für Subpath!
});Problem: User bookmarked /products/123.
Bei direktem Aufruf:
Browser: GET https://example.com/products/123
Server: "Ich habe keine products/123.html" → 404
React Router kann nicht laufen, weil JS nie geladen wurde.
Lösung: Server muss immer
index.html ausliefern, egal welcher Path.
Verschiedene Server-Konfigurationen:
| Server | Konfiguration |
|---|---|
| Nginx | try_files $uri $uri/ /index.html; |
| Apache | .htaccess mit
RewriteRule . /index.html [L] |
| Express | app.get('*', (req, res) => res.sendFile('index.html')) |
| Netlify | [[redirects]] in netlify.toml |
| Vercel | rewrites in vercel.json |
| S3 | Error Document = index.html |
Build-Output hat Hash im Dateinamen:
index-a3f2d8b9.js
index-f7c2e1a4.css
Grund: Browser-Caching.
Ohne Hash:
<script src="/main.js"></script>Browser cached main.js. Neues Deployment →
main.js ändert sich. Browser nutzt alte gecachte Version.
User sieht veralteten Code.
Mit Hash:
<script src="/assets/index-a3f2d8b9.js"></script>Code ändert sich → Hash ändert sich → Neuer Filename → Browser lädt neue Version.
Cache-Strategie:
# Hashed Assets: Cache Forever
location ~* \.[a-f0-9]{8}\.(js|css)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML: Never Cache
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
HTML wird nie gecached (enthält neue Hashes). JS/CSS mit Hash werden ewig gecached.
npm run build
npm run previewVite startet Server für dist/ – simuliert
Production-Verhalten lokal. Kein HMR, kein Dev-Server, nur Static File
Serving.
Alternativ: Globaler Static Server
npm install -g serve
serve -s dist -p 3000CSR (Client-Side Rendering) – Standard React: - Server sendet leeres HTML - JavaScript rendert UI im Browser - Vite, Create React App → CSR
SSR (Server-Side Rendering) – Next.js, Remix: - Server rendert HTML mit Daten - JavaScript hydrated im Browser - Braucht Node.js-Server in Production
SSG (Static Site Generation) – Next.js, Gatsby: - HTML wird zur Build-Zeit generiert - Jede Route → Separate HTML-Datei - Static Hosting möglich
Dieses Kapitel behandelt CSR – klassisches React ohne SSR.
Fehler 1: Hardcoded localhost URLs
// ❌ Falsch: Development-URL hardcoded
const API_URL = 'http://localhost:3000/api';
fetch(`${API_URL}/users`);
// In Production: Keine API auf localhost!
// ✓ Richtig: Environment-Variable
const API_URL = import.meta.env.VITE_API_URL;Fehler 2: Dev-Dependencies in Production
// ❌ Falsch: React in devDependencies
{
"devDependencies": {
"react": "^18.2.0" // React IST Production-Dependency!
}
}
// ✓ Richtig
{
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.0.0"
}
}Build-Tools in devDependencies, alles was im Bundle
landet in dependencies.
Fehler 3: Source Maps in Production
// vite.config.ts
export default defineConfig({
build: {
sourcemap: false // ✓ In Production keine Source Maps
}
});Source Maps zeigen original Code → Security Risk, größere Files.
Fehler 4: Console.logs vergessen
// ❌ Debug-Logs in Production
console.log('User data:', userData);
console.log('API Response:', response);
// ✓ Conditional Logging
if (import.meta.env.DEV) {
console.log('Debug:', data);
}
// ✓ Oder: Strip beim Build (Vite macht das automatisch für console.log)Fehler 5: Absolute Paths ohne Base
// ❌ Falsch: Absolute Path ohne Base-Config
<img src="/logo.svg" />
// Funktioniert auf https://example.com/
// Bricht auf https://example.com/subpath/
// ✓ Richtig: Relative oder mit base
<img src="./logo.svg" />
// Oder vite.config.ts
export default defineConfig({
base: '/subpath/'
});React-Apps sind statischer Web-Content. Kein Node.js in Production, kein komplexes Server-Setup. HTML + JS + CSS, deploybar auf jeden Webserver weltweit. Das macht React-Deployments simpel, günstig und skalierbar – aber Client-Side Routing braucht Server-Config, und Environment-Management ist kritisch.