- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
25 KiB
LiveComponents Performance Guide
Comprehensive guide to optimizing LiveComponent performance through caching, batching, fragment updates, and advanced patterns.
Table of Contents
- Overview
- Fragment-Based Rendering
- Component Caching
- Advanced Caching: varyBy
- Stale-While-Revalidate (SWR)
- Request Batching
- Polling Optimization
- Performance Monitoring
- Best Practices
Overview
LiveComponents provides multiple performance optimization strategies:
- Fragment Rendering - Update only changed parts of the DOM (60-90% bandwidth reduction)
- Component Caching - Cache rendered components with intelligent invalidation
- VaryBy Context - Cache variations based on user, locale, feature flags
- Stale-While-Revalidate - Serve stale data while refreshing in background
- Request Batching - Combine multiple actions into single request
- 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:
<div data-lc-component="dashboard">
<!-- Fragment 1: Only updates when stats change -->
<div data-lc-fragment="user-stats">
<h3>Statistics</h3>
<p>Total: {stats.total}</p>
<p>Active: {stats.active}</p>
</div>
<!-- Fragment 2: Only updates when activities change -->
<div data-lc-fragment="recent-activity">
<h3>Recent Activity</h3>
<ul>
<for items="activities" as="activity">
<li>{activity.name} - {activity.time}</li>
</for>
</ul>
</div>
<!-- Fragment 3: Updates independently -->
<div data-lc-fragment="notifications">
<span class="badge">{notification_count}</span>
</div>
</div>
Using Fragments
Automatic Fragment Updates:
<!-- Button updates only user-stats fragment -->
<button
data-lc-action="refreshStats"
data-lc-fragments="user-stats"
>
Refresh Stats Only
</button>
<!-- Button updates multiple fragments -->
<button
data-lc-action="refreshAll"
data-lc-fragments="user-stats,recent-activity"
>
Refresh Stats & Activity
</button>
JavaScript Fragment Updates:
// 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
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
<!-- ✅ Good - Independent fragments with clear boundaries -->
<div data-lc-fragment="sidebar"><!-- Sidebar content --></div>
<div data-lc-fragment="main-content"><!-- Main content --></div>
<div data-lc-fragment="footer"><!-- Footer --></div>
<!-- ❌ Bad - Overlapping fragments -->
<div data-lc-fragment="header">
<div data-lc-fragment="nav"><!-- Nested fragment causes confusion --></div>
</div>
2. Use Fragments for High-Frequency Updates
<!-- ✅ Good - Frequently updated data as fragment -->
<div data-lc-fragment="stock-price">
${current_price} <span class="change">{change}%</span>
</div>
<!-- Poll only updates the fragment, not entire page -->
<button data-lc-action="poll" data-lc-fragments="stock-price">Refresh</button>
3. Combine Related Updates
// ✅ 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
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:
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:
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
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
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
// 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
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:
#[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:
#[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:
#[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:
#[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:
#[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:
// 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
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:
- 0-5 min: Cache fresh → Serve from cache (instant)
- 5-30 min: Cache stale → Serve stale (instant) + refresh in background
- 30+ min: Cache expired → Fetch fresh → Update cache → Serve
SWR with varyBy
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:
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:
Cache-Control: max-age=300, stale-while-revalidate=1800
Age: 450
X-Cache-Status: STALE
X-Cache-Refreshing: true
Client-side interpretation:
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
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):
// Framework detects rapid actions and batches automatically
for (let i = 0; i < 5; i++) {
liveComponent.executeAction('increment');
}
// Sent as single batched request
Manual Batching:
liveComponent.batch([
{ action: 'updateStats', params: {} },
{ action: 'updateChart', params: { period: '7d' } },
{ action: 'refreshNotifications', params: {} }
]);
Conditional Batching:
// Only batch if multiple actions pending
const config = {
batching: {
enabled: true,
minBatchSize: 2 // Only batch if 2+ actions
}
};
Backend Batch Processing
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
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
// 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
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
// Stop polling when tab hidden, resume when visible
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
liveComponent.pausePolling();
} else {
liveComponent.resumePolling();
}
});
Performance Monitoring
Cache Metrics
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
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
// 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
<!-- ✅ Good - Fragment-based dashboard -->
<div data-lc-component="dashboard">
<div data-lc-fragment="stats"><!-- 2KB --></div>
<div data-lc-fragment="chart"><!-- 15KB --></div>
<div data-lc-fragment="activity"><!-- 8KB --></div>
</div>
<!-- Update only stats fragment (2KB instead of 25KB) -->
<button data-lc-action="refreshStats" data-lc-fragments="stats">Refresh</button>
2. Cache Expensive Renders
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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
// ✅ 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.