An illustration of a memory leak that occurs with Vue Router's beforeRouteEnter
guard implementation.
brew install siege
yarn build && yarn start
siege http://localhost:8080/
-
Open the test site in Chrome (http://localhost:8080)
-
Open Developer Tools
-
DevTools - Node.js (green hexagonal button)
-
Go to the Profiler tab and click Start.
-
After about 5 seconds, Stop, and review the recording.
The memory leak happens when the router-view
is programmed to appear conditionally, and the component matching the view has a beforeRouteEnter
guard and a callback is passed to it's next(...)
method (e.g. next(vm => {})
).
This will cause vue-router
to poll every 16ms until the router-view
materializes.
In a typical SSR application an instance of the app is created per request, which means the router-view
will never appear, causing infinitely recursing poll
methods.
This can be verified by taking a CPU profile.
As the application is sieged, Timeout's will grow indefinitely, and profiling the CPU reveals that poll
s are executing constantly in the event loop.
Note that this is probably not a problem for non-SSR applications.
git clone [email protected]:ronald-d-rogers/vue-router.git
cd vue-router
yarn && yarn build && yarn link
cd ..
git clone [email protected]:ronald-d-rogers/vue.git
cd vue
git checkout ssr-request-cleanup
yarn && yarn build:ssr
cd packages/vue-server-renderer
yarn link
# in vue-router-ssr-memory-leak
yarn link "vue-router"
yarn link "vue-server-renderer"
yarn build:fix && yarn start
siege http://localhost:8080/
If we profile the CPU now, the memory leak is gone.
// entry-server-fixed.js
import { createApp } from './app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router } = createApp(context)
const { url } = context
router.push(url)
router.onReady(() => {
//resolve(app)
resolve({ app, onComplete() { app.$destroy() } })
}, reject)
})
}
The change to vue-server-render
allows us to call app.$destroy
when the request is complete:
resolve({ app, onComplete() { app.$destroy() } })
This sets app._isBeingDestroyed
to true
.
vue-router
's poll
method is leaking into memory every request:
function poll (
cb: any, // somehow flow cannot infer this is a function
instances: Object,
key: string,
isValid: () => boolean
) {
if (
instances[key] &&
!instances[key]._isBeingDestroyed // do not reuse being destroyed instance
) {
cb(instances[key])
} else if (isValid()) {
setTimeout(() => {
poll(cb, instances, key, isValid)
}, 16)
}
}
It uses isValid
to stop itself from recursing:
https://github.com/vuejs/vue-router/blob/fc42d9cf8e1d381b885c9af37003198dce6f1161/src/history/base.js#L348
So we update the isValid
method to check if the app is being destroyed.
- const isValid = () => this.current === route
+ const isValid = () => {
+ if (app && app._isBeingDestroyed) {
+ return false
+ }
+ return this.current === route
+ }
Setting app._isBeingDestroyed
to true
by calling app.$destroy
, and checking if the app is being destroyed in isValid
short-circuits the leaking poll
s.