# LiveComponents Performance Guide Comprehensive guide to optimizing LiveComponent performance through caching, batching, fragment updates, and advanced patterns. ## Table of Contents - [Overview](#overview) - [Fragment-Based Rendering](#fragment-based-rendering) - [Component Caching](#component-caching) - [Advanced Caching: varyBy](#advanced-caching-varyby) - [Stale-While-Revalidate (SWR)](#stale-while-revalidate-swr) - [Request Batching](#request-batching) - [Polling Optimization](#polling-optimization) - [Performance Monitoring](#performance-monitoring) - [Best Practices](#best-practices) --- ## Overview LiveComponents provides multiple performance optimization strategies: 1. **Fragment Rendering** - Update only changed parts of the DOM (60-90% bandwidth reduction) 2. **Component Caching** - Cache rendered components with intelligent invalidation 3. **VaryBy Context** - Cache variations based on user, locale, feature flags 4. **Stale-While-Revalidate** - Serve stale data while refreshing in background 5. **Request Batching** - Combine multiple actions into single request 6. **Optimized Polling** - Intelligent intervals with backoff strategies ### Performance Impact | Optimization | Bandwidth Reduction | Latency Reduction | User Experience | |-------------|---------------------|-------------------|-----------------| | Fragment Updates | 60-90% | 50-80% | Instant updates | | Component Caching | 95-99% | 90-95% | Near-instant loads | | VaryBy Context | +10-20% | Minimal | Personalized caching | | SWR Pattern | Variable | 100% (perceived) | Zero waiting | | Request Batching | 40-70% | 60-80% | Fewer requests | --- ## Fragment-Based Rendering ### What Are Fragments? Fragments are **marked sections** of your component template that can be updated independently: ```html

Statistics

Total: {stats.total}

Active: {stats.active}

Recent Activity

