Skip to content

[Due for payment 2026-10-05] [Proposal] API.writeWhenReady — safe delayed optimistic writes across flows #96010

Closed

Description

@melvin-bot

Initial proposal and discussion in Slack

Background

API.write() is the standard write path in the App. It takes command params plus Onyx data, applies optimistic updates, and sends the request through SequentialQueue for persistence, retries, and ordering.

Those optimistic updates are useful because the destination can show the expected result before the server responds. The trade-off is that applying optimistic Onyx data can trigger a large re-render wave across subscribed components. If that happens while a modal is dismissing or a destination is becoming visible, the JS thread can spend transition-frame time processing those updates instead of keeping navigation smooth.

I fixed this for the submit-expense flow by delaying the expensive API.write() call until after the destination is visually ready.
This PR alone reduced the span from 2408 ms to 893 ms. After combining other improvements, the span is now under 300 ms. The user sees the destination first, and the write-driven re-render wave happens afterward. This kept the normal API.write() and SequentialQueue behavior intact, while improving perceived submit speed.

Problem

When a single user interaction both navigates (e.g. dismisses a modal) and triggers an optimistic API.write(), the re-render cascade from applying optimistic Onyx data can compete with the navigation transition for main-thread time. The write itself is cheap. The cost is subscribed components re-rendering while the animation still needs frames. That makes dismiss feel slow/choppy.

Solution

Add an API-owned deferred write primitive:

API.writeWhenReady(command, params, onyxData, barrier?).

The first three parameters match API.write. The fourth parameter is optional. If omitted, the write waits for the navigation transition to finish. If provided, it is a promise-like barrier that releases the write when the destination is ready for the optimistic Onyx update.

API.writeWhenReady does not replace the normal write pipeline. It waits for the default transition readiness or the passed barrier, then calls normal API.write(). Optimistic data, success data, failure data, persistence, retry behavior, and request ordering stay owned by the existing API and network layers.

The default transition behavior covers the common modal-dismiss case without any extra argument. Passing a barrier is only needed when transition completion is not the right readiness point. Search can pass the readiness signal it already uses today from layout, focus, fallback, background, or timeout handling.

The first migration should move submit-expense to this API, then remove submit-expense API-write coordination from deferredLayoutWrite. Once submit-expense is fully migrated, we can document the standard in contributingGuides/PERFORMANCE.md and update reviewer guidance.

Alternative

One suggestion was to send the network request immediately and delay only optimisticData, since the render cascade comes from the optimistic Onyx update.

That is risky in the current pipeline because successData, failureData, finallyData, and response-applied Onyx updates can arrive before the delayed optimistic base exists. To make that safe, we would need to buffer response/failure side effects behind the same readiness point too. At that point it is no longer a small scheduling wrapper over API.write; it becomes a new two-phase write pipeline. Delaying the whole API.write() keeps the existing ordering intact.

Telemetry

Each migrated flow should measure perceived latency separately from delayed-write completion:

ToDestinationVisible: action to first meaningful destination paint.
WriteWhenReadyWait: barrier creation to barrier release.
ToFullyInteractive: optional, if deferred work affects interaction readiness.
ToOnyxUpdateVisible: from barrier release to delayed Onyx update paint

Timeouts and background flushes should be logged separately because they mean the intended barrier did not release the write.

Context

This proposal is part of the broader standardization effort tracked in #88596.

Related PR that demonstrated the gains: #85629 (2408 ms → 893 ms on the submit-expense span; combined improvements now under 300 ms).

Issue OwnerCurrent Issue Owner: @brunovjk

Activity

  1. melvin-bot commented on Jul 17, 2026

    @melvin-bot
    Author

    @JakubKorytko Whoops! This issue is 2 days overdue. Let's get this updated quick!

  2. JakubKorytko commented on Jul 20, 2026

    @JakubKorytko
    Member

    Not OD, was OOO (sick), back now and working on it.

  3. 74 remaining items

  4. changed the title [-][Proposal] API.writeWhenReady — safe delayed optimistic writes across flows[/-] [+][Due for payment 2026-10-05] [Proposal] API.writeWhenReady — safe delayed optimistic writes across flows[/+] on Sep 28, 2026
  5. melvin-bot commented on Sep 28, 2026

    @melvin-bot
    Author

    @brunovjk

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.4.94-0 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-05. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  6. melvin-bot commented on Oct 1, 2026

    @melvin-bot
    Author

    @brunovjk Whoops! This issue is 2 days overdue. Let's get this updated quick!

  7. JakubKorytko commented on Oct 2, 2026

    @JakubKorytko
    Member

    I am working on the #102324 and it blocks the docs PR, not OD

  8. melvin-bot commented on Oct 4, 2026

    @melvin-bot
    Author

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  9. brunovjk commented on Oct 6, 2026

    @brunovjk
    Contributor

    Contributor+ Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR: N/A - The issue is a planned performance improvement/architecture update rather than a regression fix.
      Link to the comment on the Issue:

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion:

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    Regression Test Proposal

    Precondition:

    • Be signed in with a workspace where expenses can be created and have a 1:1 chat available for split expenses.

    Test:

    1. Open Spend > Expenses.
    2. Use the FAB to create a manual expense and submit it.
    3. Verify the expense appears exactly once and no loading skeleton remains stuck.
    4. Open a 1:1 chat and create a Split expense using Scan so the "Split receipt" quick action becomes available.
    5. Return to Spend > Expenses and use the FAB > "Split receipt" quick action to upload a receipt.
    6. Verify the new scanning expense row appears exactly once with its receipt/scanning state and is not displayed as a stuck skeleton loading row.
    7. Background the app while a new expense submission is pending, then reopen it.
    8. Verify the pending expense is present and neither the write nor the loading state remains stuck.

    Do we agree 👍 or 👎

  10. brunovjk commented on Oct 6, 2026

    @brunovjk
    Contributor

    Contributor+ Summary

    • I previously reviewed work related to this issue and received $125 for the earlier PR. That payment was for the previous PR cycle.
    • I also reviewed and approved this new PR, including testing and review feedback.
    • After this PR was deployed, a new payment cycle was started specifically for it, with payment due on 2026-10-05.
    • A regression related to this PR was reported in this issue.

    So, as I understand it, the previous $125 payment does not cover my review of this PR. I should still be eligible for the C+ payment for this PR, but with the regression reduction applied, which would make the payment $125 via NewDot.

    I believe this is the correct interpretation, but I’m open to any discussion if I missed something.

    cc: @ mallenexpensify

  11. mallenexpensify commented on Oct 6, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @brunovjk due $125 via NewDot

    @brunovjk makes sense to me, thx for the detailed breakdown. cc @Julesssss @roryabraham in case either of you disagree

  12. laurenreidexpensify commented on Oct 7, 2026

    @laurenreidexpensify
    Contributor

    Approved $125 @brunovjk New Expensify

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2EngineeringImprovementItem broken or needs improvement.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions