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
7 changes: 5 additions & 2 deletions .github/workflows/performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
env:
REFLEX_CLI_BENCHMARK_TIMEOUT_SECONDS: "120"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand All @@ -43,9 +45,10 @@ jobs:
run: uv sync --all-extras --dev

- name: Run benchmarks
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
with:
mode: instrumentation
mode: simulation
simulation-track-subprocess: true
run: uv run pytest -v tests/benchmarks --codspeed

lighthouse:
Expand Down
1 change: 1 addition & 0 deletions news/+cli-startup.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reduce CLI startup time by loading component and cloud command implementations only when invoked, and avoid frontend package reinstalls after backend-only config changes.
1 change: 1 addition & 0 deletions news/+version-check-ttl.performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid repeated PyPI requests by caching successful latest-version checks for 24 hours and throttling failed checks for one hour.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reduce CLI startup time and memory by loading compiler plugins and optional SQLAlchemy property support only when used.
148 changes: 105 additions & 43 deletions packages/reflex-base/src/reflex_base/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,112 @@
"""Reflex Plugin System."""

from . import embed, sitemap, tailwind_v3, tailwind_v4
from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin
from .base import (
CommonContext,
Plugin,
PostBuildContext,
PostCompileContext,
PreCompileContext,
RegisterRouteContext,
get_plugin,
)
from .compiler import (
BaseContext,
CompileContext,
CompilerHooks,
ComponentAndChildren,
PageContext,
PageDefinition,
)
from .embed import EmbedPlugin
from .sitemap import SitemapPlugin
from .tailwind_v3 import TailwindV3Plugin
from .tailwind_v4 import TailwindV4Plugin
from typing import TYPE_CHECKING

__all__ = [
"BaseContext",
"CommonContext",
"CompileContext",
"CompilerHooks",
"ComponentAndChildren",
"EmbedPlugin",
"PageContext",
"PageDefinition",
"Plugin",
"PostBuildContext",
"PostCompileContext",
"PreCompileContext",
"RegisterRouteContext",
"SitemapPlugin",
"TailwindV3Plugin",
"TailwindV4Plugin",
"_ScreenshotPlugin",
from reflex_base.utils import lazy_loader

if TYPE_CHECKING:
from . import (
_screenshot,
base,
compiler,
embed,
shared_tailwind,
sitemap,
tailwind_v3,
tailwind_v4,
)
from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin
from .base import (
CommonContext,
Plugin,
PostBuildContext,
PostCompileContext,
PreCompileContext,
RegisterRouteContext,
get_plugin,
)
from .compiler import (
BaseContext,
CompileContext,
CompilerHooks,
ComponentAndChildren,
PageContext,
PageDefinition,
)
from .embed import EmbedPlugin
from .sitemap import SitemapPlugin
from .tailwind_v3 import TailwindV3Plugin
from .tailwind_v4 import TailwindV4Plugin

__all__ = [
"BaseContext",
"CommonContext",
"CompileContext",
"CompilerHooks",
"ComponentAndChildren",
"EmbedPlugin",
"PageContext",
"PageDefinition",
"Plugin",
"PostBuildContext",
"PostCompileContext",
"PreCompileContext",
"RegisterRouteContext",
"SitemapPlugin",
"TailwindV3Plugin",
"TailwindV4Plugin",
"_ScreenshotPlugin",
"embed",
"get_plugin",
"sitemap",
"tailwind_v3",
"tailwind_v4",
]

_SUBMODULES: set[str] = {
"_screenshot",
"base",
"compiler",
"embed",
"get_plugin",
"shared_tailwind",
"sitemap",
"tailwind_v3",
"tailwind_v4",
]
}

_SUBMOD_ATTRS: lazy_loader.SubmodAttrsType = {
"_screenshot": [("ScreenshotPlugin", "_ScreenshotPlugin")],
"base": [
"CommonContext",
"Plugin",
"PostBuildContext",
"PostCompileContext",
"PreCompileContext",
"RegisterRouteContext",
"get_plugin",
],
"compiler": [
"BaseContext",
"CompileContext",
"CompilerHooks",
"ComponentAndChildren",
"PageContext",
"PageDefinition",
],
"embed": ["EmbedPlugin"],
"sitemap": ["SitemapPlugin"],
"tailwind_v3": ["TailwindV3Plugin"],
"tailwind_v4": ["TailwindV4Plugin"],
}

