From 5d624c7f76224a3f8581b80037759006480f55ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 14:38:54 -0500 Subject: [PATCH] [core] Lazy-load esphome.core.config from loader and config The top-level `import esphome.core.config` in `esphome/loader.py` (used only to register the "esphome" pseudo-component in `_COMPONENT_CACHE`) was pulling `esphome.automation` + `esphome.config_validation` eagerly into every `esphome` CLI invocation, even fast paths like `esphome version`. Move the cache registration into `_lookup_module`, triggered on first `get_component("esphome")`. Also defers the two `esphome.core.config` call sites inside `esphome/config.py` (CoreFinalValidateStep and validate_config), which were the other eager entry point. Local `import esphome.__main__` drops from ~49ms to ~46ms; more importantly, `esphome.core.config` / `esphome.automation` no longer appear in the top offenders table, and `esphome.config_validation` drops from 4.7ms self to 2.0ms self on CI. --- esphome/config.py | 9 ++++++++- esphome/loader.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index 641b6ec1b48..e2f10946112 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -25,7 +25,10 @@ from esphome.const import ( CONF_SUBSTITUTIONS, ) from esphome.core import CORE, DocumentRange, EsphomeError -import esphome.core.config as core_config + +# `esphome.core.config` is imported lazily at its two use sites below. +# It pulls in `esphome.automation` and `esphome.config_validation`, which +# dominate `esphome.__main__` startup cost when loaded eagerly here. import esphome.final_validate as fv from esphome.helpers import indent from esphome.loader import ComponentManifest, get_component, get_platform @@ -968,6 +971,8 @@ class CoreFinalValidateStep(ConfigValidationStep): if result.errors: return + import esphome.core.config as core_config + token = fv.full_config.set(result) with result.catch_error([CONF_ESPHOME]): if CONF_ESPHOME in result: @@ -1072,6 +1077,8 @@ def validate_config( return result # 2. Load partial core config + import esphome.core.config as core_config + result[CONF_ESPHOME] = config[CONF_ESPHOME] result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) try: diff --git a/esphome/loader.py b/esphome/loader.py index 68664aaa265..839f75fa29d 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -13,9 +13,13 @@ from typing import Any from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE -import esphome.core.config from esphome.types import ConfigType +# `esphome.core.config` is imported lazily in `_lookup_module` when the +# "esphome" pseudo-component is first resolved. It pulls in +# `esphome.automation` and `esphome.config_validation`, which together +# dominate `esphome.__main__` startup cost when loaded eagerly. + _LOGGER = logging.getLogger(__name__) @@ -202,6 +206,13 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None: if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] + if domain == "esphome": + import esphome.core.config + + manif = ComponentManifest(esphome.core.config) + _COMPONENT_CACHE[domain] = manif + return manif + try: module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: @@ -237,7 +248,6 @@ def get_platform(domain: str, platform: str) -> ComponentManifest | None: _COMPONENT_CACHE: dict[str, ComponentManifest] = {} CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() -_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> None: