How I Cut Key Interaction Latency by Up to 53%
Tracing the full click-to-database path exposed duplicate auth, extra queries, and UI waits—then measurement proved what improved.
- Performance
- Supabase
- PostgreSQL
- Optimistic UI

A product can earn a respectable Lighthouse score and still feel slow every time someone uses it.
That was happening in three authenticated Studio products. Creating an activity, toggling a day, deleting a calculation, or marking a Scratchpad entry as read made the interface hesitate. The pages were not unusually heavy. The delay lived inside the complete protected request path.
I stopped treating it as frontend polish and benchmarked the interaction from click to confirmed state.
Measure the path people actually wait for
I ran authenticated localhost flows against the linked Supabase project. Routes were warmed, the browser session remained signed in, and timing ended after the response body completed.
The benchmark covered reads and full create, update, toggle, and delete cycles across Activity Tracker, Calculator history, and Scratchpad. Development compilation samples and remote-auth spikes remained visible in the raw data but were excluded from warmed comparisons. Temporary rows were deleted and verified absent after each run.
The unit of performance was the complete action:
That path is more useful than an isolated SQL duration because it includes everything the person experiences.
The first hidden cost was authentication twice
Studio’s proxy already authenticated protected requests. It stripped client-supplied identity headers so a caller could not forge a user ID, then verified the Supabase session.
The handoff was wrong. The proxy constructed the forwarded request before attaching verified identity. Route handlers received no trusted user header and called Supabase Auth again.
Every hot request paid for two identity checks:
I changed the proxy to attach the verified user ID and email after authentication. Forged incoming headers are still removed. Handlers reuse the trusted identity and retain a direct Auth fallback for isolated development calls that bypass the normal proxy path.
Regression tests prove both sides: a forged header is replaced, and a protected handler skips the fallback Auth call when trusted identity is present.
The optimization removed duplicate work without weakening authorization.
Express ownership in one mutation
Several write paths selected a record to prove ownership and then updated or deleted the same record with the user ID filter.
The write already expressed the ownership boundary. I changed Activity and Scratchpad mutations to perform one ownership-scoped operation and return a non-enumerating 404 when no row matched. The caller cannot distinguish a missing record from someone else’s record.
Daily Activity entries had a check-then-write race. The database already enforced uniqueness for activity, date, and user, so the API now uses one atomic upsert after validating activity ownership.
The general rule is simple: when PostgreSQL can safely express the condition in one statement, do not add a network round trip to ask the same question first.
Parallelize independent reads
Activity statistics needed activities and entries. The original handler loaded them sequentially, then repeatedly filtered entries for each activity.
The optimized version starts both queries together, selects only the columns used by the aggregation, and computes completion counts and unique days in one pass. It also rejects an invalid month before touching the database.
The response contract did not change. The implementation simply stopped making independent work wait in line.
Optimize the database from real query shapes
The Supabase performance advisor reported 48 row-level-security initialization warnings. Policies repeatedly evaluated the current-user function per row instead of once per statement.
I applied the statement-level initialization pattern while preserving every ownership predicate. All 48 warnings cleared.
I added only three indexes backed by real application queries: Scratchpad chronology, Calculator history chronology, and denomination lookup by calculation. I did not add indexes to every filtered column or react mechanically to sequential scans on tiny CMS tables. Every index adds write and maintenance cost; the query pattern has to justify it.
Make reversible actions feel immediate
Even a safe 130 ms mutation feels slower when the interface refuses to acknowledge the click until the response returns.
Activity toggles, Calculator deletes, and Scratchpad create, read, and delete flows now update local state optimistically. The request still performs authentication, authorization, validation, and persistence. The interface predicts the likely accepted result and keeps enough previous state to recover if it is wrong.
I limited optimistic behavior to actions with one clear reversible representation. It would be a poor fit for a workflow where the server can return several valid outcomes.
The measured result
Warmed read medians improved across all three products:
| Request | Before | After | Improvement |
|---|---|---|---|
| Activity list | 273.2 ms | 225.6 ms | 17.4% |
| Activity statistics | 180.3 ms | 160.6 ms | 10.9% |
| Calculator history | 255.2 ms | 222.8 ms | 12.7% |
| Scratchpad list | 169.8 ms | 154.9 ms | 8.8% |
The larger effect appeared in writes that had been doing unnecessary work:
| Interaction | Before | After median | Improvement |
|---|---|---|---|
| Activity create | 273.0 ms | 129.2 ms | 52.7% |
| Calculator create | 278.3 ms | 182.3 ms | 34.5% |
| Scratchpad read toggle | 221.7 ms | 124.5 ms | 43.8% |
| Scratchpad delete | 172.0 ms | 117.0 ms | 32.0% |
The hot-path write medians now sit below the working 250 ms API target. Optimistic state removes most of that remaining wait from the visible interaction.
What still limits the tail
The remaining delay is not only SQL execution. Remote session verification can spike across the development machine, application boundary, and managed Auth service.
Skipping verification would make a faster but unsafe benchmark. A future server-trusted session strategy could reduce that network dependency while preserving the trust boundary.
I also left unrelated permissive-policy advisor warnings for a separate authorization review. Performance evidence is not permission to change access semantics mechanically.
The result was a system change, not a trick
The slow feeling had several causes: duplicate authentication, serial reads, read-before-write mutations, broad payloads, repeated policy evaluation, missing query-specific indexes, and UI state that waited for every round trip.
No single optimization explains the outcome. The improvement came from treating browser intent, proxy trust, API validation, PostgreSQL work, and visible feedback as one product path.
That is why interaction performance belongs to product engineering. The user experiences the whole system at once.
Explore Studio, open Activity Tracker, or read the bundle case study.