Frame script loading and lifetime

Loading frame scripts

To load a frame script use the loadFrameScript() function.

This line of code loads a frame script into the currently selected tab. The script just writes "foo" to the command line:

// chrome script
var mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript('data:,dump("foo\\n")', true);

loadFrameScript() takes two mandatory parameters:

  • A URL that points to the frame script you want to load
  • A boolean flag, allowDelayedLoad

Note: if the message manager is a global frame message manager or a window message manager, loadFrameScript() may load the script multiple times, once in each applicable frame.

chrome: URLs

Extension developers usually use a chrome:// URL to refer to the frame scripts.

To define the mapping between a chrome:// URL and a frame script packaged with an extension, use a "chrome.manifest" file to register a chrome URL:

// chrome.manifest
content my-e10s-extension chrome/content/
// chrome script
mm.loadFrameScript("chrome://my-e10s-extension/content/content.js", true);

allowDelayedLoad

If the message manager is a global frame message manager or a window message manager then:

  • If allowDelayedLoad is true, the frame script will be loaded into any new frame, which has opened after the loadFrameScript() call. For example:

    var mm = window.messageManager;
    mm.loadFrameScript("chrome://my-e10s-extension/content/frame-script.js", true);

    The script will be loaded into all tabs currently open in this window, and all new tabs opened afterwards.

  • If allowDelayedLoad is false, the script will only be loaded into frames which are open when the call was made.

If the message manager is a browser message manager, you should always pass true here. Because a browser message manager only corresponds to a single browser tab, its loadFrameScript() function will only ever load the frame script into that one tab. Passing allowDelayedLoad is a way to ensure that the script is loaded correctly, in case the tab is not ready when making the call.

If you use allowDelayedLoad, you can cancel it by using removeDelayedFrameScript:

var mm = window.messageManager;
mm.removeDelayedFrameScript("chrome://my-e10s-extension/content/frame-script.js");

This means we will stop loading the script into new tabs. Note that this function will not remove any scripts which have been loaded earlier.

Frame script lifetime

Frame scripts are loaded as soon as loadFrameScript() is called. If you've set allowDelayedLoad, the script is loaded into a new tab as soon as it is created.

Frame scripts are associated with a browser tab and not with a page. So once you load them, they will stay loaded until the tab is closed, even if you reload the document or navigate. If you want to limit a script to the lifetime of a page you can create a Sandbox instead, where the current content page is used as prototype.

If you want a frame script to do something when a new document is loaded, you need to listen for an appropriate DOM event, generally DOMWindowCreated, DOMContentLoaded, or load.

Unloading frame scripts

Frame scripts are automatically unloaded when their hosting tab is closed. There is currently no way to unload them when loaded, other than closing the tab they were loaded into.

To listen for an event, when your frame script is unloaded (due to tab close for instance), you must set the third argument of addMessageListener to true. For example, from bootstrap.js:

Services.mm.addMessageListener(
    'my-addon-id',
    {
        receiveMessage: function() {
            console.log('incoming message from frame script:', aMsg.data);
        }
    },
    true // must set this argument to true, otherwise sending message from framescript will not work during and after the unload event on the ContentMessageManager triggers
);

Then in your frame script, listen for the unload event of the message manager (which is the global this), and sending a message. If you did not set the third argument to true in bootstrap.js on Services.mm.addMessageListener, sending this message during, and after, the unloading event will do nothing.

var gContentFrameMessageManager = this;

addEventListener('unload', function(aEvent) {
    if (aEvent.target == gContentFrameMessageManager) {
        sendAsyncMessage('my-addon-id', 'framescript-died'); // if you did not set third argument of `Services.mm.addMessageListener` to `true`, then this will fail to send a message
    }
}, false);

Note about unload during uninstallation/upgrade

When your add-on is uninstalled, or disabled, you should:

  • Cancel it, If you have used allowDelayedLoad, by calling removeDelayedFrameScript; ensuring the frame script is not loaded into any new tabs.
  • Disable any frame scripts already loaded. There is no mechanism to unload frame scripts which are already loaded. You need to send a message to your frame scripts, telling them to disable themselves; for example, by undoing any changes they've made or removing any event listeners.

There is a bug in non-e10s where this order is not true. In e10s framescripts work fine on updating. For non-e10s waiting for Bug 1202125 - framescripts are not backwards loaded in message order in non-e10s.

Note: you might think that there is a race condition here due to the asynchronous nature of the message passing:

  • Your add-on is disabled for an upgrade.
  • Your add-on broadcasts "disable" to your frame scripts.
  • Your add-on is upgraded, and the new code loads new frame scripts.
  • The new frame scripts receive the "disable" message and stop working.

In fact, the message manager guarantees that loadFrameScript and broadcastAsyncMessage are guaranteed to affect frame scripts in the order that they are called, so in this case "disable" will be received and consumed before the new frame scripts are loaded.

At the moment, frame scripts are cached until the browser restarts: this problem is tracked as bug 1051238. This is a problem especially for restartless add-ons; because when a new version of the add-on is installed, the old frame scripts will not be unloaded. The workaround is to randomize the frame script's URL; for example, by appending "?" + Math.random().