Skip to content

Memoize RadixThemesColorModeProvider instead of building a fresh instance per component #7013

Description

@masenf

Low priority, and it should not be merged without a benchmark showing it actually pays — see "Must be benchmarked" below.

Describe the Enhancement you want

RadixThemesComponent._get_app_wrap_components builds a new provider on every call:

# packages/reflex-components-radix/src/reflex_components_radix/themes/base.py#L131-L134
@staticmethod
def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
    return {
        (45, "RadixThemesColorModeProvider"): RadixThemesColorModeProvider.create(),
    }

Every Radix component in the tree contributes one, and they are all equivalent — the dict is keyed (45, "RadixThemesColorModeProvider"), so _get_all_app_wrap_components collapses them and all but one is discarded. The rest are constructed only to be thrown away, and anything that walks or renders them in between pays for each one separately.

The enhancement is to return a memoized instance rather than a fresh one per call.

What this costs today

Profiling a memoization-heavy page (a synthetic one built for this, not a repo benchmark), during a single page compile:

RadixThemesColorModeProvider values reaching the memo-name hash 114
distinct object identities among them 113
time spent encoding them 6.58 ms of 22.1 ms total encoding (~30%)
encoder share of component_hash ~13–14%
component_hash share of the compile ~45%

So the encoder-side cost alone is roughly 1.6% of that compile. Because the instances are distinct objects with identity-based equality, no cache in the hash can collapse them — the duplication has to be removed where they are created.

What has not been measured

Only the encoding cost above was measured. The construction cost of the ~113 discarded instances, and whatever they add to the _get_all_app_wrap_components walk, are unmeasured and plausibly larger than the encoding. That is the main thing a benchmark needs to settle.

Must be benchmarked

The numbers above come from one synthetic page on one machine. Before this is worth taking:

  • show the improvement on tests/benchmarks/test_compilation.py (codspeed), not just a local script
  • confirm it holds on a page with many Radix components, which is where the duplication concentrates
  • if the win is inside noise, close this — the change is not worth the risk on its own

Correctness to check first

Sharing one instance is not automatically safe. The compiler mutates component trees in place (for example when inserting memo wrappers), so a module-level singleton could be mutated by one page's compile and observed by the next. Anything done here needs to establish that the shared provider is never mutated, or hand out a copy at the point where mutation could happen. A per-compile cache may be a better fit than a process-lifetime one.

Additional context

Found while profiling the memo-name hash in #6947. That PR does not touch this; the hash is only what made the duplication visible.

A different angle on the same waste, if the memoization turns out to be awkward: hash app-wrap components by their (priority, tag) key plus class rather than by rendering them, since the key already identifies the wrapper uniquely. That helps the hash only, not the construction cost, so it is the weaker of the two.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAnything you want improved

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions