Skip to content
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

Merged
merged 3 commits into from
Jun 20, 2018

Conversation

the83
Copy link
Contributor

@the83 the83 commented Jun 1, 2018

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-example

This moves the setup into the componentDidMount hook so that we don't even observe visibility at all server-side.

Copy link
Member

@tazsingh tazsingh left a 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'
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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"
Copy link
Member

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

Copy link
Contributor Author

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 = []
Copy link
Member

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 😅

Copy link
Contributor Author

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.

@the83 the83 force-pushed the th/fix-memory-leak branch from aedb8b6 to d5e5089 Compare June 18, 2018 16:38
@the83 the83 force-pushed the th/fix-memory-leak branch from d5e5089 to a84a0c9 Compare June 18, 2018 16:40
Copy link
Member

@tazsingh tazsingh left a 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', () => {
Copy link
Member

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()
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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
Copy link
Member

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
Copy link
Member

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 👍

@tazsingh tazsingh merged commit d951b11 into stratiformltd:master Jun 20, 2018
@tazsingh
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants