Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
a453ccb
feat(compiler): add incremental compile cache (REFLEX_COMPILE_CACHE)
FarhanAliRaza Jun 28, 2026
8360450
feat(compiler): warm fork-per-compile hot-reload daemon
FarhanAliRaza Jun 28, 2026
7078d9e
perf(docs): build component prop-docs lazily at page eval
FarhanAliRaza Jun 28, 2026
5ba6614
perf(compiler): trim compile-daemon hot reload
FarhanAliRaza Jun 28, 2026
fe06056
perf(compiler): shrink compile-cache manifest, drop in-process cache
FarhanAliRaza Jun 29, 2026
9f1b1c2
refactor(compiler): drop unused page_cache helpers
FarhanAliRaza Jun 29, 2026
43d5351
fix(compiler): track app-level config and memo imports in compile cache
FarhanAliRaza Jun 29, 2026
19b2df4
test(compiler): skip model-metadata daemon test without sqlmodel
FarhanAliRaza Jun 29, 2026
aac67b2
docs(changelog): add news fragments for the incremental compile cache
FarhanAliRaza Jun 29, 2026
519f885
feat(compiler): track runtime module imports in compile cache
FarhanAliRaza Jun 29, 2026
f79b9db
refactor(compiler): simplify page_cache module-file recording
FarhanAliRaza Jun 30, 2026
451be1e
fix(compiler): guard read-tracker recursion on pathlib lazy imports
FarhanAliRaza Jul 1, 2026
724cd3d
feat(compiler): track dynamic app-import reads for the compile cache
FarhanAliRaza Jul 1, 2026
21e7d3e
fix(compiler): close staleness gaps in the incremental compile cache
FarhanAliRaza Jul 3, 2026
14e5a3c
fix(compiler): make compile output deterministic so dev HMR stays gra…
FarhanAliRaza Jul 3, 2026
bf6a8bf
fix(compiler): keep contexts file and HMR intact across hot reloads
FarhanAliRaza Jul 3, 2026
5cb52c5
perf(compiler): cut redundant work in the incremental compile path
FarhanAliRaza Jul 3, 2026
6dbface
test(compiler): scope contexts snapshot to the test file's states
FarhanAliRaza Jul 3, 2026
516d9fe
Merge remote-tracking branch 'upstream/main' into reflex-hmr
FarhanAliRaza Jul 14, 2026
17a31ac
Merge remote-tracking branch 'origin/main' into reflex-hmr
masenf Jul 28, 2026
e765921
Merge remote-tracking branch 'upstream/main' into reflex-hmr
FarhanAliRaza Aug 9, 2026
4cbe7b6
fix(compiler): emit user memo files for modules new to the compile cache
FarhanAliRaza Aug 9, 2026
510f138
compile cache: make the warm fork real, watch with watchfiles, valida…
claude Sep 5, 2026
7e38b65
compile cache: seed generated names per page so partial recompiles st…
claude Sep 5, 2026
e7242f5
compile cache: fix the daemon end to end on a real `reflex run`
claude Sep 5, 2026
612118f
compile cache: probe lock/marker holder liveness without os.kill on W…
claude Sep 5, 2026
8c11a8e
compile cache: gate on import-time state modules and surface cache fa…
FarhanAliRaza Sep 5, 2026
085e816
Merge main and address incremental compile cache review findings
FarhanAliRaza Sep 8, 2026
717aae2
compile cache: fix Windows unit-test failures
FarhanAliRaza Sep 10, 2026
3888659
Merge branch 'main' of https://github.com/reflex-dev/reflex into refl…
FarhanAliRaza Sep 10, 2026
83dedd1
compile cache: persist the import set after a memo-only rewrite
FarhanAliRaza Sep 10, 2026
f2bd03e
compile cache: key the manifest on compile mode and adopt evaluated p…
FarhanAliRaza Sep 10, 2026
81c6235
Merge remote-tracking branch 'upstream/main' into pr-6688
FarhanAliRaza Sep 14, 2026
2b45a6a
Fix cached state slices
FarhanAliRaza Sep 14, 2026
470d72a
Merge upstream/main into reflex-hmr
FarhanAliRaza Oct 20, 2026
e4c64bf
fix(compiler): address compile cache review findings
FarhanAliRaza Oct 20, 2026
52f16d8
fix(compile-daemon): own the daemon in the run scope and log through …
FarhanAliRaza Oct 20, 2026
bd16ff1
fix(compile-daemon): mark daemon ownership with OS locks, not pid files
FarhanAliRaza Oct 20, 2026
9f52380
perf(compiler): predict dynamic state names, verify, retry exactly on…
FarhanAliRaza Oct 20, 2026
6c5457e
test: wait for OS lock cleanup after killed holders exit
masenf Oct 20, 2026
93b2ce7
fix(compiler): let a page gain or lose states without a full compile
FarhanAliRaza Oct 20, 2026
7f62b86
perf(compiler): rebuild the app root from recorded inputs, not a full…
FarhanAliRaza Oct 20, 2026
994ce69
perf(compiler): fail a name prediction only on families other pages s…
FarhanAliRaza Oct 20, 2026
966280e
perf(compiler): add, remove and re-register pages without a full compile
FarhanAliRaza Oct 20, 2026
53b16f9
perf(compiler): rebuild the contexts for module-level state edits, no…
FarhanAliRaza Oct 20, 2026
5ba0f85
perf(compiler): predict ComponentState instance numbers per page
FarhanAliRaza Oct 20, 2026
2ce8c71
perf(compile-daemon): clear only the model registries the parent loaded
FarhanAliRaza Oct 20, 2026
564c32c
perf(compiler): track a page's own module by the code its function reads
FarhanAliRaza Oct 20, 2026
90fa2e8
perf(compiler): predict dynamic state names in cold compiles too
FarhanAliRaza Oct 20, 2026
a51e8b2
fix(compiler): address compile cache review findings
FarhanAliRaza Oct 20, 2026
1354854
test(compiler): advance timestamps for slice digest edits
masenf Oct 20, 2026
98e0259
Retry compile-input record replacement blocked by Windows readers
masenf Oct 21, 2026
0d40332
fix(compiler): address second round of compile cache review findings
FarhanAliRaza Oct 21, 2026
a8f05fb
fix(compiler): keep the compile manifest consistent with uncached output
FarhanAliRaza Oct 21, 2026
3565bb6
fix(compiler): watch every recorded input; stop aborting a released b…
FarhanAliRaza Oct 21, 2026
1a465cb
fix(compiler): attribute reads to modules, mark racy edits, track mor…
claude Oct 21, 2026
b88452e
refactor(compiler): content-addressed auto memos; app root and page s…
claude Oct 21, 2026
8894621
fix(compiler): deterministic generated names across import, pages, st…
claude Oct 21, 2026
0d144dc
fix(compile-daemon): keep library-created dynamic states, private byt…
claude Oct 21, 2026
495a0ef
fix(compile-daemon): load the manifest once per reset; a restarted da…
claude Oct 21, 2026
faed563
fix(compile-cache): address Greptile's four P1 findings
FarhanAliRaza Oct 22, 2026
396f418
fix(compile-cache): address cubic's review of the recent commits
FarhanAliRaza Oct 22, 2026
9752de1
test(memo): let React Compiler compile the derived-count memo
FarhanAliRaza Oct 22, 2026
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
77 changes: 47 additions & 30 deletions docs/app/reflex_docs/pages/docs/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,36 @@ def get_code_style(color: str):
"Union": "gray",
}

