Performance

This highlights some performance pitfalls related to frame scripts/message manager usage and alternative approaches to avoid them.

Key points to keep in mind

  • Scripts registered during addon startup get executed during session restore. The more expensive they are to execute the longer it will take for the browser to become responsive after startup.
  • Frame scripts also get executed on non-restored tabs. All their overhead is thus not just incurred by active tabs but by the total number of tabs in a session.

The following examples omit some boilerplate code for the sake of brevity

The "better" examples also omit some best practices and only demonstrate how to fix the problem described in their respective subtopics.

Performance best practices

Declaring stateless functions once per process

BAD:

// addon.js
Services.mm.loadFrameScript("framescript.js", true)
// framescript.js

const precomputedConstants = // ...

function helper(window, action) {
  // ...  do some work on the window
}

function doSomething(message) {
  result = helper(content, message.data)
  sendAsyncMessage("my-addon:response-from-child", {something: result})
}

addMessageListener("my-addon:request-from-parent", doSomething)

Why is this bad? Because declared functions are also objects. And since frame scripts get evaluated for each tab this means new function objects get instantiated, new constants get computed, block scopes must be set up etc.

While it may seem fairly innocencous in this toy example, real scripts often have a lot more functions and initialize some fairly heavyweight objects.

BETTER:

addon.js as above

// framescript.js
Components.utils.import("resource://my-addon/processModule.jsm", {}).addFrame(this)
// processModule.jsm

const EXPORTED_SYMBOLS = ['addFrame'];

const precomputedConstants = // ...

function helper(window, action) {
  // ... do some work on the window
}

function doSomething(message) {
  frameGlobal = message.target
  result = helper(frameGlobal.content, message.data)
  frameGlobal.sendAsyncMessage("my-addon:response-from-child", {something: result})
}

function addFrame(frameGlobal) {
  frameGlobal.addMessageListener("my-addon:request-from-parent", doSomething)
}

Javascript modules are per-process singletons and thus all their objects are only initialized once, which makes them suitable for stateless callbacks.

But care must be taken to not leak references to the frame script global when it is passed into a JSM. Alternatively the frame's unload event or weak maps can be used to ensure that frames can be cleaned up when their respective tab is closed.

Store heavyweight state once per process

BAD:

// addon.js
var main = new MyAddonService();

main.onChange(stateChange);

function stateChange() {
  Services.mm.broadcastAsyncMessage("my-addon:update-configuration", {newConfig: main.serialize()})
}
// framescript.js
var mainCopy;

function onUpdate(message) {
   mainCopy = MyAddonService.deserialize(message.data.newConfig);
}

addMessageListener("my-addon:update-configuration", onUpdate)


// mainCopy used by other functions

The main issue here is that a separate object is kept for each tab. Not only does that increase memory footprint but the deserialization also has to be executed seperately for each tab, thus requiring more CPU time.

BETTER:

// addon.js
var main = new MyAddonService();

main.onChange(stateChange);

function stateChange() {
  Services.ppmm.broadcastAsyncMessage("my-addon:update-configuration", {newConfig: main.serialize()})
}
// processModule.jsm
const EXPORTED_SYMBOLS = ['getMainCopy'];

var mainCopy;

Services.cpmm.addMessageListener("my-addon:update-configuration", function(message) {
  mainCopy = message.data.newConfig;
})

funtion getMainCopy() {
  return mainCopy;
}

// framescript.js
Components.utils.import("resource://my-addon/processModule.jsm")

// getMainCopy() used by other functions

Don't register observers (and other callbacks to global services) in a frame script

BAD:

//framescript.js
Services.obs.addObserver("document-element-inserted", {
  observe: function(doc, topic, data) {
     if(doc.ownerGlobal.top != content)
        return; // bail out if  this is for another tab
     decorateDocument(doc);
  }
})

Observer notifications get fired for events that happen anywhere in the browser, they are not scoped to the current tab. If each framescript registers a seperate listener then the observed action will trigger the callbacks in all tabs.

Additionally the example above does not clean unregister itself, thus leaking objects each time a tab is closed. Frame message manager message and event listeners are limited in their lifetime to that of the frame itself, this does not apply to observer registrations.

BETTER:

content-document-global-created notifications
can be substituted with DOMWindowCreated events
other observers and services
should be registered in a process script or JSM instead

Load frame scripts on demand

BAD:

// addon.js
Services.mm.loadFrameScript("framescript.js", /*delayed:*/ true)

// stuff communicating with the framescript
// framescript.js
function onlyOnceInABlueMoon() {
   // we only need this during a total solar eclipse while goat blood rains from the sky
   sendAsyncMessage('my-addon:paragraph-count', {num: content.document.querySelectorAll('p').length})
}
addMessageListener("my-addon:request-from-parent", onlyOnceInABlueMoon)

BETTER:

// addon.js
function onToolbarButton(event) {
  let tabMM = gBrowser.mCurrentBrowser.frameLoader.messageManager;
  let button = event.target;
  let callback = (message) => {
    tabMM.removeMessageListener("my-addon:paragraph-count", callback)
    decorateButton(button, message.data.num)
  }
  tabMM.addMessageListener("my-addon:paragraph-count", callback);
  tabMM.loadFrameScript("data:,sendAsyncMessage('my-addon:paragraph-count', {num: content.document.querySelectorAll('p').length})", false)
}

function decorateButton(button, count) {
  // do stuff with result
}

This executes the script only when it is needed and only in one tab and allows it to be garbage-collected immediately after execution. As long as it the action does not happen frequently the memory and startup savings should outstrip the added cost of script evaluation.

Delaying the script registration until the session is restored my provide some middle ground for some addons. It does not provide the same memory footprint reductions but it improves application startup.

Beam down information in advance to avoid synchronous calls to the parent

BAD:

// processscript.js

function ContentPolicy() {
  // ...
}

Object.assign(ContentyPolicy.prototype, {
    classDescription: ..., classID: ..., contractID: ...,
    QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy]),

    shouldLoad: function(type, location, origin, context) {

       let resultList = Services.cpmm.sendSyncMessage("my-addon:check-load", {destination: location, source: origin}) // <=== SYNC MESSAGE!

       if(resultList.every((r) => r == true))
           return Ci.nsIContentPolicy.ACCEPT;

       return Ci.nsIContentPolicy.REJECT_REQUEST;
    }
});

// more boilerplate code here

This example is a (somewhat condensed) content policy which gets triggered for every network request in a child process to either allow or deny the request. Since the code calls to the parent process to check whether a specific request can be allowed it would slow down all page loads, as web pages generally issue dozens of requests.

BETTER:

Instead of only keeping the state in the parent an addon can employ a master-slave architecture where the parent has the authoritative state and replicates it to the child processes in advance so they can act based on their local copy.

See the previous chapter on how to efficiently replicate addon state to each process.

Clean up on addon unload

BAD:

All the previous examples, *even the "better" ones*

If your addon is restartless or uses the SDK then updates or the user turning it off and on will load to unload/reload events. Not handling those properly can lead to duplicate or conflicting code execution, especially when messages are sent. It can also lead to conflicts between the old and new code. Under some circumstances it may even cause exceptions when attempting to register something twice under the same ID.

BETTER:

// addon.js
function onUnload() {
  Services.mm.removeDelayedFrameScript("resources://my-addon/framescript.js");
  Services.ppmm.removeDelayedProcessScript("resources://my-addon/processcript.js");
  Services.mm.broadcastAsyncMessage("my-addon:unload");
  Services.ppmm.broadcastAsyncMessage("my-addon:unload");
}

In the frame/process scripts:

  • remove all kinds of listeners
  • remove observer notifications
  • remove custom categories and services
  • nuke sandboxes
  • unload JSMs
  • restore content DOM states where necessary, e.g. remove interactive UI elements that would be inert with the addon