Some checks failed
Deploy Application / deploy (push) Has been cancelled
698 lines
15 KiB
Markdown
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
|
|
|
|
|
|
|
|
|
|
|