Our team has encountered similar issues and identified a common root cause: an abundance of components relying on the same reactive object. There are three main scenarios that can impact any project:
- Having numerous
router-link
components
- Utilizing many components (of any type) with Vue I18n installed
- Using multiple components that directly access the Vuex store in their render or computed properties.
Our recommended approach involves refraining from accessing shared reactive objects within the render and computed properties functions. Instead, consider passing them as props
(reactive) or accessing them in the created
or updated
hooks (non-reactive) to store them in the component's $data
. Further details for each of the three cases are provided below.
An Overview of Vue 2 Reactivity Mechanics
(Please skip this section if not needed)
The reactivity mechanism in Vue revolves around two interconnected objects: Watcher and Dep. Watchers maintain a list of dependencies (Deps) in the deps
attribute, while Deps include a list of dependents (Watchers) in the subs
attribute.
For every reactive element, Vue creates a corresponding Dep entity to track reads and writes associated with it.
A Watcher is established for each component (specifically, for the render
function) and Computed Property. These Watchers essentially oversee the execution of a function. During this oversight, if a reactive object is accessed, the linked Dep is alerted, establishing a relationship between them: The Watcher.deps
includes the Dep
, and the Dep.subs
contains the Watcher
.
Subsequently, upon modification of the reactive element, the relevant Dep
notifies all its dependents (Dep.subs
) instructing them to update (Watcher.update
).
Upon destruction of a component, its associated Watchers are also dismantled. This process entails scanning through each Watcher.deps
to eliminate the Watcher itself from the Dep.subs
(refer to Watcher.teardown).
The Issue at Hand
All components dependent on the same reactive entity introduce a Watcher onto the identical Dep.subs
. For instance, in a scenario where the same Dep.subs
comprises 10,000 watchers:
- Rendering 1,000 items (e.g., grid layout, infinite scroll, etc.)
- Each item involving 10 components: itself, 2 router-links, 3 buttons, and 4 other elements (nested and non-nested, spanning own codebase or third-party tools).
- All components reliant on the same reactive object.
When terminating the page, the 10,000 watchers gradually detach themselves from the Dep.subs
array individually. The cost incurred during removal is calculated as 10k * O(10k - i)
, with 'i' denoting the count of already-removed watchers.
In general, the expense of removing n
items follows an O((n^2)/2)
scheme.
Possible Solutions
If your project involves rendering myriad components, strive to circumvent accessing shared reactive dependencies within the render
or computed properties.
Instead, contemplate conveying them via props
or retrieving them in the created
or updated
hooks and storing them in the component's $data
section. It's important to note that these hooks aren't monitored, meaning the component won't refresh in response to alterations in the data source – a suitable option for static data scenarios (wherein data remains unchanged post-component mount).
In situations featuring a lengthy list of rendered items, leveraging vue-virtual-scroller can be beneficial. In such instances, you can still interact with shared reactive dependencies since vue-virtual-scroller utilizes a small collection of components (ignoring off-screen renditions).
Keep in mind that dealing with thousands of components may not be as challenging as anticipated, primarily due to our inclination towards crafting petite components and assembling them (which is indeed commendable practice).
Scenario: Vuex
If your operation within the render or computed property mimics the example below, your component becomes entwined with the entire chain of reactive entities: state
, account
, profile
.
function myComputedProperty() {
this.$store.state.account.profile.name;
}
In this context, assuming the account remains unaltered post-mounting, extract the value in the created
or beforeMount
hook and retain the name
in the Vue $data
. Since this falls outside the purview of the render function or computed property, no Watcher continuously supervises the store access.
function beforeMount() {
this.$data.userName = this.$store.state.account.profile.name;
}
Scenario: router-link
Refer to the issue #3500
Scenario: Vue I18n
While sharing a fundamentally similar core predicament, the Vue I18n case presents a slightly distinct explanation. Explore issue #926 for further insights.