From ebc3cd52fa230253f4a6e5149dbb4a0aaa739b76 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 19:59:20 +0000 Subject: [PATCH 1/3] Fast-path framework bookkeeping fields in state attribute access _get_attribute was 43% of profiled event-handling time; much of it was the framework's own reads of dirty_vars, parent_state, substates, and _backend_vars paying the full inherited-vars/event-handler/proxying chain on every access. - Add those names (and dirty_substates) to the CLASS_VAR_NAMES fast path; they are never state vars, never inherited, and never proxied. - Check base_vars/backend_vars membership before is_mutable_type when deciding whether to proxy, so non-var values skip the type check. - Cache get_skip_vars() per class (invalidated when inherited_vars changes) instead of rebuilding the set on every internal write. - Cache the frontend computed var names used by get_delta per class (invalidated when computed_vars changes). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X --- reflex/state.py | 71 +++++++++++++++++++++++++++++---------- tests/units/test_state.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 3762ad0ac2a..b0f69ee441d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -357,6 +357,10 @@ def _is_user_descriptor(value: Any) -> bool: all_base_state_classes: dict[str, None] = {} +# Attribute names resolved directly via object.__getattribute__, bypassing the +# inherited-var / event-handler / proxying logic in _get_attribute. Class-level +# tracking dicts as well as per-instance framework bookkeeping fields that are +# never state vars belong here. CLASS_VAR_NAMES = frozenset({ "vars", "base_vars", @@ -369,6 +373,11 @@ def _is_user_descriptor(value: Any) -> bool: "_always_dirty_computed_vars", "_always_dirty_substates", "_potentially_dirty_states", + "parent_state", + "substates", + "dirty_vars", + "dirty_substates", + "_backend_vars", }) @@ -408,6 +417,13 @@ class BaseState(EvenMoreBasicBaseState): # Set of states which might need to be recomputed if vars in this state change. _potentially_dirty_states: ClassVar[set[str]] = set() + # Cached result of get_skip_vars(), rebuilt lazily after inherited_vars changes. + _skip_var_names: ClassVar[frozenset[str] | None] = None + + # Cached names of non-backend computed vars, rebuilt lazily after + # computed_vars changes. + _frontend_computed_vars: ClassVar[frozenset[str] | None] = None + # The parent state. parent_state: BaseState | None = field(default=None, is_var=False) @@ -828,6 +844,7 @@ def computed_var_func(state: Self): setattr(cls, unique_var_name, computed_var_func_arg) cls.computed_vars[unique_var_name] = computed_var_func_arg + cls._frontend_computed_vars = None cls.vars[unique_var_name] = computed_var_func_arg cls._update_substate_inherited_vars({unique_var_name: computed_var_func_arg}) cls._always_dirty_computed_vars.add(unique_var_name) @@ -992,23 +1009,42 @@ def _check_overridden_computed_vars(cls) -> None: raise ComputedVarShadowsStateVarError(msg) @classmethod - def get_skip_vars(cls) -> set[str]: + def get_skip_vars(cls) -> frozenset[str]: """Get the vars to skip when serializing. Returns: The vars to skip when serializing. """ - return ( - set(cls.inherited_vars) - | { - "parent_state", - "substates", - "dirty_vars", - "dirty_substates", - "router_data", - } - | types.RESERVED_BACKEND_VAR_NAMES - ) + # Cached per class; invalidated when inherited_vars changes. + if (skip_vars := cls.__dict__.get("_skip_var_names")) is None: + skip_vars = ( + frozenset(cls.inherited_vars) + | { + "parent_state", + "substates", + "dirty_vars", + "dirty_substates", + "router_data", + } + | types.RESERVED_BACKEND_VAR_NAMES + ) + cls._skip_var_names = skip_vars + return skip_vars + + @classmethod + def _get_frontend_computed_var_names(cls) -> frozenset[str]: + """Get the names of computed vars that are sent to the frontend. + + Returns: + The names of non-backend computed vars. + """ + # Cached per class; invalidated when computed_vars changes. + if (cached := cls.__dict__.get("_frontend_computed_vars")) is None: + cached = frozenset( + name for name, cv in cls.computed_vars.items() if not cv._backend + ) + cls._frontend_computed_vars = cached + return cached @classmethod @functools.lru_cache @@ -1343,6 +1379,8 @@ def _update_substate_inherited_vars(cls, vars_to_add: dict[str, Var]): substate_class.vars.setdefault(name, var) substate_class.inherited_vars.setdefault(name, var) substate_class._update_substate_inherited_vars(vars_to_add) + # The skip vars cache incorporates inherited_vars. + substate_class._skip_var_names = None # Reinitialize dependency tracking dicts. cls._init_var_dependency_dicts() @@ -1401,6 +1439,7 @@ def inner_func(self: BaseState) -> list[str]: # Update tracking dicts. cls.computed_vars.update(dynamic_vars) + cls._frontend_computed_vars = None cls.vars.update(dynamic_vars) cls._update_substate_inherited_vars(dynamic_vars) @@ -1474,9 +1513,9 @@ def _get_attribute(self, name: str) -> Any: if parent_state is not None: return getattr(parent_state, name) - if is_mutable_type(type(value)) and ( + if ( name in super().__getattribute__("base_vars") or name in backend_vars - ): + ) and is_mutable_type(type(value)): # track changes in mutable containers (list, dict, set, etc) return MutableProxy(wrapped=value, state=self, field_name=name) @@ -1863,9 +1902,7 @@ def get_delta(self) -> Delta: delta = {} self._mark_dirty_computed_vars() - frontend_computed_vars: set[str] = { - name for name, cv in self.computed_vars.items() if not cv._backend - } + frontend_computed_vars = type(self)._get_frontend_computed_var_names() # Return the dirty vars for this instance, any cached/dependent computed vars, # and always dirty computed vars (cache=False) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 076024c0363..300c1bac9a6 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -1437,6 +1437,57 @@ def comp_v(self) -> int: assert comp_v_calls == 2 +def test_get_skip_vars_cached_per_class(): + """get_skip_vars is cached per class and includes inherited vars.""" + + class SkipVarsParentState(BaseState): + p: int = 0 + + class SkipVarsChildState(SkipVarsParentState): + c: int = 0 + + parent_skip = SkipVarsParentState.get_skip_vars() + assert SkipVarsParentState.get_skip_vars() is parent_skip + assert "parent_state" in parent_skip + child_skip = SkipVarsChildState.get_skip_vars() + assert child_skip is not parent_skip + assert "p" in child_skip + assert "p" not in parent_skip + + +def test_frontend_computed_var_names_cached_per_class(): + """Frontend computed var names exclude backend vars and cache per class.""" + + class FrontendCvarState(BaseState): + @rx.var + def visible(self) -> int: + return 1 + + @rx.var(backend=True) + def _hidden(self) -> int: + return 2 + + names = FrontendCvarState._get_frontend_computed_var_names() + assert "visible" in names + assert "_hidden" not in names + assert FrontendCvarState._get_frontend_computed_var_names() is names + + +def test_framework_bookkeeping_fields_not_proxied(): + """Framework bookkeeping containers are returned raw, not proxied.""" + + class BookkeepingState(BaseState): + values: list[int] = [] + + state = BookkeepingState() + assert type(state.dirty_vars) is set + assert type(state.substates) is dict + assert type(state._backend_vars) is dict + assert state.parent_state is None + # Actual state vars still get proxied for mutation tracking. + assert isinstance(state.values, MutableProxy) + + def test_computed_var_cached_depends_on_non_cached(): """Test that a cached var is recalculated if it depends on non-cached ComputedVar.""" From 483d2d125337a806e5b06053f18a46128d957851 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 00:34:17 +0000 Subject: [PATCH 2/3] Invalidate skip vars cache in add_var Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X --- reflex/state.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reflex/state.py b/reflex/state.py index b0f69ee441d..b5c22367f14 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1250,6 +1250,8 @@ def add_var(cls, name: str, type_: Any, default_value: Any = None): # let substates know about the new variable for substate_class in cls.get_substates(): substate_class.vars.setdefault(name, var) + # inherited_vars may alias this class's vars dict. + substate_class._skip_var_names = None # Reinitialize dependency tracking dicts. cls._init_var_dependency_dicts() From 97e9e37980920333b9fb6e8ab3477acd70cac160 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 00:36:08 +0000 Subject: [PATCH 3/3] Add changelog fragment Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X --- news/6757.performance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6757.performance.md diff --git a/news/6757.performance.md b/news/6757.performance.md new file mode 100644 index 00000000000..7b6943c8c59 --- /dev/null +++ b/news/6757.performance.md @@ -0,0 +1 @@ +Speed up state attribute access: framework bookkeeping fields (dirty_vars, parent_state, substates, _backend_vars) resolve via the fast path, and get_skip_vars() plus get_delta's frontend computed var set are cached per class.