Skip to content
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,7 +6,6 @@
import contextlib
import copy
import dataclasses
import enum
import functools
import json
import logging
Expand All @@ -15,7 +14,6 @@
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 @@ -611,88 +609,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 @@ -1496,71 +1412,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