diff --git a/packages/reflex-base/news/6576.performance.md b/packages/reflex-base/news/6576.performance.md new file mode 100644 index 00000000000..e55e226cbc0 --- /dev/null +++ b/packages/reflex-base/news/6576.performance.md @@ -0,0 +1 @@ +Speed up component creation by resolving field defaults lazily (via class-level descriptors) instead of eagerly on every instance, caching each component class's event triggers, and memoizing `to_camel_case`. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 9f68ee46908..767df549dad 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -95,6 +95,35 @@ def __repr__(self) -> str: return f"ComponentField(default={self.default!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" return f"ComponentField(default_factory={self.default_factory!r}, is_javascript={self.is_javascript!r}{annotated_type_str})" + def __get__(self, instance: Any, owner: type[Any] | None = None) -> Any: + """Supply an unset field's default via the descriptor protocol. + + With no ``__set__`` this is a non-data descriptor: an explicitly-set + value in the instance ``__dict__`` shadows it, so only unset fields + reach here. Construction can therefore skip materializing every + default onto every instance and let reads resolve them lazily. + + Args: + instance: The component instance, or ``None`` for class access. + owner: The owning class. + + Returns: + ``self`` for class access, otherwise the default. Factory defaults + are cached on the instance so later in-place mutation persists. + + Raises: + AttributeError: The field has neither a default nor a factory. + """ + if instance is None: + return self + if self.default is not MISSING: + return self.default + if self.default_factory is not None: + value = self.default_factory() + instance.__dict__[self._name] = value + return value + raise AttributeError(self._name) + def field( default: FIELD_TYPE | _MISSING_TYPE = MISSING, @@ -278,6 +307,17 @@ def _finalize_fields( if value.is_javascript is True } + # Install each own field as a class-level descriptor so unset instance + # attributes resolve to their default through ``ComponentField.__get__`` + # (inherited fields resolve via the MRO). A name bound to a plain value + # — a ``@property``, method, or literal default — serves the attribute + # itself, so only absent names and field() markers get the descriptor. + for field_name, field_ in own_fields.items(): + if field_name not in namespace or isinstance( + namespace[field_name], ComponentField + ): + namespace[field_name] = field_ + class BaseComponent(metaclass=BaseComponentMeta): """The base class for all Reflex components. @@ -314,9 +354,6 @@ def __init__( """ for key, value in kwargs.items(): setattr(self, key, value) - for name, value in self.get_fields().items(): - if name not in kwargs: - setattr(self, name, value.default_value()) def set(self, **kwargs): """Set the component props. @@ -1070,7 +1107,14 @@ def get_event_triggers(cls) -> dict[str, types.ArgsSpec | Sequence[types.ArgsSpe """ # Look for component specific triggers, # e.g. variable declared as EventHandler types. - return DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue] + # Cache on the class's own __dict__ (not inherited) so each subclass + # computes its own; the field set is fixed at class creation. + cached = cls.__dict__.get("_event_triggers_cache") + if cached is not None: + return cached + result = DEFAULT_TRIGGERS | args_specs_from_fields(cls.get_fields()) # pyright: ignore [reportOperatorIssue] + cls._event_triggers_cache = result + return result def __repr__(self) -> str: """Represent the component in React. diff --git a/packages/reflex-base/src/reflex_base/components/field.py b/packages/reflex-base/src/reflex_base/components/field.py index 5487bdd923d..413b29d062c 100644 --- a/packages/reflex-base/src/reflex_base/components/field.py +++ b/packages/reflex-base/src/reflex_base/components/field.py @@ -15,6 +15,9 @@ class BaseField(Generic[FIELD_TYPE]): """Base field class used by internal metadata classes.""" + # Set by ``FieldBasedMeta._finalize_fields`` once the owning class is built. + _name: str + def __init__( self, default: FIELD_TYPE | _MISSING_TYPE = MISSING, diff --git a/packages/reflex-base/src/reflex_base/utils/format.py b/packages/reflex-base/src/reflex_base/utils/format.py index c8f11e01f02..35a2bc3e30a 100644 --- a/packages/reflex-base/src/reflex_base/utils/format.py +++ b/packages/reflex-base/src/reflex_base/utils/format.py @@ -6,6 +6,7 @@ import json import os import re +from functools import lru_cache from typing import TYPE_CHECKING, Any from reflex_base import constants @@ -13,13 +14,8 @@ if TYPE_CHECKING: from reflex_base.components.component import ComponentStyle - from reflex_base.event import ( - ArgsSpec, - EventChain, - EventHandler, - EventSpec, - EventType, - ) + from reflex_base.event import EventChain, EventHandler, EventSpec, EventType + from reflex_base.utils.types import ArgsSpec WRAP_MAP = { "{": "}", @@ -174,6 +170,7 @@ def to_snake_case(text: str) -> str: return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_") +@lru_cache(maxsize=4096) def to_camel_case(text: str, treat_hyphens_as_underscores: bool = True) -> str: """Convert a string to camel case.