# Performance & Optimization Standards

## 🚀 Backend (Laravel/PHP)

### 1. Database Eager Loading (The N+1 Rule)
**❌ BAD:** Loading relationships inside a loop or API resource.
```php
foreach ($users as $user) {
    echo $user->profile->name; // Queries DB 100 times
}
```
**✅ GOOD:** Eager load upfront.
```php
$users = User::with('profile')->get();
```

### 2. Query Complexity (Aggregations)
**❌ BAD:** Using complex `INNER JOIN`s on `COUNT(*)` queries when the joined data is not needed to filter the count. This causes massive execution timeouts on large datasets (e.g., NetSuite Transaction Lines).
```php
$count = TransactionLine::join('transaction', ...)->where('mainline', 'F')->count(); // BAD
```
**✅ GOOD:** Simplify the query to just the base table + its indexable filters.
```php
$count = TransactionLine::where('mainline', 'F')->count(); // GOOD
```

### 3. Caching Strategy
**Mandatory:** Use caching for any read-heavy data that changes infrequently (Permissions, Settings, Meta-data, Saved Searches).
```php
// Tier 1: Local Property Cache (Request Scope)
private array $cache = [];

// Tier 2: System Cache (Redis)
return Cache::remember("tenant_{$id}_settings", 3600, fn() => ...);
```
**Invalidation:** Automatically invalidate Redis keys via Model `Observers` (e.g. `RecordCacheInvalidator`) when records are updated, rather than manually flushing inside controllers.
**Transaction Safety (Race Conditions):** When invalidating cache inside Eloquent `saved`/`updated`/`created` events, you MUST wrap the invalidation logic in `DB::afterCommit(fn() => ...)` or use `$afterCommit = true` on the Observer. This prevents the cache from being purged *before* the encompassing database transaction commits, which would otherwise cause concurrent requests to cache stale pre-commit data.
**Non-atomic Cache Guard — TOCTOU Race (BUG-N):** Any code that follows a `Cache::get → check → create-external-resource → Cache::put` sequence on a shared cache key is a TOCTOU (Time-Of-Check-Time-Of-Use) race condition. Two concurrent requests can both miss the cache, both pass the check, and both execute the creation side-effect — producing duplicate cloud folders, external API objects, or DB records. The correct pattern is the **double-checked locking** idiom using `Cache::lock()`:
```php
// ❌ BAD — non-atomic: two concurrent requests both miss and both create
$cached = Cache::get($key);
if ($cached === null) {
    $id = $this->createExternalResource($name, $parent);
    Cache::put($key, $id, 3600);
}

// ✅ GOOD — atomic: only one request creates; second finds the cache populated
$cached = Cache::get($key); // fast path: no lock needed on hit
if ($cached === null) {
    $lock = Cache::lock("create_lock_{$key}", 10);
    if ($lock->get()) {
        try {
            $cached = Cache::get($key); // double-check inside critical section
            if ($cached === null) {
                $cached = $this->createExternalResource($name, $parent);
                Cache::put($key, $cached, 3600);
            }
        } finally {
            $lock->release();
        }
    } else {
        sleep(1); // wait for the winner, then re-read
        $cached = Cache::get($key) ?? $this->resolveWithoutCreating($key);
    }
}
return $cached;
```
Lock keys MUST be tenant-scoped (via `TenantCacheKeyService::make()`) to prevent cross-tenant lock collisions.
**High-Volume In-memory Fallback:** Logic controlling access (like `User::hasPermissionTo()`) evaluated row-by-row on massive record sets MUST be backed by short-lived in-memory Request Arrays + Redis TTL caching to prevent exponential N+1 query bottlenecks.
**Driver Guard Rule:** Never use `Cache::tags()` without an explicit `Cache::store('redis')` or `Cache::store('memcached')` prefix. The default driver (`file`) does not support tagging and will throw `BadMethodCallException` at runtime. Prefer `Cache::store('redis')->remember(...)` with a structured composite key when tag semantics are not strictly required.

### 4. Telemetry & Sentry Quotas
Avoid polluting the Sentry error tracker with high-volume/low-value events.
* **Filter Exceptions:** Ensure `Livewire\Exceptions\ValidationException` is filtered out before sending.
* **Trace Sampler:** Do not sample health-checks (`/up`, `/_health`); keep queue job traces low (e.g. 5%) to avoid eating span quotas.

### 5. Validator/Parser Class Instantiation in Hot Paths
**Rule: Never instantiate third-party validator or parser classes inside hot-path methods.** Many external libraries carry internal caches (schema registries, parsed rule objects, compiled expressions, resolved file URIs) that are destroyed when the instance is garbage-collected. Creating `new ValidatorClass()` on every invocation defeats this caching, forcing repeated disk I/O, JSON parsing, or regex compilation per call.

**Pattern:** Bind such classes as singletons in the service container. The instance lives for the lifetime of the process, and its internal cache warms after the first call. Subsequent calls skip all initialization work.

```php
// ❌ BAD — cache discarded on every call
public function execute(array $payload): object
{
    $validator = new ThirdPartyValidator(); // fresh empty cache every time
    $validator->validate($payload, $schema);
}

// ✅ GOOD — singleton injected via DI, cache persists across calls
public function execute(array $payload): object
{
    $validator = new ThirdPartyValidator($this->sharedFactory); // cache reused
    $validator->validate($payload, $schema);
}
// Register in ServiceProvider: $this->app->singleton(ValidatorFactory::class);
```

**Applies to:** JSON schema validators, XML parsers, expression evaluators, regex compilers, serializers — any library that parses a schema or ruleset from disk or a string and stores the result internally.

### 6. Queue Heavy Operations
Any logic taking >500ms (Emails, PDF generation, External API sync) **MUST** be queued.
* Implement `ShouldQueue` on Listeners/Mailables.
* Use `dispatch()->onQueue('low')` for background tasks.

---

## ⚡ Frontend (Livewire/Alpine)

### 1. Lazy Loading & Loop Rendering
**❌ BAD:** Loading heavy metrics or N multiple saved searches simultaneously on page render.
**✅ GOOD:** Use placeholders and `wire:init="loadData"` to implement deferred/progressive loading for heavy dashboard blocks.
**❌ BAD:** Allowing nested Livewire components (inside a loop) to trigger DB queries identically on their `mount()` lifecycle.
**✅ GOOD:** The parent component MUST eagerly load all needed data (Props) via `LEFT JOIN` and pass it down cleanly to children.

### 2. Input Debouncing
**❌ BAD:** Triggering network requests on every keystroke.
`<input wire:model="search">`
**✅ GOOD:** Debounce or Lazy update.
`<input wire:model.live.debounce.500ms="search">` OR `<input wire:model.blur="search">`

### 3. Alpine.js Memory Management
If you use `init()`, you **MUST** provide a `destroy()` method to clean up listeners and intervals.

### 4. Asset Loading
* Use `defer` for non-critical scripts.
* Use `lazy` loading for images below the fold: `<img loading="lazy" ...>`
