Compare commits

...

4 Commits

Author SHA1 Message Date
Paul Bottein
3ae3b8c9fc clean up 2026-03-12 15:44:30 +01:00
Paul Bottein
6749ae330b Improve viewCache cleanup 2026-03-12 11:53:30 +01:00
Paul Bottein
1d424ec375 Remove unused type 2026-03-12 09:36:54 +01:00
Paul Bottein
a02fe1d258 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>
2026-03-11 18:11:39 +01:00
4 changed files with 46 additions and 96 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";
@@ -156,7 +155,7 @@ class HUIRoot extends LitElement {
private _configChangedByUndo = false;
private _viewCache?: Record<string, HUIView>;
private _viewCache: Record<string, HUIView> = {};
private _viewScrollPositions: Record<string, number> = {};
@@ -170,23 +169,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[] = [];
@@ -632,7 +618,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>
@@ -762,7 +747,6 @@ class HUIRoot extends LitElement {
}
let newSelectView;
let force = false;
let viewPath: string | undefined = this.route!.path.split("/")[1];
viewPath = viewPath ? decodeURI(viewPath) : undefined;
@@ -794,9 +778,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) {
@@ -815,15 +798,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")) {
@@ -835,7 +815,7 @@ class HUIRoot extends LitElement {
scrollTo({ behavior: "auto", top: position })
);
}
this._selectView(newSelectView, force);
this._selectView(newSelectView);
});
}
}
@@ -1162,8 +1142,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;
}
@@ -1176,11 +1167,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;
@@ -1208,12 +1194,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);
@@ -325,6 +279,12 @@ export class HUIView extends ReactiveElement {
viewConfig: LovelaceViewConfig,
isStrategy: boolean
) {
if (isStrategy && deepEqual(viewConfig, this._config)) {
return;
}
this._config = viewConfig;
// Create a new layout element if necessary.
let addLayoutElement = false;
@@ -332,7 +292,6 @@ export class HUIView extends ReactiveElement {
addLayoutElement = true;
this._createLayoutElement(viewConfig);
}
this._layoutElementConfig = viewConfig;
this._createBadges(viewConfig);
this._createCards(viewConfig);
this._createSections(viewConfig);
@@ -355,9 +314,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);
}