Skip to content
Merged
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.
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. Tags now render through `render(children)`: `CommonTag` holds the generic protocol shared by every tag class, and `Tag` overrides it with a direct fast path.
54 changes: 41 additions & 13 deletions packages/reflex-base/src/reflex_base/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from reflex_base.breakpoints import Breakpoints
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 import CommonTag, Tag
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 @@ -1143,7 +1143,7 @@ def _get_tag_name(self) -> str:
name = '"' + name + '"'
return name

def _render(self, props: dict[str, Any] | None = None) -> Tag:
def _render(self, props: dict[str, Any] | None = None) -> CommonTag:
"""Define how to render the component in React.

Args:
Expand All @@ -1161,7 +1161,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 +1201,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]
Comment thread
FarhanAliRaza marked this conversation as resolved.
elif prop in defaulted:
yield prop, getattr(self, prop)

@classmethod
@functools.cache
def get_initial_props(cls) -> set[str]:
Expand All @@ -1215,9 +1248,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 +1470,8 @@ 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]
rendered_dict = tag.render(children)
self._replace_prop_names(rendered_dict)
self._cached_render_result = rendered_dict
return rendered_dict
Expand Down Expand Up @@ -1581,8 +1610,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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from .cond_tag import CondTag
from .iter_tag import IterTag
from .match_tag import MatchTag
from .tag import Tag
from .tag import CommonTag, Tag

__all__ = ["CondTag", "IterTag", "MatchTag", "Tag"]
__all__ = ["CommonTag", "CondTag", "IterTag", "MatchTag", "Tag"]
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from collections.abc import Iterator, Mapping
from typing import Any

from reflex_base.components.tags.tag import Tag
from reflex_base.components.tags.tag import CommonTag


@dataclasses.dataclass(frozen=True, kw_only=True)
class CondTag(Tag):
class CondTag(CommonTag):
"""A conditional tag."""

# The condition to determine which component to render.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING

from reflex_base.components.tags.tag import Tag
from reflex_base.components.tags.tag import CommonTag
from reflex_base.utils.types import GenericType
from reflex_base.vars import LiteralArrayVar, Var, get_unique_variable_name
from reflex_base.vars.sequence import _determine_value_of_array_index
Expand All @@ -17,7 +17,7 @@


@dataclasses.dataclass(frozen=True)
class IterTag(Tag):
class IterTag(CommonTag):
"""An iterator tag."""

# The var to iterate over.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from collections.abc import Iterator, Mapping, Sequence
from typing import Any

from reflex_base.components.tags.tag import Tag
from reflex_base.components.tags.tag import CommonTag


@dataclasses.dataclass(frozen=True, kw_only=True)
class MatchTag(Tag):
class MatchTag(CommonTag):
"""A match tag."""

# The condition to determine which case to match.
Expand Down
51 changes: 47 additions & 4 deletions packages/reflex-base/src/reflex_base/components/tags/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from collections.abc import Iterator, Mapping, Sequence
from typing import Any

from typing_extensions import Self

from reflex_base.event import EventChain
from reflex_base.utils import format
from reflex_base.vars.base import LiteralVar, Var
Expand All @@ -20,6 +22,9 @@ def render_prop(value: Any) -> Any:
Returns:
The rendered value.
"""
if type(value) in (str, dict):
return value

from reflex_base.components.component import BaseComponent

if isinstance(value, BaseComponent):
Expand All @@ -32,8 +37,8 @@ def render_prop(value: Any) -> Any:


@dataclasses.dataclass(frozen=True)
class Tag:
"""A React tag."""
class CommonTag:
"""The fields and render protocol shared by every tag."""

# The name of the tag.
name: str = ""
Expand All @@ -55,6 +60,20 @@ def format_props(self) -> list[str]:
"""
return format.format_props(*self.special_props, **self.props)

def render(self, children: Sequence[Any] | None = None) -> dict[str, Any]:
"""Render the tag into the dictionary consumed by the templates.

Args:
children: The already rendered children, or None to render the
tag's own children.

Returns:
The rendered tag dictionary.
"""
if children is not None:
return dict(self.set(children=children))
return dict(self)

def set(self, **kwargs: Any):
"""Return a new tag with the given fields set.

Expand All @@ -80,7 +99,7 @@ def __iter__(self) -> Iterator[tuple[str, Any]]:
if rendered_value is not None:
yield field.name, rendered_value

def add_props(self, **kwargs: Any | None) -> Tag:
def add_props(self, **kwargs: Any | None) -> Self:
"""Return a new tag with the given props added.

Args:
Expand All @@ -105,7 +124,7 @@ def add_props(self, **kwargs: Any | None) -> Tag:
},
)

def remove_props(self, *args: str) -> Tag:
def remove_props(self, *args: str) -> Self:
"""Return a new tag with the given props removed.

Args:
Expand Down Expand Up @@ -135,3 +154,27 @@ def is_valid_prop(prop: Var | None) -> bool:
Whether the prop is valid.
"""
return prop is not None and not (isinstance(prop, dict) and len(prop) == 0)


@dataclasses.dataclass(frozen=True)
class Tag(CommonTag):
"""A React tag."""

def render(self, children: Sequence[Any] | None = None) -> dict[str, Any]:
"""Render the tag without going through the generic field protocol.

