Aaronius / penpal

A promise-based library for securely communicating with iframes via postMessage.
MIT License
389 stars 56 forks source link

remove parent window message handler if child iframe no longer in DOM #28

Closed srhyne closed 5 years ago

srhyne commented 5 years ago

This would what I experienced in #27 which was old messageHandler calls trying to connect to an iframe that no longer exists.

Aaronius commented 5 years ago

Hey @srhyne, thanks for opening the issue and this pull request. I'm glad you brought it up. It's indicative of a memory leak when the iframe is removed from the DOM but the consumer never calls connection.destroy(). In this case, the event listener that watches for messages on the parent is never removed and the handler (handleMessage) holds a reference to the iframe within its closure (as you've noted), so the iframe sticks around in memory too. I've verified that this memory leak does exist through some local testing and viewing memory snapshots.

Your PR removes the event listener on the next message that comes through, but I believe it has a couple weaknesses still that I think I'd like to cover. With the code as it exists in this PR:

To resolve both of these issues, I'm considering adding a mutation observer to watch for the removal of the iframe and then where we add the iframe to the document:

(appendTo || document.body).appendChild(iframe);
detectElementRemoval(iframe, destroy);

I'm bummed about it because it will increase the file size by a few hundred bytes and having to run a mutation observer kind of sucks. I'm still thinking about this one, but wanted to let you know where I am with it.

As an interim solution to your specific problem, I could just change:

const child = iframe.contentWindow || iframe.contentDocument.parentWindow;

to

const child = iframe.contentWindow;

as I believe the last part is actually unnecessary.

srhyne commented 5 years ago

@Aaronius thank you for your very thoughtful review! Your points make total sense.

I actually started with a mutation observer but quickly realized that the mutation observer only listens for children of the target being destroyed. The observer callback won't fire if the target itself is removed. So the observer has to bind to body (each time) as the target not the appendTo element otherwise the observer might not fire when the appendTo (or higher parent) is removed.

You could register iframe references in memory and then have a single static observer watching for iframe's with X ID being removed and destroy that way, maybe?

Aaronius commented 5 years ago

@srhyne Thanks again for the PR and talking this through. I decided to not to go with mutation observers but took a different route instead. In short, I added some logic that will call destroy when a method is called from the parent and the iframe was previously removed. I also added a check that runs on an interval to see if the iframe is in the document. If it isn't, Penpal automatically calls destroy so that memory for that connection can get cleaned up. I've released these changes in v3.1.2.