Skip to content

Commit 5500fc4

Browse files
committed
perf: speed up component construction hot path
Pre-resolve each class's field defaults at metaclass time into immutable defaults, factory defaults, and required-field buckets, so BaseComponent's __init__ no longer dispatches through BaseField.default_value per instance. In BaseComponent.__init__ and Component.__init__, write fields via vars(self).update(...) instead of setattr to bypass the freeze guard in __setattr__ — a brand-new instance can't be frozen, so the per-attribute check is pure overhead during init. Other tweaks in Component.__init__: - Lazily allocate event_triggers only when a trigger is actually seen; most components have none, so the up-front dict copy was wasted work. - Single-pass kwargs loop that handles event triggers, var props, and unknown on_* errors inline instead of double-iterating. - Warn when field(default=...) is given a mutable default; nudge users toward default_factory. - Defer the _validate_children imports until after the fast no-op exit.
1 parent 41e141c commit 5500fc4

2 files changed

Lines changed: 85 additions & 56 deletions

File tree

packages/reflex-base/src/reflex_base/components/component.py

Lines changed: 69 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ def field(
129129
if default is not MISSING and default_factory is not None:
130130
msg = "cannot specify both default and default_factory"
131131
raise ValueError(msg)
132+
if default is not MISSING and not types.is_immutable(default):
133+
console.warn(
134+
"Mutable default values are not recommended. "
135+
"Use default_factory instead to avoid unexpected behavior."
136+
)
132137
return ComponentField( # pyright: ignore [reportReturnType]
133138
default=default,
134139
default_factory=default_factory,
@@ -146,6 +151,9 @@ class BaseComponentMeta(FieldBasedMeta, ABCMeta):
146151
_own_fields: Mapping[str, ComponentField]
147152
_fields: Mapping[str, ComponentField]
148153
_js_fields: Mapping[str, ComponentField]
154+
_immutable_field_defaults: Mapping[str, Any]
155+
_factory_field_defaults: Mapping[str, Callable[[], Any]]
156+
_required_fields: tuple[str, ...]
149157

150158
@classmethod
151159
def _process_annotated_fields(
@@ -306,11 +314,23 @@ def __init__(
306314
"""
307315
if "children" in kwargs:
308316
kwargs["children"] = tuple(kwargs["children"])
309-
for key, value in kwargs.items():
310-
setattr(self, key, value)
311-
for name, value in self.get_fields().items():
317+
# Bypass ``__setattr__``'s freeze guard: a brand-new instance can't be
318+
# frozen, so the per-attribute check is pure overhead during init.
319+
d = vars(self)
320+
d.update(kwargs)
321+
cls = type(self)
322+
# Defaults are pre-resolved by the metaclass into immutable/factory
323+
# buckets, so we avoid the ``BaseField.default_value`` dispatch.
324+
for name, default in cls._immutable_field_defaults.items():
312325
if name not in kwargs:
313-
setattr(self, name, value.default_value())
326+
d[name] = default
327+
for name, factory in cls._factory_field_defaults.items():
328+
if name not in kwargs:
329+
d[name] = factory()
330+
for name in cls._required_fields:
331+
if name not in kwargs:
332+
msg = f"Field `{name}` of `{cls.__name__}` requires a value."
333+
raise ValueError(msg)
314334

315335
def __setattr__(self, key: str, value: Any) -> None:
316336
"""Block writes to frozen components, except for cache attributes.
@@ -946,76 +966,70 @@ def _post_init(self, *args, **kwargs):
946966
component_specific_triggers = self.get_event_triggers()
947967
props = self.get_props()
948968

949-
# Add any events triggers.
950-
if "event_triggers" not in kwargs:
951-
kwargs["event_triggers"] = {}
952-
kwargs["event_triggers"] = kwargs["event_triggers"].copy()
969+
# Lazily allocate the event_triggers dict only when a trigger is found.
970+
# Most components have no events; allocating up-front is pure waste.
971+
existing_triggers = kwargs.get("event_triggers")
972+
event_triggers: dict[str, Any] | None = (
973+
dict(existing_triggers) if existing_triggers else None
974+
)
975+
event_keys: list[str] = []
953976

954977
# Iterate through the kwargs and set the props.
955978
for key, value in kwargs.items():
956-
if (
957-
key.startswith("on_")
958-
and key not in component_specific_triggers
959-
and key not in props
960-
):
961-
valid_triggers = sorted(component_specific_triggers.keys())
962-
msg = (
963-
f"The {(comp_name := type(self).__name__)} does not take in an `{key}` event trigger. "
964-
f"Valid triggers for {comp_name}: {valid_triggers}. "
965-
f"If {comp_name} is a third party component make sure to add `{key}` to the component's event triggers. "
966-
f"visit https://reflex.dev/docs/wrapping-react/guide/#event-triggers for more info."
967-
)
968-
raise ValueError(msg)
969979
if key in component_specific_triggers:
970-
# Event triggers are bound to event chains.
971-
is_var = False
972-
elif key in props:
973-
# Set the field type.
974-
is_var = (
975-
field.type_origin is Var if (field := fields.get(key)) else False
980+
if event_triggers is None:
981+
event_triggers = {}
982+
event_triggers[key] = EventChain.create(
983+
value=value,
984+
args_spec=component_specific_triggers[key],
985+
key=key,
976986
)
977-
else:
987+
event_keys.append(key)
978988
continue
979989

980-
# Check whether the key is a component prop.
981-
if is_var:
990+
if key in props:
991+
field = fields.get(key)
992+
if field is None or field.type_origin is not Var:
993+
continue
982994
try:
983995
kwargs[key] = LiteralVar.create(value)
984-
985-
# Get the passed type and the var type.
986996
passed_type = kwargs[key]._var_type
987997
expected_type = typing.get_args(
988998
types.get_field_type(type(self), key)
989999
)[0]
9901000
except TypeError:
991-
# If it is not a valid var, check the base types.
9921001
passed_type = type(value)
9931002
expected_type = types.get_field_type(type(self), key)
9941003

9951004
if not satisfies_type_hint(value, expected_type):
9961005
value_name = value._js_expr if isinstance(value, Var) else value
997-
9981006
additional_info = (
9991007
" You can call `.bool()` on the value to convert it to a boolean."
10001008
if expected_type is bool and isinstance(value, Var)
10011009
else ""
10021010
)
1003-
10041011
raise TypeError(
10051012
f"Invalid var passed for prop {type(self).__name__}.{key}, expected type {expected_type}, got value {value_name} of type {passed_type}."
10061013
+ additional_info
10071014
)
1008-
# Check if the key is an event trigger.
1009-
if key in component_specific_triggers:
1010-
kwargs["event_triggers"][key] = EventChain.create(
1011-
value=value,
1012-
args_spec=component_specific_triggers[key],
1013-
key=key,
1015+
continue
1016+
1017+
if key.startswith("on_"):
1018+
valid_triggers = sorted(component_specific_triggers.keys())
1019+
comp_name = type(self).__name__
1020+
msg = (
1021+
f"The {comp_name} does not take in an `{key}` event trigger. "
1022+
f"Valid triggers for {comp_name}: {valid_triggers}. "
1023+
f"If {comp_name} is a third party component make sure to add `{key}` to the component's event triggers. "
1024+
f"visit https://reflex.dev/docs/wrapping-react/guide/#event-triggers for more info."
10141025
)
1026+
raise ValueError(msg)
10151027

1016-
# Remove any keys that were added as events.
1017-
for key in kwargs["event_triggers"]:
1018-
kwargs.pop(key, None)
1028+
# Promote any registered event triggers; drop the raw on_* keys.
1029+
if event_triggers is not None:
1030+
kwargs["event_triggers"] = event_triggers
1031+
for key in event_keys:
1032+
kwargs.pop(key, None)
10191033

10201034
# Place data_ and aria_ attributes into custom_attrs
10211035
special_attributes = [
@@ -1086,9 +1100,9 @@ def _post_init(self, *args, **kwargs):
10861100
):
10871101
msg = f"Invalid class_name passed for prop {type(self).__name__}.class_name, expected type str, got value {class_name._js_expr} of type {class_name._var_type}."
10881102
raise TypeError(msg)
1089-
# Construct the component.
1090-
for key, value in kwargs.items():
1091-
setattr(self, key, value)
1103+
# Construct the component. Bypass ``__setattr__``'s freeze guard: the
1104+
# instance is freshly created and not yet frozen.
1105+
vars(self).update(kwargs)
10921106

10931107
@classmethod
10941108
def get_event_triggers(cls) -> dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]:
@@ -1300,8 +1314,8 @@ def _unsafe_create(
13001314
children_tuple = tuple(children)
13011315
comp = cls.__new__(cls)
13021316
super(Component, comp).__init__(id=props.get("id"), children=children_tuple)
1303-
for prop, value in props.items():
1304-
setattr(comp, prop, value)
1317+
# Bypass ``__setattr__``'s freeze guard: ``comp`` is not yet frozen.
1318+
vars(comp).update(props)
13051319
comp._freeze()
13061320
return comp
13071321

@@ -1533,19 +1547,18 @@ def _validate_component_children(self, children: list[Component]):
15331547
children: The children of the component.
15341548
15351549
"""
1536-
from reflex_components_core.base.fragment import Fragment
1537-
from reflex_components_core.core.cond import Cond
1538-
from reflex_components_core.core.foreach import Foreach
1539-
from reflex_components_core.core.match import Match
1540-
1541-
no_valid_parents_defined = all(child._valid_parents == [] for child in children)
15421550
if (
15431551
not self._invalid_children
15441552
and not self._valid_children
1545-
and no_valid_parents_defined
1553+
and all(child._valid_parents == [] for child in children)
15461554
):
15471555
return
15481556

1557+
from reflex_components_core.base.fragment import Fragment
1558+
from reflex_components_core.core.cond import Cond
1559+
from reflex_components_core.core.foreach import Foreach
1560+
from reflex_components_core.core.match import Match
1561+
15491562
comp_name = type(self).__name__
15501563
allowed_components = [
15511564
comp.__name__ for comp in (Fragment, Foreach, Cond, Match)

packages/reflex-base/src/reflex_base/components/field.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,19 @@ def _finalize_fields(
181181
namespace["_own_fields"] = own_fields
182182
namespace["_inherited_fields"] = inherited_fields
183183
namespace["_fields"] = all_fields
184+
185+
# Pre-resolve defaults at class definition time so per-instance ``__init__``
186+
# can skip the ``BaseField.default_value`` dispatch entirely.
187+
immutable_defaults: dict[str, Any] = {}
188+
factory_defaults: dict[str, Callable[[], Any]] = {}
189+
required_fields: list[str] = []
190+
for field_name, field in all_fields.items():
191+
if field.default is not MISSING:
192+
immutable_defaults[field_name] = field.default
193+
elif field.default_factory is not None:
194+
factory_defaults[field_name] = field.default_factory
195+
else:
196+
required_fields.append(field_name)
197+
namespace["_immutable_field_defaults"] = immutable_defaults
198+
namespace["_factory_field_defaults"] = factory_defaults
199+
namespace["_required_fields"] = tuple(required_fields)

0 commit comments

Comments
 (0)