Iframe Garbage Collection
When you have an iframe loading an intense amount of content (like an entire video game, in the case of https://playfeed.io), and you need you need to reuse that iframe, you need to force the iframe to free up its memory usage so the browser will garbage collect it.
Here are some ideas that didn't work:
- simply setting
src
to a new URL - get a reference to the iframe, remove it from the DOM:
parent.querySelector('iframe').remove()
- this only removes the element from the DOM but it keeps the iframe in memory still
- gate the
iframe
behind anif
statement of some kind (such as a ternary,renderIf
in element-vir,ngIf
in Angular, etc.)
What does work, however, is the following:
- Load the initial iframe
src
. - When it's time to reuse the iframe, set its
src
to an empty page (the below example usessrc="/blank.html"
). - Wait for the empty page to finish loading.
- Set the new iframe
src
.
Waiting for the empty page to load
Step 3 in the working steps is the hard part. The browser provides several naive ways of determining when a child iFrame is loaded (such as the load
event). However, after lots of experimentation over the years I've found that none of them are reliable for knowing when an iframe has truly loaded. So I built a package to handle that: https://www.npmjs.com/package/interlocking-iframe-messenger
Here's how to use interlocking-iframe-messenger
to solve this problem:
-
Create an iframe messenger using
interlocking-iframe-messenger
:// iframe-messenger.ts
import {createIframeMessenger, IframeMessageDirectionEnum} from 'interlocking-iframe-messenger';
export enum LoadedIframeMessageType {
IframeLoaded = 'iframe-loaded',
}
export type LoadedIframeMessageData = {
[LoadedIframeMessageType.IframeLoaded]: {
[IframeMessageDirectionEnum.FromChild]: string;
[IframeMessageDirectionEnum.FromParent]: undefined;
};
};
export const iframeMessenger = createIframeMessenger<LoadedIframeMessageData>(); -
Create a script for determining when a child iframe has loaded (this will be called by the child's HTML):
// child-iframe.script.ts
import {LoadedIframeMessageType, iframeMessenger} from './iframe-message.js';
iframeMessenger.listenForParentMessages({
parentOrigin: window.location.origin,
listener({type}) {
if (type === LoadedIframeMessageType.IframeLoaded) {
return window.location.pathname;
}
throw new Error(`Unexpected iframe message type: '${String(type)}'`);
},
}); -
Create a
blank.html
file (this will be loaded in the iframe to allow garbage collection):<!-- blank.html -->
<!doctype html>
<html>
<head>
<script type="module" src="./child-iframe.script.ts"></script>
</head>
<body></body>
</html> -
Update your code that sets the new iframe URL to first set it to
blank.html
:// update-iframe.ts
import {LoadedIframeMessageType, iframeMessenger} from './iframe-message.js';
export async function loadNewIframeUrl(newUrl: string) {
iframeRef.src = '/blank.html';
// Wait for the blank page to finish loading so the browser can garbage collect the last URL.
await loadedIframeMessenger.sendMessageToChild({
childOrigin: window.location.origin,
data: undefined,
iframeElement: iframeRef,
type: LoadedIframeMessageType.IframeLoaded,
verifyChildData(data: any) {
return data === '/blank.html';
},
interval: {
// This page should load very quickly so we want to check it as often as possible to
// a minimal load time.
milliseconds: 5,
},
});
// Now we can finally set the new URL!
iframeRef.src = newUrl;
}