Jayant © 2026Hyderabad, India
Jayant
HomeAboutWorkWritingResumeContact
Discuss a product
Field note / Published26 July 2026

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.

6 min read1,140 words
  • Performance
  • Supabase
  • PostgreSQL
  • Optimistic UI
Cover illustration for How I Cut Key Interaction Latency by Up to 53%
Inside this note
  1. 01Measure the path people actually wait for
  2. 02The first hidden cost was authentication twice
  3. 03Express ownership in one mutation
  4. 04Parallelize independent reads
  5. 05Optimize the database from real query shapes
  6. 06Make reversible actions feel immediate
  7. 07The measured result
  8. 08What still limits the tail
  9. 09The result was a system change, not a trick

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:

Drawing architecture diagram…

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:

Drawing architecture diagram…

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:

RequestBeforeAfterImprovement
Activity list273.2 ms225.6 ms17.4%
Activity statistics180.3 ms160.6 ms10.9%
Calculator history255.2 ms222.8 ms12.7%
Scratchpad list169.8 ms154.9 ms8.8%

The larger effect appeared in writes that had been doing unnecessary work:

InteractionBeforeAfter medianImprovement
Activity create273.0 ms129.2 ms52.7%
Calculator create278.3 ms182.3 ms34.5%
Scratchpad read toggle221.7 ms124.5 ms43.8%
Scratchpad delete172.0 ms117.0 ms32.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.

Article details
Written by
Jayant
Reading time
6 minutes
Last updated
12 September 2026
Get in touch
From the workbench

Notes from the work

More on how the software was built. Written by Jayant, Software Engineer.

Get in touch
Continue reading

How I Cut Initial JavaScript Without Cutting Features

A measured Next.js performance pass that moved optional code out of initial routes and added budgets to keep the improvements from regressing.

Next note