count = 0

def get_id(identity: str) -> str:
"""Derive a stable state variable name from a property-control identity.

def get_id(s):
global count
count += 1
s = str(count)
hash_object = hashlib.sha256(s.encode())
hex_dig = hash_object.hexdigest()
return "a_" + hex_dig[:8]
Args:
identity: Qualified component, property and control kind.

Returns:
A valid state variable name independent of page evaluation order.
"""
return "a_" + hashlib.sha256(identity.encode()).hexdigest()


class PropDocsState(rx.State):
"""Container for dynamic vars used by the prop docs."""


def _ensure_prop_var(name: str, type_: type, default: object) -> None:
"""Register a property-control variable once, including its setter.

Args:
name: Stable state variable name.
type_: Control value type.
default: Initial control value.
"""
if name not in PropDocsState.__fields__:
PropDocsState.add_var(name, type_, default)
PropDocsState._create_setter(name, getattr(PropDocsState, name))


EXCLUDED_COMPONENTS = [
"Theme",
"ThemePanel",
Expand Down Expand Up @@ -163,11 +177,12 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict
"default_open",
"default_checked",
]:
name = get_id(f"{component.__qualname__}_{prop.name}")
name = get_id(
f"{component.__module__}.{component.__qualname__}.{prop.name}"
)
default = prop.name == "loading" and component.__name__ == "Spinner"
PropDocsState.add_var(name, bool, default)
_ensure_prop_var(name, bool, default)
var = getattr(PropDocsState, name)
PropDocsState._create_setter(name, var)
setter = getattr(PropDocsState, f"set_{name}")
prop_dict[prop.name] = var
return _bool_pills(var, setter)
Expand Down Expand Up @@ -196,20 +211,20 @@ def render_select(prop: PropDocumentation, component: type[Component], prop_dict
if str(lit_arg) != ""
]
option = literal_values[0]
name = get_id(f"{component.__qualname__}_{prop.name}")
PropDocsState.add_var(name, str, option)
name = get_id(
f"{component.__module__}.{component.__qualname__}.{prop.name}"
)
_ensure_prop_var(name, str, option)
var = getattr(PropDocsState, name)
PropDocsState._create_setter(name, var)
setter = getattr(PropDocsState, f"set_{name}")
prop_dict[prop.name] = var
return _pill_row(literal_values, var, setter)
# Get the first non-empty option.
non_empty_args = [a for a in type_.__args__ if str(a) != ""]
option = non_empty_args[0] if non_empty_args else type_.__args__[0]
name = get_id(f"{component.__qualname__}_{prop.name}")
PropDocsState.add_var(name, str, option)
name = get_id(f"{component.__module__}.{component.__qualname__}.{prop.name}")
_ensure_prop_var(name, str, option)
var = getattr(PropDocsState, name)
PropDocsState._create_setter(name, var)
setter = getattr(PropDocsState, f"set_{name}")
prop_dict[prop.name] = var

Expand Down Expand Up @@ -358,10 +373,11 @@ def prop_docs(
expanded_name = None
expanded = None
if is_long_row:
expanded_name = get_id(f"{component.__qualname__}_{prop.name}_expanded")
PropDocsState.add_var(expanded_name, bool, False)
expanded_name = get_id(
f"{component.__module__}.{component.__qualname__}.{prop.name}.expanded"
)
_ensure_prop_var(expanded_name, bool, False)
expanded = getattr(PropDocsState, expanded_name)
PropDocsState._create_setter(expanded_name, expanded)

cell_content_class = (
rx.cond(expanded, "cell-content", _PROPS_TABLE_COMPACT_CELL_CLASS)
Expand Down Expand Up @@ -949,19 +965,9 @@ def multi_docs(
ll_component_list: list | None = None,
source: str | None = None,
):
shared = shared_html_props([item[0] for item in component_list[1:]])
components = [
component_docs(component_tuple, previews, shared)
for component_tuple in component_list[1:]
]
ll_actual_path = actual_path.replace(".md", "-ll.md")
ll_doc_exists = os.path.exists(ll_actual_path)
ll_list = ll_component_list if ll_component_list is not None else component_list
ll_shared = shared_html_props([item[0] for item in ll_list[1:]])
ll_components = [
component_docs(component_tuple, previews, ll_shared)
for component_tuple in ll_list[1:]
]

def shared_reference(props):
"""Render the common reference once, retaining it in prerendered HTML."""
Expand Down Expand Up @@ -1040,6 +1046,12 @@ def links(current_page, ll_doc_exists, path):
source_path=actual_path,
)
def out():
# Build prop docs during page eval so imports stay cheap.
shared = shared_html_props([item[0] for item in component_list[1:]])
components = [
Comment thread
FarhanAliRaza marked this conversation as resolved.
component_docs(component_tuple, previews, shared)
for component_tuple in component_list[1:]
]
toc = get_docgen_toc(actual_path)
# Reuse the source already read by the caller to avoid a second read.
doc_content = (
Expand Down Expand Up @@ -1092,6 +1104,11 @@ def out():
source_path=ll_actual_path,
)
def ll():
ll_shared = shared_html_props([item[0] for item in ll_list[1:]])
ll_components = [
component_docs(component_tuple, previews, ll_shared)
for component_tuple in ll_list[1:]
]
ll_virtual = virtual_path.replace(".md", "-ll.md")
toc = get_docgen_toc(ll_actual_path)
doc_content = Path(ll_actual_path).read_text(encoding="utf-8")
Expand Down
36 changes: 36 additions & 0 deletions docs/app/tests/test_prop_docs_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Regression coverage for stable property-control identities."""


def test_prop_ids_do_not_depend_on_evaluation_order():
"""Partial page compiles must reuse the same state variable names."""
from reflex_docs.pages.docs.component import get_id

first = get_id("module.Button.size")
other = get_id("module.Text.size")
expanded = get_id("module.Button.size.expanded")
assert get_id("module.Button.size") == first
assert len({first, other, expanded}) == 3


def test_repeated_prop_controls_reuse_their_state_variable():
"""The same component documented twice can share its stable control ID."""
from typing import Literal

import reflex as rx
from reflex_components_radix.themes.components.button import Button
from reflex_docgen import PropDocumentation

from reflex_docs.pages.docs.component import prop_docs, render_select

prop = PropDocumentation(
name="size",
type=rx.Var[Literal["1", "2", "3"]],
description="Long description " * 20,
default_value="1",
)
first = {}
second = {}
render_select(prop, Button, first)
render_select(prop, Button, second)
assert str(first["size"]) == str(second["size"])
assert prop_docs(prop, Button)[2] == prop_docs(prop, Button)[2]
1 change: 1 addition & 0 deletions news/+compile-daemon-windows-record.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid Windows hot-reload failures when a backend worker briefly holds the compile-input record open.
1 change: 1 addition & 0 deletions news/+content-addressed-auto-memos.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Compiler-generated memo components (the auto-memoized stateful subtrees) now compile to one file each under `.web/utils/components/`, named by their content, instead of one file per source module under `.web/app_components/`; identical subtrees anywhere in the app share one file. `@rx.memo` components keep mirroring their source modules.
1 change: 1 addition & 0 deletions news/6688.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added an experimental incremental compile cache, enabled with `REFLEX_COMPILE_CACHE=1`. A compile reuses the previous build in `.web` and recompiles only the pages whose inputs changed, and `reflex run` keeps a warm compile daemon so a hot reload rebuilds only what the edit touched. Off by default.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6688.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added an optional per-page source-read recorder hook (`page_source_recorder` in the compiler plugin) used by the incremental compile cache to track the exact files each page reads during evaluation. Auto-generated unique ref names are now reproducible: each page draws them from a sequence seeded by its route, a state's default values from one seeded by the state's name, and whatever compiles after the pages from a sequence that never repeats a name drawn at import. Sets are sorted in compiled literals only; a state delta serializes a set as a plain list again.
40 changes: 39 additions & 1 deletion packages/reflex-base/src/reflex_base/components/memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import dataclasses
import inspect
from collections.abc import Callable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from contextvars import ContextVar
from copy import copy
from enum import Enum
from functools import cache, partial, update_wrapper
Expand Down Expand Up @@ -289,6 +291,12 @@ class MemoDefinition:
# used for memos that can't be mirrored. ``kw_only`` so subclasses can keep
# their own required fields.
source_module: str | None = dataclasses.field(default=None, kw_only=True)
# The page route whose evaluation triggered this memo's registration (see
# ``registration_owner``), or None for memos registered at module import
# outside any page evaluation. The compile cache attributes the memo's
# output file to this route: the file is reusable while the route's page
# is, and is recompiled by re-evaluating that page.
owner_route: str | None = dataclasses.field(default=None, kw_only=True)


@dataclasses.dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -589,6 +597,36 @@ def reset_memo_component_classes() -> None:

MEMOS: dict[tuple[str, str | None], MemoDefinition] = {}

# The route of the page currently being evaluated, if any. Modules imported
# during a page's evaluation register their memos inside this scope, so the
# registered definition records that page as its owner.
_registration_owner: ContextVar[str | None] = ContextVar(
"_registration_owner", default=None
)


@contextmanager
def registration_owner(route: str) -> Iterator[None]:
"""Attribute memo registrations to the page route that triggers them.

Salsa-style entity ownership: a memo registered while a page evaluates
(e.g. from a module imported inside the page function) is an artifact of
that page's compilation, not of app import. Recording the owning route
lets the compile cache validate the memo's output file by the page's
hit/miss status and recompile it by re-evaluating that page.

Args:
route: The route of the page being evaluated.

Yields:
None.
"""
token = _registration_owner.set(route)
try:
yield
finally:
_registration_owner.reset(token)


def _memo_registry_key(definition: MemoDefinition) -> tuple[str, str | None]:
"""Get the registry key for a memo.
Expand Down Expand Up @@ -650,7 +688,7 @@ def _register_memo_definition(definition: MemoDefinition) -> None:
)
raise ValueError(msg)