{notification_count}
``` ### Using Fragments **Automatic Fragment Updates**: ```html ``` **JavaScript Fragment Updates**: ```javascript // Update specific fragments await liveComponent.executeAction('refreshStats', {}, { fragments: ['user-stats'] }); // Update multiple fragments await liveComponent.executeAction('updateDashboard', {}, { fragments: ['user-stats', 'recent-activity', 'notifications'] }); ``` ### Backend Fragment Support ```php use App\Framework\LiveComponents\Rendering\FragmentRenderer; #[LiveComponent('dashboard')] final readonly class DashboardComponent implements LiveComponentContract { public function __construct( public ComponentId $id, public DashboardState $state, private FragmentRenderer $fragmentRenderer ) {} #[Action] public function refreshStats(): DashboardState { // Update only stats in state $newStats = $this->statsService->getLatest(); return $this->state->withStats($newStats); } // Framework automatically extracts requested fragments // No manual fragment handling needed! } ``` ### Fragment Performance Characteristics **Bandwidth Reduction**: ``` Full Render: 50 KB HTML Fragment Update: 5 KB HTML (90% reduction) Full Render: 20 KB HTML (dashboard) Fragment Update: 2 KB HTML (stats only) (90% reduction) Full Render: 100 KB HTML (product list) Fragment Update: 8 KB HTML (grid only) (92% reduction) ``` **DOM Operations**: ``` Full Render: 500 nodes replaced Fragment Update: 30 nodes replaced (94% reduction) Full Render: 1000 nodes traversed Fragment Update: 100 nodes traversed (90% reduction) ``` **Perceived Performance**: - **Full Render**: 200-500ms (visible flash, scroll reset) - **Fragment Update**: 50-100ms (smooth, no interruption) ### Fragment Best Practices **1. Identify Independent Sections** ```html
``` **2. Use Fragments for High-Frequency Updates** ```html
${current_price} {change}%
``` **3. Combine Related Updates** ```javascript // ✅ Good - Update related fragments together liveComponent.executeAction('updateDashboard', {}, { fragments: ['stats', 'chart', 'summary'] }); // ❌ Bad - Multiple separate requests liveComponent.executeAction('updateStats'); liveComponent.executeAction('updateChart'); liveComponent.executeAction('updateSummary'); ``` --- ## Component Caching ### Enable Caching ```php use App\Framework\LiveComponents\Attributes\Action; use App\Framework\LiveComponents\Contracts\Cacheable; use App\Framework\Core\ValueObjects\Duration; #[LiveComponent('product-list')] final readonly class ProductListComponent implements LiveComponentContract, Cacheable { // Implement Cacheable interface public function getCacheTTL(): Duration { return Duration::fromMinutes(5); } public function getCacheKey(): string { return "products-{$this->state->categoryId}"; } public function shouldCache(): bool { // Don't cache for admin users return !$this->state->isAdmin; } #[Action] public function loadProducts(): ProductListState { // Expensive database query $products = $this->productRepository->findByCategory( $this->state->categoryId ); return $this->state->withProducts($products); } } ``` ### Cache Invalidation **Manual Invalidation**: ```php use App\Framework\Cache\Cache; use App\Framework\Cache\CacheKey; public function updateProduct(Product $product): void { $this->productRepository->save($product); // Invalidate component cache $this->cache->forget( CacheKey::fromString("livecomponent:product-list:products-{$product->categoryId}") ); } ``` **Event-Based Invalidation**: ```php use App\Framework\Event\EventHandler; #[EventHandler] final readonly class InvalidateProductCacheHandler { public function handle(ProductUpdatedEvent $event): void { // Invalidate all product list caches for this category $this->cache->invalidateTag("product-category-{$event->categoryId}"); } } ``` ### Cache Tags ```php public function getCacheTags(): array { return [ "product-category-{$this->state->categoryId}", "user-{$this->state->userId}", 'products' ]; } // Invalidate all products $this->cache->invalidateTag('products'); // Invalidate specific category $this->cache->invalidateTag('product-category-123'); ``` --- ## Advanced Caching: varyBy ### What is varyBy? **varyBy** allows you to cache different versions of the same component based on context: - **User ID**: Different cache per user - **Locale**: Different cache per language - **Roles**: Different cache per permission level - **Feature Flags**: Different cache when features enabled - **Custom**: Any custom factor (theme, region, etc.) ### Basic varyBy Usage ```php use App\Framework\LiveComponents\Contracts\Cacheable; use App\Framework\LiveComponents\Caching\VaryBy; use App\Framework\Core\ValueObjects\Duration; #[LiveComponent('user-dashboard')] final readonly class UserDashboardComponent implements LiveComponentContract, Cacheable { public function getCacheTTL(): Duration { return Duration::fromMinutes(10); } public function getCacheKey(): string { return 'dashboard'; } // Cache varies by user ID and locale public function getVaryBy(): ?VaryBy { return VaryBy::userAndLocale(); } // Results in cache keys like: // livecomponent:user-dashboard:dashboard:u:123:l:en // livecomponent:user-dashboard:dashboard:u:456:l:de } ``` ### VaryBy Factory Methods ```php // No variation - global cache VaryBy::none(); // Vary by user ID only VaryBy::userId(); // Vary by locale only VaryBy::locale(); // Vary by user and locale (most common) VaryBy::userAndLocale(); // Vary by everything VaryBy::all(); // Vary by feature flags VaryBy::featureFlags(['new-ui', 'dark-mode']); // Vary by custom factors VaryBy::custom(['theme', 'region']); ``` ### Fluent VaryBy Builder ```php public function getVaryBy(): ?VaryBy { return VaryBy::none() ->withUserId() // Add user variation ->withLocale() // Add locale variation ->withFeatureFlags([ // Add feature flag variation 'new-ui', 'dark-mode', 'beta-features' ]) ->withCustom(['theme']); // Add custom variation } // Results in: // livecomponent:dashboard:main:u:123:l:en:f:new-ui,dark-mode:c:theme=dark ``` ### VaryBy Examples **User-Specific Dashboard**: ```php #[LiveComponent('user-dashboard')] final readonly class UserDashboardComponent implements Cacheable { public function getVaryBy(): ?VaryBy { return VaryBy::userId(); } // Each user gets their own cached version // User 123: livecomponent:user-dashboard:main:u:123 // User 456: livecomponent:user-dashboard:main:u:456 } ``` **Localized Content**: ```php #[LiveComponent('product-catalog')] final readonly class ProductCatalogComponent implements Cacheable { public function getVaryBy(): ?VaryBy { return VaryBy::locale(); } // Each language gets its own cache // English: livecomponent:product-catalog:products:l:en // German: livecomponent:product-catalog:products:l:de } ``` **Permission-Based Content**: ```php #[LiveComponent('admin-panel')] final readonly class AdminPanelComponent implements Cacheable { public function getVaryBy(): ?VaryBy { return VaryBy::roles(); } // Each role combination gets its own cache // Admin: livecomponent:admin-panel:main:r:admin // Moderator: livecomponent:admin-panel:main:r:moderator // Both: livecomponent:admin-panel:main:r:admin,moderator } ``` **Feature Flag Variations**: ```php #[LiveComponent('homepage')] final readonly class HomepageComponent implements Cacheable { public function getVaryBy(): ?VaryBy { return VaryBy::featureFlags(['new-design', 'personalized-feed']); } // Different cache for each feature combination // Old design: livecomponent:homepage:main:global // New design only: livecomponent:homepage:main:f:new-design // Both features: livecomponent:homepage:main:f:new-design,personalized-feed } ``` **Complex Multi-Factor Caching**: ```php #[LiveComponent('shopping-cart')] final readonly class ShoppingCartComponent implements Cacheable { public function getVaryBy(): ?VaryBy { return VaryBy::none() ->withUserId() // Different per user ->withLocale() // Different per language ->withFeatureFlags(['express-checkout']) // Different if feature enabled ->withCustom(['currency', 'region']); // Different per currency/region } // Results in keys like: // livecomponent:cart:main:u:123:l:en:f:express-checkout:c:currency=USD,region=US // livecomponent:cart:main:u:123:l:de:c:currency=EUR,region=DE } ``` ### CacheContext and Runtime Context The framework automatically provides `CacheContext` based on the current request: ```php // Framework automatically creates CacheContext from: $context = CacheContext::create( userId: $currentUser?->id, // From auth session locale: $request->getLocale(), // From request roles: $currentUser?->roles ?? [], // From user object featureFlags: $featureFlags->getActive(), // From feature service custom: [ 'theme' => $request->cookie->get('theme'), 'region' => $request->getRegion() ] ); // VaryBy then applies this context to generate cache key suffix $suffix = $varyBy->apply($context); ``` ### VaryBy Performance Impact **Cache Hit Rate**: ``` No varyBy (global): 95% hit rate, 1 cache entry varyBy userId: 85% hit rate, 1000 cache entries varyBy userId + locale: 75% hit rate, 3000 cache entries varyBy userId + locale + flags: 60% hit rate, 10000 cache entries ``` **Trade-offs**: - **More specific varyBy** = More cache entries = Lower hit rate = More storage - **Less specific varyBy** = Fewer cache entries = Higher hit rate = Less personalization **Recommendation**: Start with `VaryBy::userId()` or `VaryBy::userAndLocale()`, add more factors only if needed. --- ## Stale-While-Revalidate (SWR) ### What is SWR? **Stale-While-Revalidate** serves cached content immediately (even if expired) while refreshing in the background: ``` Traditional: Request → Cache Miss → Database → Response (500ms) SWR: Request → Stale Cache → Response (10ms) + Background Refresh → Updated Cache ``` ### Enable SWR ```php use App\Framework\LiveComponents\Contracts\Cacheable; use App\Framework\Core\ValueObjects\Duration; #[LiveComponent('news-feed')] final readonly class NewsFeedComponent implements Cacheable { public function getCacheTTL(): Duration { return Duration::fromMinutes(5); // Fresh for 5 minutes } public function getStaleWhileRevalidate(): ?Duration { return Duration::fromMinutes(30); // Serve stale up to 30 minutes } public function getCacheKey(): string { return 'feed-latest'; } } ``` **How it works**: 1. **0-5 min**: Cache fresh → Serve from cache (instant) 2. **5-30 min**: Cache stale → Serve stale (instant) + refresh in background 3. **30+ min**: Cache expired → Fetch fresh → Update cache → Serve ### SWR with varyBy ```php public function getVaryBy(): ?VaryBy { return VaryBy::userId(); } public function getStaleWhileRevalidate(): ?Duration { return Duration::fromHours(1); } // Each user gets their own SWR cache: // User 123: Fresh for 5min, stale-ok for 1h // User 456: Fresh for 5min, stale-ok for 1h ``` ### SWR Metrics The framework provides metrics for SWR performance: ```php use App\Framework\LiveComponents\Caching\CacheMetrics; // After rendering component $metrics = $component->getCacheMetrics(); if ($metrics->isStale) { $this->logger->info('SWR: Served stale content', [ 'age_seconds' => $metrics->ageSeconds, 'is_refreshing' => $metrics->isRefreshing, 'cache_key' => $metrics->cacheKey ]); } ``` ### SWR Use Cases **✅ Perfect for SWR**: - News feeds (5min fresh, 1h stale-ok) - Product listings (10min fresh, 2h stale-ok) - User profiles (15min fresh, 24h stale-ok) - Dashboards (5min fresh, 30min stale-ok) **❌ Not suitable for SWR**: - Real-time stock prices - Shopping cart totals - Payment confirmations - Authentication status ### SWR Response Headers The framework automatically adds cache headers: ```http Cache-Control: max-age=300, stale-while-revalidate=1800 Age: 450 X-Cache-Status: STALE X-Cache-Refreshing: true ``` Client-side interpretation: ```javascript const cacheStatus = response.headers.get('X-Cache-Status'); if (cacheStatus === 'STALE') { // Show indicator: "Updating..." showRefreshIndicator(); // Listen for updated data liveComponent.on('cache:refreshed', (data) => { hideRefreshIndicator(); // Data automatically updated }); } ``` --- ## Request Batching ### What is Batching? Combine multiple component actions into a single HTTP request: ``` Without Batching: Action 1 → HTTP Request → Response (100ms) Action 2 → HTTP Request → Response (100ms) Action 3 → HTTP Request → Response (100ms) Total: 300ms + 3x overhead With Batching: Actions 1+2+3 → Single HTTP Request → Response (120ms) Total: 120ms + 1x overhead ``` ### Enable Batching ```javascript import { LiveComponent } from './live-component.js'; const component = new LiveComponent('dashboard:main', { batching: { enabled: true, maxBatchSize: 10, // Max actions per batch batchWindow: 50 // Wait 50ms to collect actions } }); // Execute multiple actions component.executeAction('updateStats'); component.executeAction('updateChart'); component.executeAction('updateNotifications'); // Framework automatically batches these into single request ``` ### Batching Strategies **Auto-Batching** (default): ```javascript // Framework detects rapid actions and batches automatically for (let i = 0; i < 5; i++) { liveComponent.executeAction('increment'); } // Sent as single batched request ``` **Manual Batching**: ```javascript liveComponent.batch([ { action: 'updateStats', params: {} }, { action: 'updateChart', params: { period: '7d' } }, { action: 'refreshNotifications', params: {} } ]); ``` **Conditional Batching**: ```javascript // Only batch if multiple actions pending const config = { batching: { enabled: true, minBatchSize: 2 // Only batch if 2+ actions } }; ``` ### Backend Batch Processing ```php use App\Framework\LiveComponents\Batching\BatchRequest; use App\Framework\LiveComponents\Batching\BatchResponse; // Framework automatically handles batched requests // Your component actions work the same way #[Action] public function updateStats(): DashboardState { // Normal action implementation // Framework handles batching transparently } ``` ### Batching Performance **Bandwidth Reduction**: ``` 3 separate requests: 3x overhead (headers, handshake) = ~2KB 1 batched request: 1x overhead = ~0.7KB (65% reduction) ``` **Latency Reduction**: ``` 3 sequential requests: 300ms 3 parallel requests: 100ms (but 3x connections) 1 batched request: 120ms (60% faster than sequential) ``` --- ## Polling Optimization ### Adaptive Polling Intervals ```php use App\Framework\LiveComponents\Contracts\Pollable; use App\Framework\Core\ValueObjects\Duration; #[LiveComponent('stock-ticker')] final readonly class StockTickerComponent implements LiveComponentContract, Pollable { public function poll(): StockTickerState { $price = $this->stockService->getCurrentPrice($this->state->symbol); return $this->state->withPrice($price); } public function getPollInterval(): int { // Adaptive interval based on market hours $isMarketOpen = $this->timeService->isMarketOpen(); return $isMarketOpen ? 5000 // 5 seconds during market hours : 60000; // 1 minute when market closed } } ``` ### Backoff Strategies ```javascript // Exponential backoff on errors const component = new LiveComponent('notifications:main', { polling: { enabled: true, interval: 10000, // Start with 10s maxInterval: 300000, // Max 5 minutes backoffMultiplier: 2, // Double on each error backoffReset: 60000 // Reset after 1min success } }); // After error: // Attempt 1: 10s // Attempt 2: 20s (error) // Attempt 3: 40s (error) // Attempt 4: 80s (error) // Attempt 5: 160s (error) // Attempt 6: 300s (capped at max) ``` ### Conditional Polling ```php public function shouldPoll(): bool { // Don't poll if user inactive if ($this->state->lastActivityAt < time() - 300) { return false; } // Don't poll if data is fresh if ($this->state->updatedAt > time() - 60) { return false; } return true; } ``` ### Visibility-Based Polling ```javascript // Stop polling when tab hidden, resume when visible document.addEventListener('visibilitychange', () => { if (document.hidden) { liveComponent.pausePolling(); } else { liveComponent.resumePolling(); } }); ``` --- ## Performance Monitoring ### Cache Metrics ```php use App\Framework\LiveComponents\Caching\CacheMetrics; // Log cache performance $this->logger->info('Component cached', [ 'hit' => $metrics->hit, 'age_seconds' => $metrics->ageSeconds, 'is_stale' => $metrics->isStale, 'freshness_percent' => $metrics->getFreshnessPercent(), 'cache_key' => $metrics->cacheKey ]); ``` ### Performance Tracking ```php use App\Framework\Performance\PerformanceCollector; #[Action] public function loadData(): ComponentState { $start = microtime(true); $data = $this->expensiveOperation(); $this->performance->track('component.action.load-data', [ 'duration_ms' => (microtime(true) - $start) * 1000, 'cache_hit' => $this->wasCached(), 'fragment_count' => count($this->getFragments()) ]); return $this->state->withData($data); } ``` ### Client-Side Performance ```javascript // Track render performance liveComponent.on('render:complete', (metrics) => { console.log('Render performance:', { duration: metrics.renderDuration, fragmentCount: metrics.fragmentsUpdated, nodesPatched: metrics.nodesPatched, wasCached: metrics.fromCache }); // Send to analytics analytics.track('livecomponent.render', metrics); }); ``` --- ## Best Practices ### 1. Use Fragments for Large Components ```html
``` ### 2. Cache Expensive Renders ```php // ✅ Good - Cache expensive database queries public function getCacheTTL(): Duration { return Duration::fromMinutes(5); } public function getVaryBy(): ?VaryBy { return VaryBy::userId(); } ``` ### 3. Use SWR for Non-Critical Data ```php // ✅ Good - News feed with SWR public function getCacheTTL(): Duration { return Duration::fromMinutes(5); } public function getStaleWhileRevalidate(): ?Duration { return Duration::fromHours(1); // Serve stale up to 1 hour } ``` ### 4. Batch Related Actions ```javascript // ✅ Good - Batch dashboard updates component.batch([ { action: 'updateStats' }, { action: 'updateChart' }, { action: 'refreshActivity' } ]); // ❌ Bad - Separate requests component.executeAction('updateStats'); component.executeAction('updateChart'); component.executeAction('refreshActivity'); ``` ### 5. Optimize Polling ```php // ✅ Good - Adaptive polling interval public function getPollInterval(): int { return $this->isHighPriority() ? 5000 : 30000; } // ❌ Bad - Aggressive fixed interval public function getPollInterval(): int { return 1000; // Too frequent! } ``` ### 6. Monitor Performance ```php // ✅ Good - Track and optimize $this->performance->track('component.render', [ 'duration_ms' => $duration, 'cache_hit' => $cacheHit, 'fragment_count' => $fragmentCount ]); // Alert if performance degrades if ($duration > 500) { $this->alerting->send('Slow component render', $context); } ``` --- ## Performance Checklist Before deploying to production: - [ ] Identify large components and add fragments - [ ] Enable caching for expensive renders - [ ] Configure appropriate varyBy context - [ ] Use SWR for non-critical data - [ ] Batch related actions - [ ] Optimize polling intervals - [ ] Add performance monitoring - [ ] Run performance benchmarks - [ ] Test with realistic data volumes - [ ] Monitor cache hit rates --- ## Summary LiveComponents performance optimizations: ✅ **Fragment Rendering** - 60-90% bandwidth reduction ✅ **Component Caching** - 95-99% faster repeated renders ✅ **VaryBy Context** - Intelligent cache variations ✅ **Stale-While-Revalidate** - Zero perceived latency ✅ **Request Batching** - 40-70% fewer requests ✅ **Adaptive Polling** - Optimized real-time updates Apply these strategies based on your use case for optimal performance.