fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
697
docs/livecomponents/performance-guide-complete.md
Normal file
697
docs/livecomponents/performance-guide-complete.md
Normal file
@@ -0,0 +1,697 @@
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user