Found during the Beta v0.6 pre-launch QA sweep. Pre-existing.
What happens
There is no <KeepAlive> anywhere in the app — the layout is a bare <router-view/>. So every trip to Recipes / Parts / Change Log / Graph and back fully remounts Planner.vue and all its children.
Several components register eventBus listeners at setup scope with no matching off and no onUnmounted. All of them use anonymous arrow functions, so they cannot be removed even by hand afterwards.
| component |
.on |
.off |
Planner.vue:305,316,321,325,330 |
5 |
0 |
PlannerFactoryList.vue:244,248 |
2 |
0 |
StatisticsFactorySummary.vue:513,524 |
2 |
0 |
Statistics.vue:138 |
1 |
0 |
PlannerFactoryList mounts twice per Planner mount (docked sidebar plus the teleported mobile drawer), so that alone is 4 leaked handlers per navigation. Statistics and StatisticsFactorySummary sit behind v-if inside Planner, so they also remount on every plan load and tab switch.
OptionsDialog.vue:609-610 and BuildingGroups.vue:377-383 are the counter-examples and are correctly paired — the latter carries a comment explaining exactly why it matters.
Evidence
Mount / unmount / remount spec counting eventBus.all:
after 1 mount of PlannerFactoryList : prepareForLoad handlers 0 -> 1
after unmount : still 1 (expected 0)
after 5 mount/unmount cycles : incrementLoad handlers 1 -> 6
after 20 mount/unmount of StatisticsFactorySummary : {"fs":20,"sec":20}, unchanged after unmount
Effect
No visible misbehaviour today — the dead closures write to orphaned refs. The real costs are:
- Retention. Every dead component scope stays reachable, including
props.factories and a statuses Map computed over the whole plan.
- Repeated work on load.
loadingCompleted / plannerShow on each dead Planner re-runs showPlan(), which calls resyncWorldResources() (a full pass over every factory x product with a gameData.recipes.find() per product) and schedules updateActiveFactory (a getElementById per factory plus per group band). After N navigations, every plan load does that work N+1 times.
- A latent race.
showPlan() consumes and deletes sessionStorage['navigateToFactory'], so the oldest dead instance claims the Parts-page deep link before the live one reads it. It still works only because scrollToElement is pure DOM.
Suggested fix
Name the handlers and eventBus.off them in onUnmounted, following BuildingGroups.vue.
Found during the Beta v0.6 pre-launch QA sweep. Pre-existing.
What happens
There is no
<KeepAlive>anywhere in the app — the layout is a bare<router-view/>. So every trip to Recipes / Parts / Change Log / Graph and back fully remountsPlanner.vueand all its children.Several components register eventBus listeners at setup scope with no matching
offand noonUnmounted. All of them use anonymous arrow functions, so they cannot be removed even by hand afterwards..on.offPlanner.vue:305,316,321,325,330PlannerFactoryList.vue:244,248StatisticsFactorySummary.vue:513,524Statistics.vue:138PlannerFactoryListmounts twice per Planner mount (docked sidebar plus the teleported mobile drawer), so that alone is 4 leaked handlers per navigation.StatisticsandStatisticsFactorySummarysit behindv-ifinside Planner, so they also remount on every plan load and tab switch.OptionsDialog.vue:609-610andBuildingGroups.vue:377-383are the counter-examples and are correctly paired — the latter carries a comment explaining exactly why it matters.Evidence
Mount / unmount / remount spec counting
eventBus.all:Effect
No visible misbehaviour today — the dead closures write to orphaned refs. The real costs are:
props.factoriesand astatusesMap computed over the whole plan.loadingCompleted/plannerShowon each deadPlannerre-runsshowPlan(), which callsresyncWorldResources()(a full pass over every factory x product with agameData.recipes.find()per product) and schedulesupdateActiveFactory(agetElementByIdper factory plus per group band). After N navigations, every plan load does that work N+1 times.showPlan()consumes and deletessessionStorage['navigateToFactory'], so the oldest dead instance claims the Parts-page deep link before the live one reads it. It still works only becausescrollToElementis pure DOM.Suggested fix
Name the handlers and
eventBus.offthem inonUnmounted, followingBuildingGroups.vue.