-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix memory leak when using server-side #11
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch @the83! Thanks for the PR and apologies for the delayed response - I was on vacation for the past few weeks.
I've requested a few changes here. Let me know if you have any questions. But I'm happy to merge this and cut a release as soon as it's ready 👍
@@ -1,8 +1,8 @@ | |||
import React, { Component } from 'react' | |||
import * as React from 'react' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to revert this change? I'd prefer to be explicit about imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -7,6 +7,7 @@ const { IntersectionObserver, makeElementsVisible } = require('../../__mocks__/I | |||
global.IntersectionObserver = IntersectionObserver | |||
|
|||
const loadableVisiblity = require('../../loadable-components') | |||
const trackedElements = require('../../tracked_elements').default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this is primarily being used for the tests. Would you be able to modify the IntersectionObserver
mock to include this behaviour instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -95,7 +77,7 @@ function createLoadableVisibilityComponent (args, { | |||
return <div | |||
style={{display: 'inline-block', minHeight: '1px', minWidth: '1px'}} | |||
className={this.props.className} | |||
ref={this.attachRef} | |||
ref="loading" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you revert this change? String refs have been deprecated - https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -22,63 +22,45 @@ function createLoadableVisibilityComponent (args, { | |||
LoadingComponent, | |||
}) { | |||
let preloaded = false | |||
const visibilityHandlers = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove this? It's to handle the case where preload
is called after the component is part of the tree. In that scenario, we want all of the components to hit the underlying LoadableComponent
to handle the interaction accordingly.
I believe by removing this and if preload
is called after the component is mounted, then there will be a bug as it will fall back to the IntersectionObserver
instead of already being loaded.
Of course, tests for this behaviour would be helpful 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I think I was confused by the fact that the this.state.visible
is set based on the preloaded
var in the constructor, so I assumed that this was unnecessary.
I added the visibilityHandlers
back in and wrote a test that demonstrates that the underlying component will be displayed when preload
is called after mount.
aedb8b6
to
d5e5089
Compare
d5e5089
to
a84a0c9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there 🙂
test('preload calls loadable load', () => { | ||
loadableVisiblity(opts).load() | ||
|
||
expect(loadable().load).toHaveBeenCalled() | ||
}) | ||
|
||
test('preload will cause the loadable component do be displayed', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding this test! Can you add it for the react-loadable
section as well?
Also minor typo: should be to
instead of do
@@ -2,7 +2,7 @@ import React, { Component } from 'react' | |||
import { IntersectionObserver } from './capacities' | |||
|
|||
let intersectionObserver | |||
let trackedElements = new Map() | |||
const trackedElements = new Map() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this now be removed as it's handled inside the mocked IntersectionObserver?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow—what does the mocked IntersectionObserver have to do with the trackedElements
in createLoadableVisibilityComponent
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I confused this with something else. Took another look and all is good 👍
@@ -1,4 +1,7 @@ | |||
const intersectionObservers = [] | |||
const trackedElements = [] | |||
|
|||
module.exports.trackedElements = trackedElements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the naming here is a bit confusing. Maybe allTrackedElements
or globallyTrackedElements
or similar to signify that it's a global that affects all instances of IntersectionObserver
?
@@ -17,7 +20,7 @@ module.exports.IntersectionObserver = class IntersectionObserver { | |||
constructor(callback) { | |||
this.callback = callback | |||
|
|||
this.trackedElements = [] | |||
this.trackedElements = trackedElements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of initializing the IntersectionObserver
with what was potentially previously tracked as it's not quite per spec. But I do like what you've done inside the LoadableVisibilityComponent
itself where you double observed the tracked element via the IntersectionObserver
and in trackedElements
.
Perhaps a different approach would be something like:
class IntersectionObserver {
constructor(callback) {
// ...
this.trackedElements = []
// ...
}
observe(element) {
this.trackedElements.push(element)
globallyTrackedElements.push(element)
}
unobserve(element) {
// remove the element from this.trackedElements and globallyTrackedElements
}
}
Then you can assert against globallyTrackedElements
to ensure that there aren't any leaks 👍
Released under |
I recently ran into a memory leak when using this server-side. This occurs because the visibility handlers are set up in the constructor instead of a lifecycle hook, and because
componentWillUnmount
does not execute server-side, we never stop observing the element. I set up a simple example of this happening here: https://github.com/the83/memory-leak-exampleThis moves the setup into the
componentDidMount
hook so that we don't even observe visibility at all server-side.