Performance ist User Experience
Eine Sekunde längere Ladezeit = 7% weniger Conversions. Das ist nicht nur Theorie - ich habe es in Production gesehen.
Hier sind 10 Quick Wins, die ich bei jedem Projekt umsetze, um Websites spürbar schneller zu machen.
1. Lazy Loading für Bilder
Vorher:
<!-- Alle Bilder werden sofort geladen, auch off-screen -->
<img src="hero.jpg" alt="Hero Image">
<img src="feature1.jpg" alt="Feature 1">
<img src="feature2.jpg" alt="Feature 2">
Nachher:
<!-- Native Lazy Loading - Browser entscheidet wann -->
<img src="hero.jpg" alt="Hero Image" loading="eager">
<img src="feature1.jpg" alt="Feature 1" loading="lazy">
<img src="feature2.jpg" alt="Feature 2" loading="lazy">
Resultat: Initial Page Load um 60% schneller!
2. Moderne Bildformate verwenden
<picture>
<!-- Browser wählt das beste Format -->
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
Savings:
- AVIF: ~50% kleiner als JPEG
- WebP: ~30% kleiner als JPEG
Automatisierung mit build tools:
// vite.config.js
import { imagetools } from 'vite-imagetools';
export default {
plugins: [
imagetools({
defaultDirectives: new URLSearchParams({
format: 'avif;webp;jpg',
quality: '80'
})
})
]
}
3. Code Splitting und Dynamic Imports
Problem:
// Alles wird upfront geladen
import { HugeChart } from './huge-chart-library'; // 500kb!
import { RarelyUsedFeature } from './rarely-used';
function App() {
return <Dashboard />;
}
Bundle Size: 800kb
Lösung:
// Lazy load nur wenn gebraucht
const HugeChart = lazy(() => import('./huge-chart-library'));
const RarelyUsedFeature = lazy(() => import('./rarely-used'));
function App() {
const [showChart, setShowChart] = useState(false);
return (
<Dashboard>
{showChart && (
<Suspense fallback={<Spinner />}>
<HugeChart />
</Suspense>
)}
</Dashboard>
);
}
Bundle Size: 150kb initial, Rest on-demand!
4. Debouncing und Throttling
Problem: Zu viele API Calls
// Search Input triggert bei jedem Keystroke
function SearchBox() {
const handleSearch = async (query) => {
const results = await api.search(query);
setResults(results);
};
return <input onChange={(e) => handleSearch(e.target.value)} />;
}
// User tippt "React" → 5 API Calls!
Lösung: Debouncing
import { debounce } from 'lodash';
function SearchBox() {
const handleSearch = debounce(async (query) => {
const results = await api.search(query);
setResults(results);
}, 300); // Warte 300ms nach letztem Keystroke
return <input onChange={(e) => handleSearch(e.target.value)} />;
}
// User tippt "React" → 1 API Call!
Custom Debounce Hook:
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// Usage:
const searchTerm = useDebounce(inputValue, 500);
useEffect(() => {
if (searchTerm) {
searchAPI(searchTerm);
}
}, [searchTerm]);
5. Resource Hints
<!-- Preconnect: Verbinde früh zu externen Domains -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://api.example.com">
<!-- DNS Prefetch: Nur DNS lookup -->
<link rel="dns-prefetch" href="https://analytics.google.com">
<!-- Preload: Kritische Ressourcen früh laden -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- Prefetch: Niedrige Priorität für nächste Seite -->
<link rel="prefetch" href="/about-page-bundle.js">
Impact: Spart 100-300ms bei externen Requests!
6. Virtualisierung für lange Listen
Problem:
// 10.000 Items rendern
function UserList({ users }) {
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
// 10.000 DOM Nodes = Browser stirbt
Lösung: react-window
import { FixedSizeList } from 'react-window';
function UserList({ users }) {
const Row = ({ index, style }) => (
<div style={style}>
<UserCard user={users[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={users.length}
itemSize={80}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Nur sichtbare Items werden gerendert!
Resultat: 10.000 Items fühlen sich wie 20 an!
7. Memoization und React.memo
Problem:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild data={staticData} />
{/* Re-rendert bei jedem Click! */}
</>
);
}
Lösung:
// Child rendert nur bei Props-Change
const ExpensiveChild = React.memo(({ data }) => {
console.log('Rendering ExpensiveChild');
return <div>{/* expensive rendering */}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
// useMemo für berechnete Werte
const processedData = useMemo(() => {
return expensiveCalculation(staticData);
}, [staticData]);
// useCallback für Funktionen
const handleClick = useCallback(() => {
doSomething();
}, []);
return (
<>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild
data={processedData}
onClick={handleClick}
/>
</>
);
}
8. Web Workers für CPU-intensive Tasks
// main.js
const worker = new Worker('/worker.js');
worker.postMessage({ data: largeDataset, operation: 'sort' });
worker.onmessage = (e) => {
console.log('Sorted data:', e.data);
// UI bleibt responsive!
};
// worker.js
self.onmessage = (e) => {
const { data, operation } = e.data;
if (operation === 'sort') {
const sorted = data.sort((a, b) => a - b);
self.postMessage(sorted);
}
};
Use Cases:
- Große Daten sortieren/filtern
- Komplexe Berechnungen
- Bild-/Video-Processing
- Parsing großer Files
9. Compression und Minification
Server-side:
// Express.js mit Compression
const compression = require('compression');
app.use(compression());
// Nginx
gzip on;
gzip_types text/css application/javascript image/svg+xml;
gzip_min_length 1000;
// Brotli (besser als gzip)
brotli on;
brotli_types text/css application/javascript;
Savings: 70-80% kleinere File Sizes!
Build-time Optimization:
// Vite Production Build
export default {
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.logs
drop_debugger: true
}
}
}
}
10. Caching Strategien
HTTP Caching:
// Express.js
app.use(express.static('public', {
maxAge: '1y', // Static assets
immutable: true
}));
// Nginx
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Service Worker Caching:
// sw.js
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
Monitoring: Messen ist wichtig!
// Web Vitals tracken
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
console.log(metric);
// Send to your analytics
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Tools die ich täglich nutze:
- Lighthouse - Chrome DevTools
- WebPageTest - Detaillierte Analysen
- Bundle Analyzer - Bundle Size tracken
- Chrome Performance Tab - Profiling
Zusammenfassung
Diese 10 Quick Wins haben bei meinen Projekten:
- 3-5s schnellere Ladezeiten
- 60% kleinere Bundle Sizes
- Bessere Core Web Vitals
- Zufriedenere User
Start mit dem Größten Impact:
- Lazy Loading für Bilder
- Code Splitting
- Modern Image Formats
- Compression
Dann optimiere weiter basierend auf Messungen!
Ressourcen
Welche Performance-Optimierungen nutzt du? Teile deine Tipps! ⚡