mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
perf(workbench): persistent IO + stable bar callbacks
- PageCarousel now creates the IntersectionObserver exactly once when the track mounts and diffs observed wrappers on pages changes, instead of tearing down and rebuilding the entire IO on every add/remove. Already-mounted pages no longer re-fire the intersection callback on each reactive tick. - SceneAppBar receives stable callback identities. The bar-props effect previously recreated fresh inline arrows on every reactive pass (carouselPages / appTitles / DEFAULT_WIDTH change), forcing the bar to see new props even when only data changed. Hoisted handlers make the bar's prop diff a pure data comparison. - `createScene` failures from the bar now surface in the console instead of silently rejecting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
38b9fdb91b
commit
bd1e273f60
2 changed files with 65 additions and 12 deletions
|
|
@ -78,14 +78,20 @@
|
|||
});
|
||||
|
||||
let trackEl = $state<HTMLDivElement | null>(null);
|
||||
let io = $state<IntersectionObserver | null>(null);
|
||||
// Per-id element refs so we can diff add/remove instead of disconnecting
|
||||
// and re-observing every wrapper on each pages change. Svelte's keyed
|
||||
// {#each} preserves DOM nodes per id, so a given id → element mapping is
|
||||
// stable for the id's lifetime.
|
||||
const observed = new Map<string, HTMLElement>();
|
||||
|
||||
// Create the IntersectionObserver exactly once — when the track element
|
||||
// mounts. Previously the IO was torn down and rebuilt on every
|
||||
// pages.length change, forcing every still-visible wrapper to re-fire
|
||||
// the intersection callback. Now it persists across adds/removes.
|
||||
$effect(() => {
|
||||
if (!trackEl || typeof IntersectionObserver === 'undefined') return;
|
||||
// Track pages.length so the effect re-runs (and re-observes
|
||||
// freshly-added wrappers) when the user adds or removes an app.
|
||||
// Cheap to tear down and recreate; the IO has no internal state
|
||||
// we care to preserve.
|
||||
void pages.length;
|
||||
const io = new IntersectionObserver(
|
||||
const instance = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue;
|
||||
|
|
@ -101,9 +107,39 @@
|
|||
threshold: 0.01,
|
||||
}
|
||||
);
|
||||
io = instance;
|
||||
return () => {
|
||||
instance.disconnect();
|
||||
observed.clear();
|
||||
io = null;
|
||||
};
|
||||
});
|
||||
|
||||
// Sync the observed set with the current `pages`: observe new wrappers,
|
||||
// unobserve wrappers that were removed. Re-runs only when `pages`
|
||||
// identity (add/remove/reorder) or `io` changes.
|
||||
$effect(() => {
|
||||
const instance = io;
|
||||
if (!instance || !trackEl) return;
|
||||
// Reading pages.length subscribes this effect to additions/removals.
|
||||
void pages.length;
|
||||
const wrappers = trackEl.querySelectorAll<HTMLElement>('[data-page-id]');
|
||||
wrappers.forEach((el) => io.observe(el));
|
||||
return () => io.disconnect();
|
||||
const nextIds = new Set<string>();
|
||||
for (const el of wrappers) {
|
||||
const id = el.dataset.pageId;
|
||||
if (!id) continue;
|
||||
nextIds.add(id);
|
||||
if (!observed.has(id)) {
|
||||
instance.observe(el);
|
||||
observed.set(id, el);
|
||||
}
|
||||
}
|
||||
for (const [id, el] of observed) {
|
||||
if (!nextIds.has(id)) {
|
||||
instance.unobserve(el);
|
||||
observed.delete(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,23 @@
|
|||
// 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.
|
||||
//
|
||||
// Callback identities are stable across ticks — previously these were
|
||||
// inline arrows re-created on every reactive pass, which forced the
|
||||
// bar to see new props every tick even when only the scene/page data
|
||||
// actually changed.
|
||||
function handleBarSceneSelect(id: string) {
|
||||
workbenchScenesStore.setActiveScene(id);
|
||||
}
|
||||
function handleBarSceneCreate(name: string) {
|
||||
workbenchScenesStore.createScene({ name }).catch((err) => {
|
||||
console.error('[workbench] createScene failed:', err);
|
||||
});
|
||||
}
|
||||
function handleBarToggleShowPicker() {
|
||||
showPicker = !showPicker;
|
||||
}
|
||||
|
||||
let barRegistered = $state(false);
|
||||
$effect(() => {
|
||||
const hasScenes = scenes.length > 0;
|
||||
|
|
@ -244,12 +261,12 @@
|
|||
scenes,
|
||||
activeSceneId,
|
||||
pages: carouselPages,
|
||||
onSceneSelect: (id: string) => workbenchScenesStore.setActiveScene(id),
|
||||
onSceneCreate: (name: string) => workbenchScenesStore.createScene({ name }),
|
||||
onSceneSelect: handleBarSceneSelect,
|
||||
onSceneCreate: handleBarSceneCreate,
|
||||
onSceneContextMenu: handleSceneContextMenu,
|
||||
onAppClick: scrollToPage,
|
||||
onAppContextMenu: (e: MouseEvent, id: string) => handleTabContextMenu(e, id),
|
||||
onAddApp: () => (showPicker = !showPicker),
|
||||
onAppContextMenu: handleTabContextMenu,
|
||||
onAddApp: handleBarToggleShowPicker,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue