Files
michaelschiemer/docs/livecomponents/performance-guide-complete.md
2025-11-24 21:28:25 +01:00

698 lines
15 KiB
Markdown

# LiveComponents Performance Guide
**Complete performance optimization guide for LiveComponents covering caching, batching, middleware, and optimization techniques.**
---
## Table of Contents
1. [Caching Strategies](#caching-strategies)
2. [Request Batching](#request-batching)
3. [Middleware Performance](#middleware-performance)
4. [Debounce & Throttle](#debounce--throttle)
5. [Lazy Loading](#lazy-loading)
6. [Island Components](#island-components)
7. [Fragment Updates](#fragment-updates)
8. [SSE Optimization](#sse-optimization)
9. [Memory Management](#memory-management)
10. [Performance Checklist](#performance-checklist)
---
## Caching Strategies
### Component-Level Caching
Implement `Cacheable` interface for automatic caching:
```php
#[LiveComponent('user-stats')]
final readonly class UserStatsComponent implements LiveComponentContract, Cacheable
{
public function getCacheKey(): string
{
return 'user-stats:' . $this->state->userId;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5);
}
public function shouldCache(): bool
{
return true;
}
public function getCacheTags(): array
{
return ['user-stats', 'user:' . $this->state->userId];
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId();
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1);
}
}
```
### Cache Key Variations
**Global Cache** (same for all users):
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::none(); // Same cache for everyone
}
```
**Per-User Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different cache per user
}
```
**Per-User Per-Locale Cache**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userAndLocale(); // Different cache per user and locale
}
```
**With Feature Flags**:
```php
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId()
->withFeatureFlags(['new-ui', 'dark-mode']);
}
```
### Stale-While-Revalidate (SWR)
Serve stale content while refreshing:
```php
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5); // Fresh for 5 minutes
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1); // Serve stale for 1 hour while refreshing
}
```
**How it works**:
- 0-5min: Serve fresh cache
- 5min-1h: Serve stale cache + trigger background refresh
- >1h: Force fresh render
### Cache Invalidation
**By Tags**:
```php
// Invalidate all user-stats caches
$cache->invalidateTags(['user-stats']);
// Invalidate specific user's cache
$cache->invalidateTags(['user-stats', 'user:123']);
```
**Manual Invalidation**:
```php
$cacheKey = CacheKey::fromString('user-stats:123');
$cache->delete($cacheKey);
```
---
## Request Batching
### Client-Side Batching
Batch multiple operations in a single request:
```javascript
// Batch multiple actions
const response = await LiveComponent.executeBatch([
{
componentId: 'counter:demo',
method: 'increment',
params: { amount: 5 },
fragments: ['counter-display']
},
{
componentId: 'stats:user-123',
method: 'refresh'
},
{
componentId: 'notifications:user-123',
method: 'markAsRead',
params: { notificationId: 'abc' }
}
]);
```
### Server-Side Processing
The framework automatically processes batch requests:
```php
// BatchProcessor handles multiple operations
$batchRequest = new BatchRequest(...$operations);
$response = $batchProcessor->process($batchRequest);
// Returns:
// {
// "success": true,
// "results": [
// { "componentId": "counter:demo", "success": true, "html": "...", "fragments": {...} },
// { "componentId": "stats:user-123", "success": true, "html": "..." },
// { "componentId": "notifications:user-123", "success": true, "html": "..." }
// ],
// "totalOperations": 3,
// "successCount": 3,
// "failureCount": 0
// }
```
### Batch Benefits
- **Reduced HTTP Requests**: Multiple operations in one request
- **Lower Latency**: Single round-trip instead of multiple
- **Atomic Operations**: All succeed or all fail
- **Better Performance**: Especially on slow networks
---
## Middleware Performance
### Middleware Overview
Middleware allows you to intercept and transform component actions:
```php
#[LiveComponent('user-profile')]
#[Middleware(LoggingMiddleware::class)]
#[Middleware(CachingMiddleware::class, priority: 50)]
final readonly class UserProfileComponent implements LiveComponentContract
{
// All actions have Logging + Caching middleware
}
```
### Built-in Middleware
**LoggingMiddleware** - Logs actions with timing:
```php
#[Middleware(LoggingMiddleware::class, priority: 50)]
```
**CachingMiddleware** - Caches action responses:
```php
#[Middleware(CachingMiddleware::class, priority: 100)]
```
**RateLimitMiddleware** - Rate limits actions:
```php
#[Middleware(RateLimitMiddleware::class, priority: 200)]
```
### Custom Middleware
Create custom middleware for specific needs:
```php
final readonly class PerformanceMonitoringMiddleware implements ComponentMiddlewareInterface
{
public function handle(
LiveComponentContract $component,
string $action,
ActionParameters $params,
callable $next
): ComponentUpdate {
$startTime = microtime(true);
$startMemory = memory_get_usage();
$result = $next($component, $action, $params);
$duration = microtime(true) - $startTime;
$memoryUsed = memory_get_usage() - $startMemory;
// Log performance metrics
$this->metricsCollector->record($component::class, $action, $duration, $memoryUsed);
return $result;
}
}
```
### Middleware Priority
Higher priority = earlier execution:
```php
#[Middleware(LoggingMiddleware::class, priority: 50)] // Executes first
#[Middleware(CachingMiddleware::class, priority: 100)] // Executes second
#[Middleware(RateLimitMiddleware::class, priority: 200)] // Executes third
```
**Execution Order**:
1. RateLimitMiddleware (priority: 200)
2. CachingMiddleware (priority: 100)
3. LoggingMiddleware (priority: 50)
4. Action execution
### Middleware Performance Tips
**DO**:
- Use caching middleware for expensive operations
- Use logging middleware for debugging (disable in production)
- Keep middleware lightweight
- Use priority to optimize execution order
**DON'T**:
- Add unnecessary middleware
- Perform heavy operations in middleware
- Cache everything (be selective)
- Use middleware for business logic
---
## Debounce & Throttle
### Client-Side Debouncing
Debounce user input to reduce requests:
```javascript
let searchTimeout;
const searchInput = document.querySelector('[data-live-action="search"]');
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
LiveComponent.executeAction('search:demo', 'search', {
query: e.target.value
});
}, 300); // 300ms debounce
});
```
### Client-Side Throttling
Throttle frequent actions:
```javascript
let lastExecution = 0;
const throttleDelay = 1000; // 1 second
function throttledAction() {
const now = Date.now();
if (now - lastExecution < throttleDelay) {
return; // Skip if too soon
}
lastExecution = now;
LiveComponent.executeAction('component:id', 'action', {});
}
```
### Server-Side Rate Limiting
Use `#[Action]` attribute with rate limit:
```php
#[Action(rateLimit: 10)] // 10 requests per minute
public function search(string $query): State
{
return $this->state->withResults($this->searchService->search($query));
}
```
---
## Lazy Loading
### Lazy Component Loading
Load components when entering viewport:
```php
// In template
{{ lazy_component('notification-center:user-123', [
'priority' => 'high',
'threshold' => '0.1',
'placeholder' => 'Loading notifications...'
]) }}
```
**Options**:
- `priority`: `'high'` | `'normal'` | `'low'`
- `threshold`: `'0.0'` to `'1.0'` (viewport intersection threshold)
- `placeholder`: Custom loading text
### Lazy Island Components
Use `#[Island]` for isolated lazy loading:
```php
#[LiveComponent('heavy-widget')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')]
final readonly class HeavyWidgetComponent implements LiveComponentContract
{
// Component implementation
}
```
**Benefits**:
- Reduces initial page load time
- Loads only when needed
- Isolated rendering (no template overhead)
---
## Island Components
### Island Directive
Isolate resource-intensive components:
```php
#[LiveComponent('analytics-dashboard')]
#[Island(isolated: true, lazy: true)]
final readonly class AnalyticsDashboardComponent implements LiveComponentContract
{
// Heavy component with complex calculations
}
```
**Features**:
- **Isolated Rendering**: Separate from main template
- **Lazy Loading**: Load on viewport entry
- **Independent Updates**: No parent component re-renders
### When to Use Islands
**Use Islands for**:
- Heavy calculations
- External API calls
- Complex data visualizations
- Third-party widgets
- Below-the-fold content
**Don't use Islands for**:
- Simple components
- Above-the-fold content
- Components that need parent state
- Frequently updated components
---
## Fragment Updates
### Partial Rendering
Update only specific parts of a component:
```html
<!-- Template -->
<div data-lc-fragment="counter-display">
<h2>Count: {count}</h2>
</div>
<div data-lc-fragment="actions">
<button data-live-action="increment" data-lc-fragments="counter-display">
Increment
</button>
</div>
```
**Client-side**:
```javascript
// Only counter-display fragment is updated
LiveComponent.executeAction('counter:demo', 'increment', {}, {
fragments: ['counter-display']
});
```
### Fragment Benefits
- **Reduced DOM Updates**: Only update what changed
- **Better Performance**: Less re-rendering overhead
- **Smoother UX**: Faster perceived performance
- **Lower Bandwidth**: Smaller response payloads
---
## SSE Optimization
### Server-Sent Events
Real-time updates via SSE:
```php
#[LiveComponent('live-feed')]
final readonly class LiveFeedComponent implements LiveComponentContract, Pollable
{
public function getPollInterval(): int
{
return 5000; // Poll every 5 seconds
}
public function poll(): LiveComponentState
{
// Fetch latest data
$newItems = $this->feedService->getLatest();
return $this->state->withItems($newItems);
}
}
```
### SSE Configuration
**Heartbeat Interval**:
```env
LIVECOMPONENT_SSE_HEARTBEAT=15 # Seconds
```
**Connection Timeout**:
```env
LIVECOMPONENT_SSE_TIMEOUT=300 # Seconds
```
### SSE Best Practices
**DO**:
- Use SSE for real-time updates
- Set appropriate poll intervals
- Handle reconnection gracefully
- Monitor connection health
**DON'T**:
- Poll too frequently (< 1 second)
- Keep connections open indefinitely
- Ignore connection errors
- Use SSE for one-time operations
---
## Memory Management
### Component State Size
Keep component state minimal:
```php
// ✅ GOOD: Minimal state
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0
) {}
}
// ❌ BAD: Large state
final readonly class CounterState extends ComponentState
{
public function __construct(
public int $count = 0,
public array $largeDataSet = [], // Don't store large data in state
public string $hugeString = '' // Don't store large strings
) {}
}
```
### State Cleanup
Clean up unused state:
```php
#[Action]
public function clearCache(): CounterState
{
// Remove cached data from state
return $this->state->withoutCache();
}
```
### Memory Monitoring
Monitor component memory usage:
```php
final readonly class MemoryMonitoringMiddleware implements ComponentMiddlewareInterface
{
public function handle($component, $action, $params, $next): ComponentUpdate
{
$startMemory = memory_get_usage();
$result = $next($component, $action, $params);
$endMemory = memory_get_usage();
if ($endMemory - $startMemory > 1024 * 1024) { // > 1MB
error_log("Memory spike in {$component::class}::{$action}: " . ($endMemory - $startMemory));
}
return $result;
}
}
```
---
## Performance Checklist
### Component Development
- [ ] Use caching for expensive operations
- [ ] Implement `Cacheable` interface where appropriate
- [ ] Use fragments for partial updates
- [ ] Use lazy loading for below-the-fold content
- [ ] Use islands for heavy components
- [ ] Keep component state minimal
- [ ] Use batch requests for multiple operations
- [ ] Debounce/throttle user input
- [ ] Set appropriate rate limits
### Middleware
- [ ] Use caching middleware for expensive actions
- [ ] Use logging middleware for debugging (disable in production)
- [ ] Keep middleware lightweight
- [ ] Set appropriate middleware priorities
- [ ] Monitor middleware performance
### Caching
- [ ] Configure cache TTL appropriately
- [ ] Use cache tags for grouped invalidation
- [ ] Use `VaryBy` for user-specific caching
- [ ] Use SWR for better perceived performance
- [ ] Monitor cache hit rates
### Testing
- [ ] Test component performance under load
- [ ] Monitor memory usage
- [ ] Test with slow network conditions
- [ ] Test batch request performance
- [ ] Test fragment update performance
---
## Performance Patterns
### Pattern 1: Cached Search Component
```php
#[LiveComponent('search')]
#[Middleware(CachingMiddleware::class, priority: 100)]
final readonly class SearchComponent implements LiveComponentContract, Cacheable
{
#[Action(rateLimit: 20)]
public function search(string $query): SearchState
{
if (strlen($query) < 3) {
return $this->state->withResults([]);
}
$results = $this->searchService->search($query);
return $this->state->withQuery($query)->withResults($results);
}
public function getCacheKey(): string
{
return 'search:' . md5($this->state->query);
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(10);
}
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different results per user
}
}
```
### Pattern 2: Lazy-Loaded Dashboard
```php
#[LiveComponent('dashboard')]
#[Island(isolated: true, lazy: true, placeholder: 'Loading dashboard...')]
final readonly class DashboardComponent implements LiveComponentContract, Cacheable
{
public function getCacheKey(): string
{
return 'dashboard:' . $this->state->userId;
}
public function getCacheTTL(): Duration
{
return Duration::fromMinutes(5);
}
public function getStaleWhileRevalidate(): ?Duration
{
return Duration::fromHours(1); // Serve stale for 1 hour
}
}
```
### Pattern 3: Batch Operations
```javascript
// Client-side: Batch multiple updates
const updates = [
{ componentId: 'counter:1', method: 'increment', params: {} },
{ componentId: 'counter:2', method: 'increment', params: {} },
{ componentId: 'counter:3', method: 'increment', params: {} },
];
const response = await LiveComponent.executeBatch(updates);
```
---
## Next Steps
- [Security Guide](security-guide-complete.md) - Security best practices
- [End-to-End Guide](end-to-end-guide.md) - Complete development guide
- [API Reference](api-reference-complete.md) - Complete API documentation