15 KiB
LiveComponents Performance Guide
Complete performance optimization guide for LiveComponents covering caching, batching, middleware, and optimization techniques.
Table of Contents
- Caching Strategies
- Request Batching
- Middleware Performance
- Debounce & Throttle
- Lazy Loading
- Island Components
- Fragment Updates
- SSE Optimization
- Memory Management
- Performance Checklist
Caching Strategies
Component-Level Caching
Implement Cacheable interface for automatic caching:
#[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):
public function getVaryBy(): ?VaryBy
{
return VaryBy::none(); // Same cache for everyone
}
Per-User Cache:
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId(); // Different cache per user
}
Per-User Per-Locale Cache:
public function getVaryBy(): ?VaryBy
{
return VaryBy::userAndLocale(); // Different cache per user and locale
}
With Feature Flags:
public function getVaryBy(): ?VaryBy
{
return VaryBy::userId()
->withFeatureFlags(['new-ui', 'dark-mode']);
}
Stale-While-Revalidate (SWR)
Serve stale content while refreshing:
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:
// Invalidate all user-stats caches
$cache->invalidateTags(['user-stats']);
// Invalidate specific user's cache
$cache->invalidateTags(['user-stats', 'user:123']);
Manual Invalidation:
$cacheKey = CacheKey::fromString('user-stats:123');
$cache->delete($cacheKey);
Request Batching
Client-Side Batching
Batch multiple operations in a single request:
// 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:
// 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:
#[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:
#[Middleware(LoggingMiddleware::class, priority: 50)]
CachingMiddleware - Caches action responses:
#[Middleware(CachingMiddleware::class, priority: 100)]
RateLimitMiddleware - Rate limits actions:
#[Middleware(RateLimitMiddleware::class, priority: 200)]
Custom Middleware
Create custom middleware for specific needs:
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:
#[Middleware(LoggingMiddleware::class, priority: 50)] // Executes first
#[Middleware(CachingMiddleware::class, priority: 100)] // Executes second
#[Middleware(RateLimitMiddleware::class, priority: 200)] // Executes third
Execution Order:
- RateLimitMiddleware (priority: 200)
- CachingMiddleware (priority: 100)
- LoggingMiddleware (priority: 50)
- 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:
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:
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:
#[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:
// 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:
#[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:
#[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:
<!-- 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:
// 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:
#[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:
LIVECOMPONENT_SSE_HEARTBEAT=15 # Seconds
Connection Timeout:
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:
// ✅ 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:
#[Action]
public function clearCache(): CounterState
{
// Remove cached data from state
return $this->state->withoutCache();
}
Memory Monitoring
Monitor component memory usage:
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
Cacheableinterface 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
VaryByfor 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
#[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
#[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
// 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 best practices
- End-to-End Guide - Complete development guide
- API Reference - Complete API documentation