Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/+compile-prop-hot-paths.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping.
1 change: 1 addition & 0 deletions news/+event-chain-interning.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Share one event chain per handler and trigger across call sites, and reuse memoized event wrappers by chain identity during compilation.
1 change: 1 addition & 0 deletions news/+memo-body-analysis.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reuse unchanged memo-body analysis during module emission to reduce repeated rendering and artifact collection.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Share one event chain per handler and trigger across call sites, and reuse memoized event wrappers by chain identity during compilation.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Evaluate generated passthrough memo bodies once and retain their render and artifacts so module emission does not repeat the work.
59 changes: 48 additions & 11 deletions packages/reflex-base/src/reflex_base/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from reflex_base.components.dynamic import load_dynamic_serializer
from reflex_base.components.field import BaseField, FieldBasedMeta
from reflex_base.components.tags import Tag
from reflex_base.components.tags.tag import render_prop
from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode
from reflex_base.constants.compiler import SpecialAttributes
from reflex_base.event import (
Expand Down Expand Up @@ -323,6 +324,7 @@ def _finalize_fields(


_COMPILE_CACHE_ATTRS = (
"_memo_analysis_key",
"_cached_render_result",
"_vars_cache",
"_imports_cache",
Expand Down Expand Up @@ -1161,7 +1163,7 @@ def _render(self, props: dict[str, Any] | None = None) -> Tag:
if props is None:
# Add component props to the tag.
props = {
attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props()
prop.removesuffix("_"): value for prop, value in self._iter_set_props()
}

# Add ref to element if `ref` is None and `id` is not None.
Expand Down Expand Up @@ -1201,6 +1203,39 @@ def get_props(cls) -> Iterable[str]:
"""
return cls.get_js_fields()

@classmethod
@functools.cache
def _get_defaulted_props(cls) -> frozenset[str]:
"""Get the props whose field supplies a value when unset.

Returns:
The props with a default other than ``None`` or a default factory.
"""
return frozenset(
prop
for prop, field_ in cls.get_js_fields().items()
if field_.default_factory is not None
or (field_.default is not MISSING and field_.default is not None)
)

def _iter_set_props(self) -> Iterator[tuple[str, Any]]:
"""Walk the props that carry a value, in declaration order.

An unset prop resolves to ``None`` through its field descriptor and
every consumer drops ``None``, so only props present on the instance
or backed by a class default are read.

Yields:
Each prop name with its value.
"""
values = self.__dict__
defaulted = self._get_defaulted_props()
for prop in self.get_props():
if prop in values:
yield prop, values[prop]
elif prop in defaulted:
yield prop, getattr(self, prop)

@classmethod
@functools.cache
def get_initial_props(cls) -> set[str]:
Expand All @@ -1215,9 +1250,8 @@ def get_initial_props(cls) -> set[str]:
def _get_component_prop_property(self) -> Sequence[BaseComponent]:
return [
component
for prop in self.get_props()
if (value := getattr(self, prop)) is not None
and isinstance(value, (BaseComponent, Var))
for _, value in self._iter_set_props()
if isinstance(value, (BaseComponent, Var))
for component in _components_from(value)
]

Expand Down Expand Up @@ -1438,11 +1472,15 @@ def render(self) -> dict:
except AttributeError:
pass
tag = self._render()
rendered_dict = dict(
tag.set(
children=[child.render() for child in self.children],
)
)
children = [child.render() for child in self.children]
if type(tag) is Tag:
rendered_dict = {}
if (name := render_prop(tag.name)) is not None:
rendered_dict["name"] = name
rendered_dict["props"] = tag.format_props()
rendered_dict["children"] = children
Comment thread
FarhanAliRaza marked this conversation as resolved.
else:
rendered_dict = dict(tag.set(children=children))
self._replace_prop_names(rendered_dict)
self._cached_render_result = rendered_dict
return rendered_dict
Expand Down Expand Up @@ -1581,8 +1619,7 @@ def _get_vars(
vars.extend(event_vars)

# Get Vars associated with component props.
for prop in self.get_props():
prop_var = getattr(self, prop)
for _, prop_var in self._iter_set_props():
if isinstance(prop_var, Var):
vars.append(prop_var)

Expand Down
140 changes: 113 additions & 27 deletions packages/reflex-base/src/reflex_base/components/memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@
from reflex_components_core.base.fragment import Fragment

from reflex_base import constants
from reflex_base.components.component import Component
from reflex_base.components.component import (
BaseComponent,
Component,
_field_values_equal,
)
from reflex_base.components.memoize_helpers import (
MemoizationStrategy,
_var_data_key,
get_memoization_strategy,
)
from reflex_base.constants.compiler import (
Expand All @@ -43,7 +48,7 @@
from reflex_base.registry import RegistrationContext
from reflex_base.utils import console, format, memo_paths
from reflex_base.utils.deterministic_hash import deterministic_hash
from reflex_base.utils.imports import ImportVar
from reflex_base.utils.imports import ImportVar, ParsedImportDict
from reflex_base.utils.types import safe_issubclass, typehint_issubclass
from reflex_base.vars import VarData
from reflex_base.vars.base import LiteralVar, Var
Expand Down Expand Up @@ -1835,6 +1840,70 @@ def _create_component_wrapper(
return _MemoComponentWrapper(definition)


@dataclasses.dataclass(frozen=True, slots=True)
class _MemoBodyAnalysis:
"""Artifacts of a memo body, reusable until its compilation caches are cleared."""

component_type: type[Component]
rendered: dict
style: Any
style_data_key: tuple | None
imports: ParsedImportDict
internal_hooks: dict[str, VarData | None]
hook: str | None
added_hooks: dict[str, VarData | None]
custom_code: str | None
added_custom_code: tuple[list[str], ...]
dynamic_import: str | None
app_wraps: dict[tuple[int, str], Component]

def can_reuse(self, styled: Component) -> bool:
"""Check whether root styling and copying preserved the analyzed inputs.

Args:
styled: Its copy after applying the current app's root style.

Returns:
Whether emission can use the recorded render and artifacts.
"""
return (
type(styled) is self.component_type
and type(styled).__copy__ is BaseComponent.__copy__
and _var_data_key(styled.style._var_data) == self.style_data_key
and _field_values_equal(styled.style, self.style)
)


def _analyze_memo_body(
component: Component, rendered: dict, artifacts: tuple[Any, ...]
) -> _MemoBodyAnalysis:
"""Retain the already-collected passthrough artifacts for module emission.

Args:
component: The body whose children have been replaced by a hole.
rendered: The body's rendered JSX representation.
artifacts: The existing content-hash inputs from ``_component_artifacts``.

Returns:
Analysis shared by content hashing and module emission.
"""
_, imports, internal, hook, added, custom, *remaining = artifacts
return _MemoBodyAnalysis(
component_type=type(component),
rendered=rendered,
style=copy(component.style),
style_data_key=_var_data_key(component.style._var_data),
imports=imports,
internal_hooks=internal,
hook=hook,
added_hooks=added,
custom_code=custom,
added_custom_code=tuple(remaining[:-2]),
dynamic_import=remaining[-2],
app_wraps=remaining[-1],
)


def _component_artifacts(component: Component, *, recursive: bool) -> Iterator[Any]:
"""Yield everything besides the render that identifies a memo body.

Expand Down Expand Up @@ -1893,9 +1962,18 @@ def component_hash(component: Component, *, recursive: bool) -> str:
Returns:
The hex digest content hash.
"""
return deterministic_hash(
component.render(), *_component_artifacts(component, recursive=recursive)
)
if recursive or not component.children:
return deterministic_hash(
component.render(), *_component_artifacts(component, recursive=recursive)
)
rendered = component.render()
artifacts = tuple(_component_artifacts(component, recursive=False))
digest = deterministic_hash(rendered, *artifacts)
analyses = RegistrationContext.ensure_context()._memo_body_analyses
Comment thread
FarhanAliRaza marked this conversation as resolved.
if digest not in analyses:
analyses[digest] = _analyze_memo_body(component, rendered, artifacts)
vars(component)["_memo_analysis_key"] = digest
return digest


def memo_tag(component: Component) -> str:
Expand All @@ -1920,6 +1998,20 @@ def memo_tag(component: Component) -> str:
).capitalize()


_PASSTHROUGH_PARAMS = (
MemoParam(
name="children",
kind=MemoParamKind.CHILDREN,
annotation=Var[Component],
parameter_kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
js_prop_name="children",
placeholder_name="children",
kind_data=None,
default=inspect.Parameter.empty,
),
)


def create_passthrough_component_memo(
component: Component,
source_module: str | None = None,
Expand Down Expand Up @@ -1992,36 +2084,30 @@ def passthrough(children: Var[Component]) -> Component:
object.__setattr__(new_component, "_get_all_refs", component._get_all_refs)
return new_component

# Evaluate once to compute the tag from the rendered memo body shape.
# ``_create_component_definition`` evaluates again internally; that second
# pass appends another, identical hole to ``captured_hole_child``, and the
# ``captured_hole_child[0]`` read below picks up the first.
params = _analyze_params(passthrough, for_component=True)
preview = _normalize_component_return(_evaluate_memo_function(passthrough, params))
if preview is None:
msg = (
"`create_passthrough_component_memo` requires a component that "
"normalizes to `rx.Component`."
)
raise TypeError(msg)
# The compiler owns this fixed signature; no user annotations need resolving.
params = _PASSTHROUGH_PARAMS
rest_target_fields: set[str] = set()
preview = _evaluate_component_body(passthrough, params, rest_target_fields)
tag = memo_tag(preview)

passthrough.__name__ = format.to_snake_case(tag)
passthrough.__qualname__ = passthrough.__name__
passthrough.__module__ = __name__

definition = _create_component_definition(passthrough, Component, source_module)
# ``export_name`` is the content-hashed tag, which reads as noise in the
# React DevTools tree. Name the memo after the Python class it wraps.
replacements: dict[str, Any] = {
"auto_memo_wrapper": True,
"display_name": type(component).__qualname__,
}
if definition.export_name != tag:
replacements["export_name"] = tag
if captured_hole_child:
replacements["passthrough_hole_child"] = captured_hole_child[0]
definition = dataclasses.replace(definition, **replacements)
definition = MemoComponentDefinition(
fn=passthrough,
python_name=passthrough.__name__,
params=params,
source_module=source_module,
export_name=tag,
_component=_LazyBody.ready(preview),
_rest_target_fields=rest_target_fields,
auto_memo_wrapper=True,
display_name=type(component).__qualname__,
passthrough_hole_child=captured_hole_child[0] if captured_hole_child else None,
)

return _create_component_wrapper(definition), definition

Expand Down
Loading
Loading