Skip to content

[Due for payment 2026-05-07] Load all storage data into cache on startup and remove the eviction system #85252

Closed

Description

@Julesssss

Proposal from @TMisiukiewicz

Background
Onyx is the state management layer powering the Expensify app. It sits between a persistent storage backend and React components that subscribe to keys via useOnyx. Since its inception, Onyx has used a lazy-loading strategy: when a component subscribes to a key, Onyx checks its in-memory cache first, and falls back to a storage read if the key is absent. To prevent unbounded memory growth, a complex LRU eviction system was built on top — tracking access order, maintaining allow-lists and block-lists of evictable keys, and enforcing a 50k cached key ceiling.

The app's init() flow today runs two parallel operations before the deferred init task resolves and subscribers begin receiving data: it seeds the eviction allow-list by iterating all storage keys, and it multi-gets only the keys present in initialKeyStates from storage. All other keys are loaded on-demand, one batch per subscribing component group, each batch triggering its own round of subscriber notifications. The Expensify app's largest real-world state we are aware of sits at approximately 33k keys and ~45 MB of serialized data.

In practice, lazy-loading is an over-optimization: by the time the app finishes initializing, 99.99% of storage keys end up loaded into cache on-demand anyway. The eviction system never meaningfully reduces memory usage because nearly every key is actively subscribed to. The complexity exists to solve a problem that doesn't materialize.

Problem
When Onyx is initializing and multiple components subscribe to overlapping or dependent keys before any data is in cache, if those keys are loaded lazily in separate per-component storage fetches, then each fetch independently resolves and fires subscriber notifications with a partial view of the world, causing computationally expensive derived selectors (such as useSidebarOrderedReports) to execute multiple times with incomplete data before the final consistent state is available.

Solution
Replace the lazy per-key cache population strategy with a single bulk read of the entire storage database during init(), and populate the cache completely before the deferred init gate opens. By the time the first subscriber fires, the cache is complete — every key is present, every read is a synchronous cache hit, and no component ever triggers a storage fallback. This eliminates the batched on-demand loading that caused repeated partial-data notifications. The largest known real-world state (~33k keys, ~45 MB) sits well below the old eviction ceiling (50k), and measured memory consumption shows no significant difference between lazy-loaded and bulk-loaded states.

As a consequence of the cache being fully populated after init and with our findings that the cache eviction isn’t providing any benefits, the LRU eviction system becomes unnecessary and is removed along with its configuration surface (evictableKeys, maxCachedKeysCount, canEvict).

Beyond the immediate performance gains, loading the full state into memory at startup opens the door to several future improvements: structural sharing in the cache layer, a synchronous API for reading Onyx data, and simplification of the connection framework. Once these pieces are in place, useOnyx has the possibility to become a simple, fast hook — lightweight enough to call directly inside individual list items, a pattern that is currently impractical due to the hook's and overall framework complexity.

Measured Results (33k keys / ~45 MB)

Metric Before After Improvement
Web ManualAppStartup 3.8s 3.2s 16%
iOS ManualAppStartup 9.5s 7.0s 34%
SidebarOrderedReportsContextProvider 3.6s 1.6s 56%
JS thread occupancy (iOS) ~10s ~7.5s 25%
Memory — — No significant regression
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on Mar 13, 2026
  2. changed the title [-]Load all Onyx storage data into cache on startup and remove eviction system[/-] [+]Load all storage data into cache on startup and remove the eviction system[/+] on Mar 13, 2026
  3. melvin-bot commented on Mar 16, 2026

    @melvin-bot

    @fabioh8010 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  4. fabioh8010 commented on Mar 17, 2026

    @fabioh8010
    Contributor

    Onyx PR opened to review -> Expensify/react-native-onyx#752

  5. 75 remaining items

  6. Julesssss commented on May 4, 2026

    @Julesssss
    ContributorAuthor

    @situchan I see the following linked PRs, but 2 of which are Onyx lib bumps with PRs being reviewed in the Onyx repo. So I think this is just one PR review in Onyx?

  7. mountiny commented on May 5, 2026

    @mountiny
    Contributor

    @situchan Mind summarizing the payments here?

  8. situchan commented on May 5, 2026

    @situchan
    Contributor

    $500 based on #85252 (comment)?

  9. melvin-bot commented on May 7, 2026

    @melvin-bot

    Payment Summary

    Upwork Job

    • Contributor: @TMisiukiewicz does not require payment (Contractor)
    • Contributor: @fabioh8010 does not require payment (Contractor)
    • Reviewer: @situchan owed $250 via NewDot

    BugZero Checklist (@mallenexpensify)

    • (if NewFeature) I have created a PR for any necessary HelpDot updates, or I confirmed no updates are necessary.
    • I have verified the correct assignees and roles are listed above and updated the necessary manual offers
    • I have verified that there are no duplicate or incorrect contracts on Upwork for this job (https://www.upwork.com/ab/applicants//hired)
    • I have verified the PR was not reverted
    • I have applied any discounts due to bugs/regressions introduced by this PR
    • I have paid out the Upwork contracts or cancelled the ones that are incorrect
    • I have verified the payment summary above is correct
  10. mountiny commented on May 8, 2026

    @mountiny
    Contributor

    Sounds good!

  11. melvin-bot commented on May 8, 2026

    @melvin-bot

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

  12. mallenexpensify commented on May 9, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @situchan due $500 via NewDot

  13. laurenreidexpensify commented on May 11, 2026

    @laurenreidexpensify
    Contributor

    $500 approved for @situchan

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

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions