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

15 KiB

LiveComponents Performance Guide

Complete performance optimization guide for LiveComponents covering caching, batching, middleware, and optimization techniques.


Table of Contents

  1. Caching Strategies
  2. Request Batching
  3. Middleware Performance
  4. Debounce & Throttle
  5. Lazy Loading
  6. Island Components
  7. Fragment Updates
  8. SSE Optimization
  9. Memory Management
  10. 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:

  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:

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 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

#[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