1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Refactor lovelace view lifecycle to avoid unnecessary DOM rebuilds (#30101)

* Refactor lovelace view lifecycle to avoid unnecessary DOM rebuilds

- Remove `force` flag from `hui-root` that was clearing the entire view
  cache and destroying all cached view DOM on any config change. Views
  now receive updated lovelace in place and handle config changes
  internally.
- Add `_cleanupViewCache` to remove stale cache entries when views are
  added, removed, or reordered.
- Remove `@ll-rebuild` handler from `hui-root`. Cards and badges already
  handle `ll-rebuild` via their `hui-card`/`hui-badge` wrappers. Sections
  now always stop propagation and rebuild locally.
- Add `deepEqual` guard in `hui-view._setConfig` and
  `hui-section._initializeConfig` to skip re-rendering when strategy
  regeneration produces an identical config.
- Simplify `hui-view` refresh flow: remove `_refreshConfig`,
  `_rendered` flag, `strategy-config-changed` event, and
  connected/disconnected callbacks. Registry changes now debounce
  directly into `_initializeConfig`.
- Fix `isStrategy` check in `hui-view._initializeConfig` to use the raw
  config (before strategy expansion) rather than the generated config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove unused type

* Improve viewCache cleanup

* clean up

* Handle custom view re-creation

* Fix custom view loading

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Bottein
2026-03-24 17:41:30 +01:00
committed by GitHub
parent 6ea15f507a
commit 89755f274d
4 changed files with 59 additions and 100 deletions

View File

@@ -35,7 +35,6 @@ import {
extractSearchParamsObject,
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { afterNextRender } from "../../common/util/render-status";
import "../../components/ha-button";
import "../../components/ha-dropdown";
@@ -157,7 +156,7 @@ class HUIRoot extends LitElement {
private _configChangedByUndo = false;
private _viewCache?: Record<string, HUIView>;
private _viewCache: Record<string, HUIView> = {};
private _viewScrollPositions: Record<string, number> = {};
@@ -171,23 +170,10 @@ class HUIRoot extends LitElement {
}),
});
private _debouncedConfigChanged: () => void;
private _conversation = memoizeOne((_components) =>
isComponentLoaded(this.hass, "conversation")
);
constructor() {
super();
// The view can trigger a re-render when it knows that certain
// web components have been loaded.
this._debouncedConfigChanged = debounce(
() => this._selectView(this._curView, true),
100,
false
);
}
private _renderActionItems(): TemplateResult {
const result: TemplateResult[] = [];
@@ -633,7 +619,6 @@ class HUIRoot extends LitElement {
.hass=${this.hass}
.theme=${curViewConfig?.theme}
id="view"
@ll-rebuild=${this._debouncedConfigChanged}
>
<hui-view-background .hass=${this.hass} .background=${background}>
</hui-view-background>
@@ -770,7 +755,6 @@ class HUIRoot extends LitElement {
}
let newSelectView;
let force = false;
let viewPath: string | undefined = this.route!.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
@@ -802,9 +786,8 @@ class HUIRoot extends LitElement {
| Lovelace
| undefined;
if (!oldLovelace || oldLovelace.config !== this.lovelace!.config) {
// On config change, recreate the current view from scratch.
force = true;
if (oldLovelace && oldLovelace.config !== this.lovelace!.config) {
this._cleanupViewCache();
}
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
@@ -823,15 +806,12 @@ class HUIRoot extends LitElement {
}
}
if (!force && huiView) {
if (huiView) {
huiView.lovelace = this.lovelace!;
}
}
if (newSelectView !== undefined || force) {
if (force && newSelectView === undefined) {
newSelectView = this._curView;
}
if (newSelectView !== undefined) {
// Will allow for ripples to start rendering
afterNextRender(() => {
if (changedProperties.has("route")) {
@@ -843,7 +823,7 @@ class HUIRoot extends LitElement {
scrollTo({ behavior: "auto", top: position })
);
}
this._selectView(newSelectView, force);
this._selectView(newSelectView);
});
}
}
@@ -1166,8 +1146,19 @@ class HUIRoot extends LitElement {
}
}
private _selectView(viewIndex: HUIRoot["_curView"], force: boolean): void {
if (!force && this._curView === viewIndex) {
private _cleanupViewCache(): void {
// Keep only the currently displayed view to avoid UI flash.
// All other cached views are cleared and will be recreated on next visit.
const currentView =
this._curView != null ? this._viewCache[this._curView] : undefined;
this._viewCache = {};
if (currentView && this._curView != null) {
this._viewCache[this._curView] = currentView;
}
}
private _selectView(viewIndex: HUIRoot["_curView"]): void {
if (this._curView === viewIndex) {
return;
}
@@ -1180,11 +1171,6 @@ class HUIRoot extends LitElement {
this._curView = viewIndex;
if (force) {
this._viewCache = {};
this._viewScrollPositions = {};
}
// Recreate a new element to clear the applied themes.
const root = this._viewRoot;
@@ -1212,12 +1198,12 @@ class HUIRoot extends LitElement {
return;
}
if (!force && this._viewCache![viewIndex]) {
view = this._viewCache![viewIndex];
if (this._viewCache[viewIndex]) {
view = this._viewCache[viewIndex];
} else {
view = document.createElement("hui-view");
view.index = viewIndex;
this._viewCache![viewIndex] = view;
this._viewCache[viewIndex] = view;
}
view.lovelace = this.lovelace;

View File

@@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { storage } from "../../../common/decorators/storage";
import { deepEqual } from "../../../common/util/deep-equal";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-svg-icon";
import type { LovelaceSectionElement } from "../../../data/lovelace";
@@ -165,6 +166,11 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
...sectionConfig,
type: sectionConfig.type || DEFAULT_SECTION_LAYOUT,
};
if (isStrategy && deepEqual(sectionConfig, this._config)) {
return;
}
this._config = sectionConfig;
// Create a new layout element if necessary.

View File

@@ -24,7 +24,6 @@ declare global {
interface HASSDomEvents {
"ll-rebuild": Record<string, unknown>;
"ll-upgrade": Record<string, unknown>;
"ll-badge-rebuild": Record<string, unknown>;
}
}

View File

@@ -3,7 +3,7 @@ import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { storage } from "../../../common/decorators/storage";
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/entity/ha-state-label-badge";
@@ -90,9 +90,7 @@ export class HUIView extends ReactiveElement {
private _layoutElement?: LovelaceViewElement;
private _layoutElementConfig?: LovelaceViewConfig;
private _rendered = false;
private _config?: LovelaceViewConfig;
@storage({
key: "dashboardCardClipboard",
@@ -139,11 +137,8 @@ export class HUIView extends ReactiveElement {
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
// In edit mode let it go to hui-root and rebuild whole view.
if (!this.lovelace!.editMode) {
ev.stopPropagation();
this._rebuildSection(element, sectionConfig);
}
ev.stopPropagation();
this._rebuildSection(element, sectionConfig);
},
{ once: true }
);
@@ -154,18 +149,6 @@ export class HUIView extends ReactiveElement {
return this;
}
connectedCallback(): void {
super.connectedCallback();
this.updateComplete.then(() => {
this._rendered = true;
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._rendered = false;
}
public willUpdate(changedProperties: PropertyValues<typeof this>): void {
super.willUpdate(changedProperties);
@@ -201,51 +184,22 @@ export class HUIView extends ReactiveElement {
const viewConfig = this.lovelace.config.views[this.index];
if (oldHass && this.hass && this.lovelace && isStrategyView(viewConfig)) {
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
this.hass.config.state === "RUNNING" &&
(oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors)
) {
if (this.hass.config.state === "RUNNING") {
// If the page is not rendered yet, we can force the refresh
if (this._rendered) {
this._debounceRefreshConfig(false);
} else {
this._refreshConfig(true);
}
}
this._debounceRefreshConfig();
}
}
}
private _debounceRefreshConfig = debounce(
(force: boolean) => this._refreshConfig(force),
() => this._initializeConfig(),
200
);
private _refreshConfig = async (force: boolean) => {
if (!this.hass || !this.lovelace) {
return;
}
const viewConfig = this.lovelace.config.views[this.index];
if (!isStrategyView(viewConfig)) {
return;
}
const oldConfig = this._layoutElementConfig;
const newConfig = await this._generateConfig(viewConfig);
// Don't ask if the config is the same
if (!deepEqual(newConfig, oldConfig)) {
if (force) {
this._setConfig(newConfig, true);
} else {
fireEvent(this, "strategy-config-changed");
}
}
};
protected update(changedProperties: PropertyValues) {
super.update(changedProperties);
@@ -321,18 +275,22 @@ export class HUIView extends ReactiveElement {
};
}
private async _setConfig(
viewConfig: LovelaceViewConfig,
isStrategy: boolean
) {
private _setConfig(viewConfig: LovelaceViewConfig, isStrategy: boolean) {
if (isStrategy && deepEqual(viewConfig, this._config)) {
return;
}
this._config = viewConfig;
// Create a new layout element if necessary.
let addLayoutElement = false;
if (!this._layoutElement || this._layoutElementType !== viewConfig.type) {
addLayoutElement = true;
this._createLayoutElement(viewConfig);
} else {
this._layoutElement.setConfig(viewConfig);
}
this._layoutElementConfig = viewConfig;
this._createBadges(viewConfig);
this._createCards(viewConfig);
this._createSections(viewConfig);
@@ -355,9 +313,9 @@ export class HUIView extends ReactiveElement {
private async _initializeConfig() {
const rawConfig = this.lovelace.config.views[this.index];
const isStrategy = isStrategyView(rawConfig);
const viewConfig = await this._generateConfig(rawConfig);
const isStrategy = isStrategyView(viewConfig);
this._setConfig(viewConfig, isStrategy);
}
@@ -365,6 +323,16 @@ export class HUIView extends ReactiveElement {
private _createLayoutElement(config: LovelaceViewConfig): void {
this._layoutElement = createViewElement(config) as LovelaceViewElement;
this._layoutElementType = config.type;
this._layoutElement.addEventListener(
"ll-rebuild",
(ev: Event) => {
ev.stopPropagation();
// Force recreation of the layout element
this._layoutElementType = undefined;
this._initializeConfig();
},
{ once: true }
);
this._layoutElement.addEventListener("ll-create-card", (ev) => {
showCreateCardDialog(this, {
lovelaceConfig: this.lovelace.config,