Conversation
Move the frontend compilation pipeline from App._compile into compiler.compile_app(), introducing a CompilerPlugin protocol with enter_component/leave_component/eval_page/compile_page hooks. Remove the ExecutorType/ExecutorSafeFunctions abstractions in favor of a sequential plugin-driven compilation model.
Move memo component compilation after app_root resolution so app-wrap components are included. Fix DefaultPagePlugin to preserve Var-backed titles instead of replacing them with the default string.
Move _get_app_wrap_components collection outside the `if stateful_component is None` guard so that app wrap components (e.g. UploadFilesProvider) are collected even when a component is wrapped as a stateful component. Add test verifying upload pages correctly emit UploadFilesProvider in the app root.
Remove the class-level tag_to_stateful_component dict and instead thread a compile-scoped stateful_component_cache through compile_from() and create(). This prevents stale cache entries from leaking between independent compilation runs. Also collect imports and app_wrap_components from root components in CompileContext so stateful component libraries and providers (e.g. UploadFilesProvider) are properly propagated. Update benchmarks to inline helpers using the new plugin API and add tests covering shared stateful components across pages and cache isolation between runs.
Component._get_vars had a dead-code cache path: `getattr(self, "__vars", None)` reads the literal attribute `__vars` but `self.__vars = []` writes to the name-mangled `_Component__vars`. The cache branch was never taken, and even if the name-mangling were fixed the missing `return` after `yield from vars` would have caused duplicate yields on repeated calls. Fix the cache (as `_vars_cache`) with a proper early-return. Extend the same per-instance cache pattern to `_get_imports` and `_get_hooks_internal`, which share the same dependency on `event_triggers` / `_get_vars`. Unify invalidation with the existing render-cache clear in `StatefulComponent.create` so all four caches drop together when `_fix_event_triggers` mutates the component.
Delete the legacy StatefulComponent auto-memoization system (compile_from
pre-walk, shared stateful_components.jsx file, references/rendered_as_shared
bookkeeping) and replace it with MemoizeStatefulPlugin that participates in
the single-pass tree walk.
At each memoizable call-site, the plugin wraps the stateful component with
a MemoWrapperComponent that renders as <Tag>{children}</Tag> and emits
`const Tag = memo(({children}) => children)` as page-level custom code.
Event-trigger useCallback hooks are hoisted into the page body. The walker
descends into the wrapped subtree normally, so DefaultCollectorPlugin still
sees the original components for import/hook collection.
Removes:
- StatefulComponent class (~470 lines)
- _compile_stateful_components, _get_shared_components_recursive,
compile_stateful_components, _compile_memoized_components
- get_stateful_components_path, stateful_component_template,
stateful_components_template
- stateful_component kwarg and _compiler_stateful_only_leave_component
flag throughout the plugin hook signature chain
- stateful_components_path/code fields on CompileContext
Adds:
- MemoizeStatefulPlugin + MemoWrapperComponent
- memoize_helpers module in reflex-base (shared with window_events/upload)
- 12 unit tests for the new plugin
Merging this PR will improve performance by ×11
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | test_get_all_imports[_stateful_page] |
3,079.3 µs | 499 µs | ×6.2 |
| 🆕 | test_compile_page_full_context[_stateful_page] |
N/A | 31.4 ms | N/A |
| ⚡ | test_compile_page[_stateful_page] |
10.7 ms | 1.7 ms | ×6.3 |
| 🆕 | test_compile_page_full_context[_complicated_page] |
N/A | 168.7 ms | N/A |
| 🆕 | test_get_all_imports_single_pass[_stateful_page] |
N/A | 772 µs | N/A |
| 🆕 | test_compile_page_single_pass[_complicated_page] |
N/A | 35.1 ms | N/A |
| 🆕 | test_compile_single_pass_all_artifacts[_stateful_page] |
N/A | 13 ms | N/A |
| 🆕 | test_evaluate_page_single_pass[_complicated_page] |
N/A | 46.6 ms | N/A |
| 🆕 | test_evaluate_page_single_pass[_stateful_page] |
N/A | 7.3 ms | N/A |
| 🆕 | test_compile_page_single_pass[_stateful_page] |
N/A | 13.8 ms | N/A |
| 🆕 | test_get_all_imports_single_pass[_complicated_page] |
N/A | 3.3 ms | N/A |
| 🆕 | test_compile_single_pass_all_artifacts[_complicated_page] |
N/A | 33.9 ms | N/A |
| ⚡ | test_get_all_imports[_complicated_page] |
22.9 ms | 2.5 ms | ×9.3 |
| ⚡ | test_compile_page[_complicated_page] |
90.6 ms | 8.2 ms | ×11 |
Comparing FarhanAliRaza:compiler-hooks-memo (58a86f9) with main (1110287)
Footnotes
-
2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Greptile SummaryThis PR is a significant architectural refactor that replaces the legacy
Confidence Score: 4/5Safe to merge for typical single-page components; shared component instances compiled across multiple pages may receive incorrect useCallback wrappers due to the dead shallow-copy in fix_event_triggers_for_memo. One P1 logic bug (copy.copy dead code in fix_event_triggers_for_memo permitting silent mutation of the shared event_triggers dict on reused instances) should be addressed before merge. All other findings are P2 style/fragility concerns that do not block the typical use case. packages/reflex-base/src/reflex_base/components/memoize_helpers.py (fix_event_triggers_for_memo shallow-copy bug) and reflex/compiler/plugins/memoize.py (_memoize_suppress_depth unguarded decrement) Important Files Changed
Sequence DiagramsequenceDiagram
participant Walker
participant Plugin as MemoizeStatefulPlugin
participant Helpers as memoize_helpers
participant Comp as Component
participant PageCtx as PageContext
participant CompileCtx as CompileContext
Walker->>Plugin: enter_component(comp)
Plugin->>Comp: getattr(_memoize_wrapped)
alt _memoize_wrapped is True (re-entry guard)
Plugin->>Comp: del _memoize_wrapped
Plugin-->>Walker: None (pass-through)
else normal first visit
Plugin->>Plugin: _should_memoize(comp)
Plugin->>Comp: comp.render() — cached as _cached_render_result
Plugin->>Plugin: _compute_memo_tag → stable tag name
Plugin->>Helpers: fix_event_triggers_for_memo(comp)
Note over Helpers,Comp: copy.copy is a no-op; event_triggers dict is shared with original
Helpers->>Comp: comp.event_triggers[trigger] = memo_var
Helpers-->>Plugin: [useCallback hook lines]
Plugin->>Helpers: invalidate_event_trigger_caches(comp)
Helpers->>Comp: del _cached_render_result, _vars_cache, _imports_cache, _hooks_internal_cache
Plugin->>PageCtx: hooks[hook_line] = None
Plugin->>CompileCtx: memoize_wrappers[tag] = None
Plugin->>Comp: _memoize_wrapped = True
Plugin->>Plugin: wrapper = MemoWrapperComponent._create(children=[comp])
Plugin-->>Walker: (wrapper, (comp,))
Walker->>Plugin: enter_component(comp) [re-entry as child of wrapper]
Plugin->>Comp: _memoize_wrapped is True — del it
Plugin-->>Walker: None
Walker->>Walker: descend into comp.children normally
Walker->>Plugin: leave_component(comp, ...)
Plugin->>Comp: getattr(_memoize_pushed_suppression)
alt suppression was pushed for this leaf
Plugin->>PageCtx: _memoize_suppress_depth -= 1
Note over Plugin,PageCtx: Unguarded — raises AttributeError if PageContext is fresh (reused component)
Plugin->>Comp: del _memoize_pushed_suppression
end
Plugin-->>Walker: None
end
Reviews (1): Last reviewed commit: "Replace StatefulComponent with {children..." | Re-trigger Greptile |
Override _validate_component_children on the memoize wrapper to be a no-op. The wrapper is a transparent compile-time insertion, so the wrapped child"s _valid_parents constraint is already satisfied by the original user-authored parent (e.g. SelectGroup inside SelectContent) — the wrapper should not interpose on that relationship. Fixes ValueError from integration tests like test_form_submit where `SelectGroup can only be a child of SelectContent` fires when the MemoizeStatefulPlugin wraps SelectGroup with a MemoWrapper during compilation.
…s.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
All Submissions:
Replace StatefulComponent with {children}-pass-through memo wrappers
Delete the legacy StatefulComponent auto-memoization system (compile_from
pre-walk, shared stateful_components.jsx file, references/rendered_as_shared
bookkeeping) and replace it with MemoizeStatefulPlugin that participates in
the single-pass tree walk.
At each memoizable call-site, the plugin wraps the stateful component with
a MemoWrapperComponent that renders as {children} and emits
const Tag = memo(({children}) => children)as page-level custom code.Event-trigger useCallback hooks are hoisted into the page body. The walker
descends into the wrapped subtree normally, so DefaultCollectorPlugin still
sees the original components for import/hook collection.
Removes:
compile_stateful_components, _compile_memoized_components
stateful_components_template
flag throughout the plugin hook signature chain
Adds:
MemoizeStatefulPlugin + MemoWrapperComponent
memoize_helpers module in reflex-base (shared with window_events/upload)
12 unit tests for the new plugin
Have you followed the guidelines stated in CONTRIBUTING.md file?
Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
Please delete options that are not relevant.
New Feature Submission:
Changes To Core Features:
Closes #6213