perf(workbench): split SceneAppBar registration from prop updates

The single $effect that wired SceneAppBar into bottomBarStore was
re-writing barComponent on every reactive tick — every change to
openApps, locale or activeSceneId redirected through .set() and
re-assigned the component reference identically.

Add a setProps() method to bottomBarStore that mutates only barProps,
and split the workbench effect in two: a registration effect that
fires on the scenes-empty/non-empty transition, and a props effect
that pushes fresh data without touching barComponent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-14 16:13:32 +02:00
parent aa29ad860f
commit e95d0487b9
2 changed files with 37 additions and 12 deletions

View file

@ -26,6 +26,17 @@ export const bottomBarStore = {
barComponent = component;
barProps = props;
},
/**
* Update only the props of the currently-registered bar component.
* Use this from reactive blocks that frequently produce fresh prop
* objects (e.g. derived arrays) calling `set(...)` in those
* places needlessly re-writes barComponent every tick, which
* notifies subscribers even when the component identity hasn't
* changed.
*/
setProps(props: Record<string, unknown>) {
barProps = props;
},
clear() {
barComponent = null;
barProps = {};

View file

@ -121,21 +121,35 @@
}
// ── Register SceneAppBar in the layout's bottom-stack ───
// Split into two effects so prop churn (carouselPages re-deriving on
// every openApps change) doesn't re-write barComponent. The first
// effect handles registration/teardown when scenes appear/disappear;
// the second pushes fresh props on every reactive tick.
let barRegistered = $state(false);
$effect(() => {
if (scenes.length > 0) {
bottomBarStore.set(SceneAppBar, {
scenes,
activeSceneId,
pages: carouselPages,
onSceneSelect: (id: string) => workbenchScenesStore.setActiveScene(id),
onSceneCreate: (name: string) => workbenchScenesStore.createScene({ name }),
onSceneContextMenu: handleSceneContextMenu,
onAppClick: scrollToPage,
onAppContextMenu: (e: MouseEvent, id: string) => handleTabContextMenu(e, id),
onAddApp: () => (showPicker = !showPicker),
});
const hasScenes = scenes.length > 0;
if (hasScenes && !barRegistered) {
bottomBarStore.set(SceneAppBar, {});
barRegistered = true;
} else if (!hasScenes && barRegistered) {
bottomBarStore.clear();
barRegistered = false;
}
});
$effect(() => {
if (!barRegistered) return;
bottomBarStore.setProps({
scenes,
activeSceneId,
pages: carouselPages,
onSceneSelect: (id: string) => workbenchScenesStore.setActiveScene(id),
onSceneCreate: (name: string) => workbenchScenesStore.createScene({ name }),
onSceneContextMenu: handleSceneContextMenu,
onAppClick: scrollToPage,
onAppContextMenu: (e: MouseEvent, id: string) => handleTabContextMenu(e, id),
onAddApp: () => (showPicker = !showPicker),
});
});
// ── App CRUD (delegated to active scene) ────────────────
function handleAddApp(appId: string) {