Args:
children: The already rendered children, or None to render the
tag's own children.

Returns:
The rendered tag dictionary.
"""
rendered: dict[str, Any] = {}
if (name := render_prop(self.name)) is not None:
rendered["name"] = name
rendered["props"] = self.format_props()
rendered["children"] = (
children if children is not None else render_prop(self.children)
)
return rendered
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import dataclasses

from reflex_base.components.tags import Tag
from reflex_base.components.tags import CommonTag
from reflex_base.utils import format


@dataclasses.dataclass(frozen=True, kw_only=True)
class Tagless(Tag):
class Tagless(CommonTag):
"""A tag with no tag."""

# The inner contents of the tag.
Expand Down
52 changes: 41 additions & 11 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import logging
import re
import string
import uuid
import warnings
from abc import ABCMeta
from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence
Expand Down Expand Up @@ -115,6 +114,37 @@ class VarSubclassEntry:

_var_subclasses: list[VarSubclassEntry] = []
_var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = []
# Exact value type -> the literal class claiming it, or None when no literal
# class does. Reset whenever a literal subclass registers.
_literal_var_by_type: dict[type, type[LiteralVar] | None] = {}
Comment thread
FarhanAliRaza marked this conversation as resolved.


def _literal_var_for(value: Any) -> type[LiteralVar] | None:
"""Find the literal Var class claiming ``value``'s type.

Args:
value: The python value to wrap.

Returns:
The matching literal class, or None if no registered class claims it.
"""
value_type = type(value)
try:
return _literal_var_by_type[value_type]
except KeyError:
pass
Comment thread
masenf marked this conversation as resolved.
literal_subclass = next(
(
literal
for literal, var_subclass in reversed(_var_literal_subclasses)
if isinstance(value, var_subclass.python_types)
),
None,
)
# A class object's type is its metaclass, which other classes share.
if not isinstance(value, type):
_literal_var_by_type[value_type] = literal_subclass
return literal_subclass
Comment thread
FarhanAliRaza marked this conversation as resolved.


@functools.cache
Expand Down Expand Up @@ -236,7 +266,7 @@ def insert_app_wraps(
if seen is None:
seen = target.get(key)
if seen is not None:
if seen != wrapper:
if seen is not wrapper and seen != wrapper:
msg = (
f"Conflicting app wraps for {key!r}: two different "
"components claim the same (priority, tag) slot."
Expand Down Expand Up @@ -1651,6 +1681,7 @@ def __init_subclass__(cls, **kwargs):
_var_literal_subclasses.remove(var_literal_subclass)

_var_literal_subclasses.append((cls, var_subclass))
_literal_var_by_type.clear()

@classmethod
def _create_literal_var(
Expand Down Expand Up @@ -1678,9 +1709,8 @@ def _create_literal_var(
return value
return value._replace(merge_var_data=_var_data)

for literal_subclass, var_subclass in _var_literal_subclasses[::-1]:
if isinstance(value, var_subclass.python_types):
return literal_subclass.create(value, _var_data=_var_data)
if (literal_subclass := _literal_var_for(value)) is not None:
return literal_subclass.create(value, _var_data=_var_data)

if (
(as_var_method := getattr(value, "_as_var", None)) is not None
Expand Down Expand Up @@ -1760,9 +1790,8 @@ def _get_all_var_data_without_creating_var_dispatch(
if isinstance(value, Var):
return value._get_all_var_data()

for literal_subclass, var_subclass in _var_literal_subclasses[::-1]:
if isinstance(value, var_subclass.python_types):
return literal_subclass._get_all_var_data_without_creating_var(value)
if (literal_subclass := _literal_var_for(value)) is not None:
return literal_subclass._get_all_var_data_without_creating_var(value)

if (
(as_var_method := getattr(value, "_as_var", None)) is not None
Expand Down Expand Up @@ -2020,6 +2049,8 @@ def __set_name__(self, owner: Any, name: str):
"""
if self._attrname is None:
self._attrname = name
self._cached_field_name = "_reflex_cache_" + name
cached_field_name = self._cached_field_name

original_del = getattr(owner, "__del__", None)

Expand All @@ -2029,7 +2060,6 @@ def delete_property(this: Any):
Args:
this: The object to delete the cached property from.
"""
cached_field_name = "_reflex_cache_" + name
try:
unique_id = object.__getattribute__(this, cached_field_name)
except AttributeError:
Expand Down Expand Up @@ -2067,11 +2097,11 @@ def __get__(self, instance: Any, owner: type | None = None):
if self._attrname is None:
msg = "Cannot use cached_property on a class without __set_name__."
raise TypeError(msg)
cached_field_name = "_reflex_cache_" + self._attrname
cached_field_name = self._cached_field_name
try:
unique_id = object.__getattribute__(instance, cached_field_name)
except AttributeError:
unique_id = uuid.uuid4().int
unique_id = object()
object.__setattr__(instance, cached_field_name, unique_id)
if unique_id not in GLOBAL_CACHE:
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Annotate `_render` overrides as returning `CommonTag`, the new base of every tag class.
Loading
Loading