PR: https://github.com/curiticshealth/Care-Management/pull/2929 Merged: 2026-03-25
The assessment component is the highest-volume transaction in the app — 1.8M Livewire interactions in 2 days (Sentry data). PR #2929 added server-side autosave that fires afterStateUpdated on every form field.
afterStateUpdated is added to every assessment field in HasAssessment::createAssessmentFormField():
$field->afterStateUpdated(function () {
$this->autoSave();
});Only text fields got ->live(debounce: 500). Selects, toggles, checkboxes, and radios are ->live() with no debounce — they trigger autoSave() immediately on every click. A 30+ field assessment means 30+ Livewire round-trips during a single form fill.
Inside autoSave():
$inDraftStatus = MasterAssessmentStatus::where('name', 'Draft')->where('is_active', true)->first();This runs a raw query every time status_id is null. Could use MasterAssessmentStatus::idByName('Draft') (from the CachesIdByName trait added on the performance branch) to eliminate this query.
$this->form->getRawState() triggers Filament's form hydration, which runs all closures in form() — including master data queries:
MasterSmartText::pluck(...)— loads all smart textsMasterNoteDocStatus::where('name', 'Final')->value('id')MasterNoteDocStatus::where('name', 'Preliminary')->value('id')MasterAssessmentStatus::whereIn(...)->pluck('id')MasterNoteCategory::whereJsonContains(...)->get()
This is the M1 finding from the performance report. At 1.8M interactions, even 5 queries × 0.5ms each = ~25 CPU-minutes of pure DB time per 2 days.
catch (Throwable $e) {
logger()->error('Auto-save failed...');
}The user never knows if autosave is failing. If it fails consistently (e.g., a validation edge case), the user thinks their work is saved but it isn't.
Intentional — avoids cascading side effects and activity logging on every keystroke. Correct for autosave. But means autosaved changes won't appear in activity logs until the user hits the explicit Save button.
- Replace
MasterAssessmentStatus::where('name', 'Draft')...withMasterAssessmentStatus::idByName('Draft')— requires theCachesIdByNametrait from the performance branch (M1 work, Ryan G) - Add a user-facing indicator when autosave fails (e.g., a small warning toast or icon)
- Debounce across all fields — instead of saving per-field, use a single client-side timer that fires 2 seconds after the last change. This collapses 30 field changes into 1 autosave. Could be implemented with Alpine.js
x-on:change.debounce.2000mson the form wrapper, or a Livewire$debounceproperty. - This alone would reduce the 1.8M interaction volume significantly — most of those interactions are likely rapid sequential field changes during form fills.
- M1 caching work: add
CachesIdByNameto all master models queried inform(), wrap repeated queries inonce(). This reduces the per-autosave query cost from 5+ queries to 0 (served from cache). - Combine with debounce for the best result: fewer autosaves × cheaper autosaves.