MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

性能

这篇翻译不完整。请帮忙从英语翻译这篇文章

着重讲几点框架脚本/消息管理器的使用方式和替代方法,以避免相关的性能缺陷。

要牢记几点:

  • 附加组件启动时注册的脚本将在会话恢复时被执行。较长的执行时间会使浏览器启动后的响应速度缓慢。
  • 框架脚本还可以在未恢复的标签页上被执行。所有开销不止发生在已激活的标签页,而是取决于会话中的标签页总数。

下面的例子为了简洁,省略了一些样板代码

“更好”的例子还省略了一些最佳实践。只是为了演示如何解决各子主题中描述的问题。

性能的最佳实践

每个进程中声明无状态函数

不良:

// 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.

更好:

addon.js 如上

// 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.

每个进程中存放重量级的状态

不良:

// 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.

不良:

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

 

不要在框架脚本中注册观察者(及其他到全局服务的回调)

不良:

//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.

更好:

     

content-document-global-created 通知
可以用 DOMWindowCreated 事件取代
其他观察者和服务
应该在 进程脚本 中注册,或者用 JSM 代替

 

根据需要加载框架脚本

不良:

// 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)

更好:

// 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.

 

向下推进信息,避免到父进程的调用

不良:

// 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.

更好:

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.

 

 

附加组件卸载时的清理

 

不良:

包括上述所有例子,*包括上述的“更好”*

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.

更好:

// 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");
}

在框架/进程脚本中:

  • 移除各种监听器
  • 移除观察者通知
  • 移除自定义的类和服务
  • nuke 沙盒
  • 卸载 JSM
  • 必要时还原内容 DOM 的状态,例如移除附加组件加入的不再需要的互动 UI 元素

文档标签和贡献者

 此页面的贡献者: yfdyh000
 最后编辑者: yfdyh000,