if not TYPE_CHECKING:
__getattr__, __dir__, _lazy_all = lazy_loader.attach(
__name__,
submodules=_SUBMODULES,
submod_attrs=_SUBMOD_ATTRS,
)
__all__ = [
name
for name in _lazy_all
if name not in {"_screenshot", "base", "compiler", "shared_tailwind"}
]
63 changes: 57 additions & 6 deletions packages/reflex-base/src/reflex_base/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
PROPERTY_CLASSES: tuple[type, ...]

# Potential GenericAlias types for isinstance checks.
GenericAliasTypes = (_GenericAlias, GenericAlias, _SpecialGenericAlias)

Expand Down Expand Up @@ -570,11 +573,46 @@ def get_field_type(cls: GenericType, field_name: str) -> GenericType | None:
return type_hints.get(field_name, None)


PROPERTY_CLASSES = (property,)
if find_spec("sqlalchemy") and find_spec("sqlalchemy.ext"):
from sqlalchemy.ext.hybrid import hybrid_property
@lru_cache
def _get_property_classes() -> tuple[type, ...]:
"""Resolve the legacy property-class tuple on explicit access.

Returns:
Python's property class and SQLAlchemy's hybrid property class when
SQLAlchemy is installed.
"""
if find_spec("sqlalchemy") and find_spec("sqlalchemy.ext"):
from sqlalchemy.ext.hybrid import hybrid_property

return property, hybrid_property
return (property,)


def __getattr__(name: str) -> object:
"""Resolve compatibility attributes without importing optional runtimes.

PROPERTY_CLASSES += (hybrid_property,)
Args:
name: The module attribute being requested.

Returns:
The lazily resolved compatibility value.

Raises:
AttributeError: If the module does not define the requested attribute.
"""
if name == "PROPERTY_CLASSES":
return _get_property_classes()
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)


def __dir__() -> list[str]:
"""List module attributes, including lazy compatibility exports.

Returns:
The module's attribute names.
"""
return sorted({*globals(), "PROPERTY_CLASSES"})


def get_property_hint(attr: Any | None) -> GenericType | None:
Expand All @@ -586,9 +624,15 @@ def get_property_hint(attr: Any | None) -> GenericType | None:
Returns:
The type hint of the property, if it is a property, else None.
"""
if not isinstance(attr, PROPERTY_CLASSES) or attr.fget is None:
if not isinstance(attr, property):
sqlalchemy_hybrid = sys.modules.get("sqlalchemy.ext.hybrid")
if sqlalchemy_hybrid is None or not isinstance(
attr, sqlalchemy_hybrid.hybrid_property
):
return None
if (getter := getattr(attr, "fget", None)) is None:
return None
hints = get_type_hints(attr.fget)
hints = get_type_hints(getter)
return hints.get("return", None)


Expand Down Expand Up @@ -1523,3 +1567,10 @@ def is_immutable(i: Any) -> bool:
Whether the value is immutable.
"""
return isinstance(i, IMMUTABLE_TYPES)


if not TYPE_CHECKING:
# Keep the historical wildcard-import surface while allowing the optional
# SQLAlchemy descriptor class to resolve only when that export is used.
__all__ = [name for name in globals() if not name.startswith("_")]
__all__.append("PROPERTY_CLASSES")
4 changes: 2 additions & 2 deletions reflex/custom_components/custom_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from reflex_base import constants
from reflex_base.constants import CustomComponents

from reflex.utils import console, frontend_skeleton
from reflex.utils import console
from reflex.utils.cli_options import log_options

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -494,7 +494,7 @@ def init(
Raises:
SystemExit: If the pyproject.toml already exists.
"""
from reflex.utils import exec
from reflex.utils import exec, frontend_skeleton

if CustomComponents.PYPROJECT_TOML.exists():
logger.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
Expand Down
Loading
Loading