MEMOS[key] = definition
MEMOS[key] = dataclasses.replace(definition, owner_route=_registration_owner.get())


def _annotation_inner_type(annotation: Any) -> Any:
Expand Down
17 changes: 17 additions & 0 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,23 @@ class EnvironmentVariables:
# If this env var is set to "yes", App.compile will be a no-op
REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)

# Experimental: incremental compile cache. A fresh compile process (e.g. a
# reflex-run hot-reload worker) reuses each page's compiled output from an
# on-disk manifest and recompiles only the pages whose source changed.
# See reflex/compiler/disk_cache.py and reflex/compiler/page_cache.py.
REFLEX_COMPILE_CACHE: EnvVar[bool] = env_var(False)

# Set only in the compile daemon's own process (and its forked children):
# marks the process that owns .web while a daemon runs, so backend reload
# workers of the same `reflex run` skip frontend compilation.
REFLEX_COMPILE_DAEMON: EnvVar[bool] = env_var(False, internal=True)

# Set by the compile daemon for its compile children: a file the child
# appends its telemetry events to instead of sending them, which the
# daemon sends once the child exited (a forked child must not start the
# telemetry thread, nor touch macOS's proxy lookup after fork).
REFLEX_TELEMETRY_SPOOL: EnvVar[Path | None] = env_var(None, internal=True)

# Inherited by uvicorn/granian reload workers so the backend can distinguish
# dev reload-capable worker boots from other backend starts. Never set in prod.
REFLEX_DEV_BACKEND_RELOAD_ACTIVE: EnvVar[bool] = env_var(False, internal=True)
Expand Down
Loading
Loading