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/6947.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Compiling an app no longer leaves the auto-memoization naming caches behind. They are released when the compile finishes — including on `reflex export` and `reflex compile`, and when a compile fails — so a long-running process does not accumulate them.
3 changes: 3 additions & 0 deletions packages/reflex-base/news/6947.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Auto-memoized components whose module-level code came from `add_custom_code` no longer collide on a generated memo name. Two otherwise-identical components emitting different custom code shared one memo module, so one of their two code blocks was dropped from the compiled output.

Auto-memoized components that emit dynamic imports, or that share a class name with a component from another module, no longer collide on a generated memo name either. Both cases produced one memo module where two were needed, dropping a dynamic import statement or one class's compiled body.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6947.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Component content hashing, which auto-memoization runs for every memoized component during a compile, is 2.3-2.6x faster on large pages. The hash now buffers its encoding instead of feeding the hasher one value at a time, dispatches on the exact type, and caches the encoded form of the strings and `ImportVar`s that recur across every component. Generated memo module names change as a result; nothing outside the compiled output refers to them.
149 changes: 0 additions & 149 deletions packages/reflex-base/src/reflex_base/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@
import contextlib
import copy
import dataclasses
import enum
import functools
import logging
import operator
import typing
from abc import ABC, ABCMeta, abstractmethod
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from dataclasses import _MISSING_TYPE, MISSING
from hashlib import md5
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast

Expand Down Expand Up @@ -610,88 +608,6 @@ def _components_from(
return ()


def _hash_str(value: str) -> str:
return md5(f'"{value}"'.encode(), usedforsecurity=False).hexdigest()


def _update_deterministic_hash(hasher: Any, value: object) -> None:
"""Feed ``value`` into ``hasher`` using a self-delimiting, type-tagged encoding.

Each branch writes a distinct type tag plus length-prefixed payload, which
keeps the encoding injective without building intermediate strings — the
nested ``str([...])`` approach this replaces was the dominant cost of
``_deterministic_hash`` (~4x speedup on synthetic, ~2x on real renders).

Args:
hasher: A ``hashlib`` hasher (must accept ``.update(bytes)``).
value: The value to fold into the hasher.

Raises:
TypeError: If the value is not hashable.
"""
if value is None:
hasher.update(b"N")
elif isinstance(value, bool):
hasher.update(b"T" if value else b"F")
elif isinstance(value, (int, float, enum.Enum)):
hasher.update(b"n")
hasher.update(str(value).encode())
elif isinstance(value, str):
encoded = value.encode()
hasher.update(b"s")
hasher.update(len(encoded).to_bytes(8, "little"))
hasher.update(encoded)
elif isinstance(value, dict):
items = sorted(value.items(), key=operator.itemgetter(0))
hasher.update(b"d")
hasher.update(len(items).to_bytes(8, "little"))
for k, v in items:
_update_deterministic_hash(hasher, k)
_update_deterministic_hash(hasher, v)
elif isinstance(value, (tuple, list)):
hasher.update(b"l")
hasher.update(len(value).to_bytes(8, "little"))
for item in value:
_update_deterministic_hash(hasher, item)
elif isinstance(value, Var):
hasher.update(b"v")
_update_deterministic_hash(hasher, value._js_expr)
_update_deterministic_hash(hasher, value._get_all_var_data())
elif dataclasses.is_dataclass(value):
fields = dataclasses.fields(value)
hasher.update(b"D")
hasher.update(len(fields).to_bytes(8, "little"))
for field in fields:
hasher.update(field.name.encode())
_update_deterministic_hash(hasher, getattr(value, field.name))
elif isinstance(value, BaseComponent):
hasher.update(b"C")
_update_deterministic_hash(hasher, value.render())
else:
msg = (
f"Cannot hash value `{value}` of type `{type(value).__name__}`. "
"Only BaseComponent, Var, VarData, dict, str, tuple, and enum.Enum are supported."
)
raise TypeError(msg)


def _deterministic_hash(value: object) -> str:
"""Hash a rendered dictionary.

Args:
value: The dictionary to hash.

Returns:
The hash of the dictionary.

Raises:
TypeError: If the value is not hashable.
"""
hasher = md5(usedforsecurity=False)
_update_deterministic_hash(hasher, value)
return hasher.hexdigest()


@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
class TriggerDefinition:
"""A default event trigger with its args spec and description."""
Expand Down Expand Up @@ -1495,71 +1411,6 @@ def render(self) -> dict:
self._cached_render_result = rendered_dict
return rendered_dict

def _get_component_hash(self, shallow: bool = False) -> str:
"""Get a stable content hash for this component.

The hash incorporates the rendered JSX dict plus the component's
recursive imports, hooks (including internal lifecycle hooks),
custom code, and app-wrap components, so two components that
compile to semantically distinct JS modules hash differently
even when their ``render()`` output happens to match (e.g. two
components differing only in ``on_mount``, which is excluded
from ``_render`` props but lives in the lifecycle hook).

Args:
shallow: If True, only hash the component's own render output and
directly defined hooks, imports, custom code, and app-wrap
components, excluding any of those from child components.

Returns:
The hex digest content hash.
"""
hasher = md5(usedforsecurity=False)
_update_deterministic_hash(hasher, self.render())
if shallow:
# For non-snapshot strategies, we only hash the component's own hooks, imports, custom code, and app-wrap components
_update_deterministic_hash(hasher, dict(self._get_imports()))
_update_deterministic_hash(hasher, dict(self._get_hooks_internal()))
_update_deterministic_hash(hasher, dict(self._get_added_hooks()))
_update_deterministic_hash(hasher, self._get_hooks())
_update_deterministic_hash(hasher, self._get_custom_code())
_update_deterministic_hash(hasher, dict(self._get_app_wrap_components()))
else:
_update_deterministic_hash(hasher, dict(self._get_all_imports()))
_update_deterministic_hash(hasher, dict(self._get_all_hooks_internal()))
_update_deterministic_hash(hasher, dict(self._get_all_hooks()))
_update_deterministic_hash(hasher, dict(self._get_all_custom_code()))
_update_deterministic_hash(
hasher, dict(self._get_all_app_wrap_components())
)
return hasher.hexdigest()

def _compute_memo_tag(self) -> str:
"""Compute a stable tag name for memoizing this component.

The class qualname is encoded directly in the tag prefix so that
distinct classes which happen to render identically never collide
on a tag. Tag collision would silently share a single cached memo
wrapper across classes and drop the later class's class-level
metadata (e.g. ``_get_app_wrap_components``, which carries
providers like ``UploadFilesProvider`` that must reach the app
root).

Returns:
The stable tag name.
"""
from reflex_base.components.memoize_helpers import (
MemoizationStrategy,
get_memoization_strategy,
)

comp_hash = self._get_component_hash(
shallow=get_memoization_strategy(self) == MemoizationStrategy.PASSTHROUGH
)
return format.format_state_name(
f"{type(self).__qualname__}_{self.tag or 'Comp'}_{comp_hash}"
).capitalize()

def _replace_prop_names(self, rendered_dict: dict) -> None:
"""Replace the prop names in the render dictionary.

Expand Down
Loading
Loading