Index: background.js
===================================================================
--- a/background.js
+++ b/background.js
@@ -86,15 +86,9 @@
   // due to loading filters or saving filter changes
   if (action == "load" || action == "save")
   {
-    ext.windows.getAll(function(windows)
+    ext.pages.query({}, function(pages)
     {
-      for (var i = 0; i < windows.length; i++)
-      {
-        windows[i].getAllTabs(function(tabs)
-        {
-          tabs.forEach(refreshIconAndContextMenu);
-        });
-      }
+      pages.forEach(refreshIconAndContextMenu);
     });
   }
 });
@@ -119,24 +113,25 @@
 var activeNotification = null;
 
 // Adds or removes browser action icon according to options.
-function refreshIconAndContextMenu(tab)
+function refreshIconAndContextMenu(page)
 {
-  var whitelisted = isWhitelisted(tab.url);
+  var whitelisted = isWhitelisted(page.url);
 
   var iconFilename;
   if (whitelisted && require("info").platform != "safari")
-    // There is no grayscale version of the icon for whitelisted tabs
+    // There is no grayscale version of the icon for whitelisted pages
     // when using Safari, because icons are grayscale already and icons
-    // aren't per tab in Safari.
+    // aren't per page in Safari.
     iconFilename = "icons/abp-$size-whitelisted.png";
   else
     iconFilename = "icons/abp-$size.png";
 
-  tab.browserAction.setIcon(iconFilename);
-  iconAnimation.registerTab(tab, iconFilename);
+  page.browserAction.setIcon(iconFilename);
+  iconAnimation.registerPage(page, iconFilename);
 
-  // Set context menu status according to whether current tab has whitelisted domain
-  if (whitelisted || !/^https?:/.test(tab.url))
+  // show or hide the context menu entry dependent on whether
+  // adblocking is active on that page
+  if (whitelisted || !/^https?:/.test(page.url))
     ext.contextMenus.hideMenuItems();
   else
     ext.contextMenus.showMenuItems();
@@ -234,10 +229,7 @@
 
   function notifyUser()
   {
-    ext.windows.getLastFocused(function(win)
-    {
-      win.openTab(ext.getURL("firstRun.html"));
-    });
+    ext.pages.open(ext.getURL("firstRun.html"));
   }
 
   if (addSubscription)
@@ -272,10 +264,10 @@
   if (Prefs.shouldShowBlockElementMenu)
   {
     // Register context menu item
-    ext.contextMenus.addMenuItem(ext.i18n.getMessage("block_element"), ["image", "video", "audio"], function(srcUrl, tab)
+    ext.contextMenus.addMenuItem(ext.i18n.getMessage("block_element"), ["image", "video", "audio"], function(srcUrl, page)
     {
       if (srcUrl)
-        tab.sendMessage({type: "clickhide-new-filter", filter: srcUrl});
+        page.sendMessage({type: "clickhide-new-filter", filter: srcUrl});
     });
   }
   else
@@ -290,34 +282,29 @@
 setContextMenu();
 
 /**
-  * Opens options tab or focuses an existing one, within the last focused window.
+  * Opens options page or focuses an existing one, within the last focused window.
   * @param {Function} callback  function to be called with the
-                                Tab object of the options tab
+                                Page object of the options page
   */
 function openOptions(callback)
 {
-  ext.windows.getLastFocused(function(win)
+  ext.pages.query({lastFocusedWindow: true}, function(pages)
   {
-    win.getAllTabs(function(tabs)
+    var optionsUrl = ext.getURL("options.html");
+
+    for (var i = 0; i < pages.length; i++)
     {
-      var optionsUrl = ext.getURL("options.html");
+      var page = pages[i];
+      if (page.url == optionsUrl)
+      {
+        page.activate();
+        if (callback)
+          callback(page);
+        return;
+      }
+    }
 
-      for (var i = 0; i < tabs.length; i++)
-      {
-        if (tabs[i].url == optionsUrl)
-        {
-          tabs[i].activate();
-          if (callback)
-            callback(tabs[i]);
-          return;
-        }
-      }
-
-      win.openTab(optionsUrl, callback && function(tab)
-      {
-        tab.onCompleted.addListener(callback);
-      });
-    });
+    ext.pages.open(optionsUrl, callback);
   });
 }
 
@@ -498,8 +485,8 @@
     case "get-selectors":
       var selectors = null;
 
-      if (!isFrameWhitelisted(sender.tab, sender.frame, "DOCUMENT") &&
-          !isFrameWhitelisted(sender.tab, sender.frame, "ELEMHIDE"))
+      if (!isFrameWhitelisted(sender.page, sender.frame, "DOCUMENT") &&
+          !isFrameWhitelisted(sender.page, sender.frame, "ELEMHIDE"))
       {
         var noStyleRules = false;
         var host = extractHostFromURL(sender.frame.url);
@@ -525,7 +512,7 @@
       sendResponse(selectors);
       break;
     case "should-collapse":
-      if (isFrameWhitelisted(sender.tab, sender.frame, "DOCUMENT"))
+      if (isFrameWhitelisted(sender.page, sender.frame, "DOCUMENT"))
       {
         sendResponse(false);
         break;
@@ -548,9 +535,9 @@
     case "get-domain-enabled-state":
       // Returns whether this domain is in the exclusion list.
       // The browser action popup asks us this.
-      if(sender.tab)
+      if(sender.page)
       {
-        sendResponse({enabled: !isWhitelisted(sender.tab.url)});
+        sendResponse({enabled: !isWhitelisted(sender.page.url)});
         return;
       }
       break;
@@ -562,18 +549,18 @@
       }
       break;
     case "add-subscription":
-      openOptions(function(tab)
+      openOptions(function(page)
       {
-        tab.sendMessage(msg);
+        page.sendMessage(msg);
       });
       break;
     case "add-key-exception":
-      processKeyException(msg.token, sender.tab, sender.frame);
+      processKeyException(msg.token, sender.page, sender.frame);
       break;
     case "forward":
-      if (sender.tab)
+      if (sender.page)
       {
-        sender.tab.sendMessage(msg.payload, sendResponse);
+        sender.page.sendMessage(msg.payload, sendResponse);
         // Return true to indicate that we want to call
         // sendResponse asynchronously
         return true;
@@ -585,11 +572,11 @@
   }
 });
 
-// Update icon if a tab changes location
-ext.tabs.onLoading.addListener(function(tab)
+// update icon when page changes location
+ext.pages.onLoading.addListener(function(page)
 {
-  tab.sendMessage({type: "clickhide-deactivate"});
-  refreshIconAndContextMenu(tab);
+  page.sendMessage({type: "clickhide-deactivate"});
+  refreshIconAndContextMenu(page);
 });
 
 setTimeout(function()
Index: chrome/ext/background.js
===================================================================
--- a/chrome/ext/background.js
+++ b/chrome/ext/background.js
@@ -17,187 +17,111 @@
 
 (function()
 {
-  /* Events */
+  /* Pages */
 
-  var TabEventTarget = function()
+  var sendMessage = chrome.tabs.sendMessage || chrome.tabs.sendRequest;
+
+  var Page = ext.Page = function(tab)
   {
-    WrappedEventTarget.apply(this, arguments);
+    this._id = tab.id;
+    this._url = tab.url;
 
-    this._tabs = {};
+    this.browserAction = new BrowserAction(tab.id);
+  };
+  Page.prototype = {
+    get url()
+    {
+      // usually our Page objects are created from Chrome's Tab objects, which
+      // provide the url. So we can return the url given in the constructor.
+      if (this._url != null)
+        return this._url;
 
-    this._sharedListener = this._sharedListener.bind(this);
-    this._removeTab = this._removeTab.bind(this);
-  };
-  TabEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _bindToTab: function(tab)
+      // but sometimes we only have the tab id when we create a Page object.
+      // In that case we get the url from top frame of the tab, recorded by
+      // the onBeforeRequest handler.
+      var frames = framesOfTabs[this._id];
+      if (frames)
+      {
+        var frame = frames[0];
+        if (frame)
+          return frame.url;
+      }
+    },
+    activate: function()
     {
-      return {
-        addListener: function(listener)
-        {
-          var listeners = this._tabs[tab._id];
-
-          if (!listeners)
-          {
-            this._tabs[tab._id] = listeners = [];
-
-            if (Object.keys(this._tabs).length == 1)
-              this.addListener(this._sharedListener);
-
-            tab.onRemoved.addListener(this._removeTab);
-          }
-
-          listeners.push(listener);
-        }.bind(this),
-        removeListener: function(listener)
-        {
-          var listeners = this._tabs[tab._id];
-          if (!listeners)
-            return;
-
-          var idx = listeners.indexOf(listener);
-          if (idx == -1)
-            return;
-
-          listeners.splice(idx, 1);
-
-          if (listeners.length == 0)
-            tab.onRemoved.removeListener(this._removeTab);
-          else
-          {
-            if (listeners.length > 1)
-              return;
-            if (listeners[0] != this._removeTab)
-              return;
-          }
-
-          this._removeTab(tab);
-        }.bind(this)
-      };
+      chrome.tabs.update(this._id, {selected: true});
     },
-    _sharedListener: function(tab)
+    sendMessage: function(message, responseCallback)
     {
-      var listeners = this._tabs[tab._id];
-
-      if (!listeners)
-        return;
-
-      // copy listeners before calling them, because they might
-      // add or remove other listeners, which must not be taken
-      // into account before the next occurrence of the event
-      listeners = listeners.slice(0);
-
-      for (var i = 0; i < listeners.length; i++)
-        listeners[i](tab);
-    },
-    _removeTab: function(tab)
-    {
-      delete this._tabs[tab._id];
-
-      if (Object.keys(this._tabs).length == 0)
-        this.removeListener(this._sharedListener);
+      sendMessage(this._id, message, responseCallback);
     }
   };
 
-  var BeforeNavigateTabEventTarget = function()
-  {
-    TabEventTarget.call(this, chrome.webNavigation.onBeforeNavigate);
-  };
-  BeforeNavigateTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
+  ext.pages = {
+    open: function(url, callback)
     {
-      return function(details)
+      if (callback)
       {
-        if (details.frameId == 0)
-          listener(new Tab({id: details.tabId, url: details.url}));
-      };
-    }
+        chrome.tabs.create({url: url}, function(openedTab)
+        {
+          var onUpdated = function(tabId, changeInfo, tab)
+          {
+            if (tabId == openedTab.id && changeInfo.status == "complete")
+            {
+              chrome.tabs.onUpdated.removeListener(onUpdated);
+              callback(new Page(tab));
+            }
+          };
+          chrome.tabs.onUpdated.addListener(onUpdated);
+        });
+      }
+      else
+        chrome.tabs.create({url: url});
+    },
+    query: function(info, callback)
+    {
+      var rawInfo = {};
+      for (var property in info)
+      {
+        switch (property)
+        {
+          case "active":
+          case "lastFocusedWindow":
+            rawInfo[property] = info[property];
+        }
+      }
+
+      chrome.tabs.query(rawInfo, function(tabs)
+      {
+        callback(tabs.map(function(tab)
+        {
+          return new Page(tab);
+        }));
+      });
+    },
+    onLoading: new ext._EventTarget()
   };
 
-  var LoadingTabEventTarget = function()
+  chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab)
   {
-    TabEventTarget.call(this, chrome.tabs.onUpdated);
-  };
-  LoadingTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id, info, tab)
-      {
-        if (info.status == "loading")
-          listener(new Tab(tab));
-      };
-    }
-  };
+    if (changeInfo.status == "loading")
+      ext.pages.onLoading._dispatch(new Page(tab));
+  });
 
-  var CompletedTabEventTarget = function()
+  chrome.webNavigation.onBeforeNavigate.addListener(function(details)
   {
-    TabEventTarget.call(this, chrome.tabs.onUpdated);
-  };
-  CompletedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id, info, tab)
-      {
-        if (info.status == "complete")
-          listener(new Tab(tab));
-      };
-    }
-  };
+    if (details.frameId == 0)
+      ext._removeFromAllPageMaps(details.tabId);
+  });
 
-  var ActivatedTabEventTarget = function()
+  chrome.tabs.onRemoved.addListener(function(tabId)
   {
-    TabEventTarget.call(this, chrome.tabs.onActivated);
-  };
-  ActivatedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(info)
-      {
-        chrome.tabs.get(info.tabId, function(tab)
-        {
-          listener(new Tab(tab));
-        });
-      };
-    }
-  }
+    ext._removeFromAllPageMaps(tabId);
+    delete framesOfTabs[tabId];
+  });
 
-  var RemovedTabEventTarget = function()
-  {
-    TabEventTarget.call(this, chrome.tabs.onRemoved);
-  };
-  RemovedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id) { listener(new Tab({id: id})); };
-    }
-  };
 
-  var BackgroundMessageEventTarget = function()
-  {
-    MessageEventTarget.call(this);
-  }
-  BackgroundMessageEventTarget.prototype = {
-    __proto__: MessageEventTarget.prototype,
-    _wrapSender: function(sender)
-    {
-      var tab = new Tab(sender.tab);
-      
-      //url parameter is missing in sender object (Chrome v28 and below)
-      if (!("url" in sender))
-        sender.url = tab.url;
-      return {tab: tab, frame: new Frame({url: sender.url, tab: tab})};
-    }
-  };
-
-
-  /* Tabs */
-
-  var sendMessage = chrome.tabs.sendMessage || chrome.tabs.sendRequest;
+  /* Browser actions */
 
   var BrowserAction = function(tabId)
   {
@@ -244,148 +168,15 @@
     }
   };
 
-  Tab = function(tab)
-  {
-    this._id = tab.id;
-    this._url = tab.url;
-
-    this.browserAction = new BrowserAction(tab.id);
-
-    this.onLoading = ext.tabs.onLoading._bindToTab(this);
-    this.onCompleted = ext.tabs.onCompleted._bindToTab(this);
-    this.onActivated = ext.tabs.onActivated._bindToTab(this);
-    this.onRemoved = ext.tabs.onRemoved._bindToTab(this);
-
-    // the "beforeNavigate" event in Safari isn't dispatched when a new URL
-    // was entered into the address bar. So we can only use it only on Chrome,
-    // but we have to hide it from the browser-independent high level code.
-    this._onBeforeNavigate = ext.tabs._onBeforeNavigate._bindToTab(this);
-  };
-  Tab.prototype = {
-    get url()
-    {
-      // usually our Tab objects are created from chrome Tab objects, which
-      // provide the url. So we can return the url given in the constructor.
-      if (this._url != null)
-        return this._url;
-
-      // but sometimes we only have the id when we create a Tab object.
-      // In that case we get the url from top frame of the tab, recorded by
-      // the onBeforeRequest handler.
-      var frames = framesOfTabs.get(this);
-      if (frames)
-      {
-        var frame = frames[0];
-        if (frame)
-          return frame.url;
-      }
-    },
-    close: function()
-    {
-      chrome.tabs.remove(this._id);
-    },
-    activate: function()
-    {
-      chrome.tabs.update(this._id, {selected: true});
-    },
-    sendMessage: function(message, responseCallback)
-    {
-      sendMessage(this._id, message, responseCallback);
-    }
-  };
-
-  TabMap = function(deleteOnPageUnload)
-  {
-    this._map = {};
-
-    this._delete = this._delete.bind(this);
-    this._deleteOnPageUnload = deleteOnPageUnload;
-  };
-  TabMap.prototype = {
-    get: function(tab)
-    {
-      return (this._map[tab._id] || {}).value;
-    },
-    set: function(tab, value)
-    {
-      if (!(tab._id in this._map))
-      {
-        tab.onRemoved.addListener(this._delete);
-        if (this._deleteOnPageUnload)
-          tab._onBeforeNavigate.addListener(this._delete);
-      }
-
-      this._map[tab._id] = {tab: tab, value: value};
-    },
-    has: function(tab)
-    {
-      return tab._id in this._map;
-    },
-    clear: function()
-    {
-      for (var id in this._map)
-        this.delete(this._map[id].tab);
-    },
-    _delete: function(tab)
-    {
-      // delay so that other event handlers can still lookup this tab
-      setTimeout(this.delete.bind(this, tab), 0);
-    },
-    delete: function(tab)
-    {
-      delete this._map[tab._id];
-
-      tab.onRemoved.removeListener(this._delete);
-      tab._onBeforeNavigate.removeListener(this._delete);
-    }
-  };
-
-
-  /* Windows */
-
-  Window = function(win)
-  {
-    this._id = win.id;
-    this.visible = win.status != "minimized";
-  };
-  Window.prototype = {
-    getAllTabs: function(callback)
-    {
-      chrome.tabs.query({windowId: this._id}, function(tabs)
-      {
-        callback(tabs.map(function(tab) { return new Tab(tab); }));
-      });
-    },
-    getActiveTab: function(callback)
-    {
-      chrome.tabs.query({windowId: this._id, active: true}, function(tabs)
-      {
-        callback(new Tab(tabs[0]));
-      });
-    },
-    openTab: function(url, callback)
-    {
-      var props = {windowId: this._id, url: url};
-
-      if (!callback)
-        chrome.tabs.create(props);
-      else
-        chrome.tabs.create(props, function(tab)
-        {
-          callback(new Tab(tab));
-        });
-    }
-  };
-
 
   /* Frames */
 
-  var framesOfTabs = new TabMap();
+  var framesOfTabs = {__proto__: null};
 
-  Frame = function(params)
+  var Frame = ext.Frame = function(params)
   {
-    this._tab = params.tab;
-    this._id = params.id;
+    this._frameId = params.frameId;
+    this._tabId = params.tabId;
     this._url = params.url;
   };
   Frame.prototype = {
@@ -394,22 +185,22 @@
       if (this._url != null)
         return this._url;
 
-      var frames = framesOfTabs.get(this._tab);
+      var frames = framesOfTabs[this._tabId];
       if (frames)
       {
-        var frame = frames[this._id];
+        var frame = frames[this._frameId];
         if (frame)
           return frame.url;
       }
     },
     get parent()
     {
-      var frames = framesOfTabs.get(this._tab);
+      var frames = framesOfTabs[this._tabId];
       if (frames)
       {
         var frame;
-        if (this._id != null)
-          frame = frames[this._id];
+        if (this._frameId != null)
+          frame = frames[this._frameId];
         else
         {
           // the frame ID wasn't available when we created
@@ -428,13 +219,18 @@
         if (!frame || frame.parent == -1)
           return null;
 
-        return new Frame({id: frame.parent, tab: this._tab});
+        return new Frame({frameId: frame.parent, tabId: this._tabId});
       }
     }
   };
 
 
-  /* Web request blocking */
+  /* Web requests */
+
+  ext.webRequest = {
+    onBeforeRequest: new ext._EventTarget(true),
+    handlerBehaviorChanged: chrome.webRequest.handlerBehaviorChanged
+  };
 
   chrome.webRequest.onBeforeRequest.addListener(function(details)
   {
@@ -446,13 +242,12 @@
       if (details.tabId == -1)
         return;
 
-      var tab = new Tab({id: details.tabId});
-      var frames = framesOfTabs.get(tab);
+      var page = new Tab({id: details.tabId});
+      var frames = framesOfTabs[details.tabId];
 
       if (!frames)
       {
-        frames = [];
-        framesOfTabs.set(tab, frames);
+        frames = framesOfTabs[details.tabId] = [];
 
         // assume that the first request belongs to the top frame. Chrome
         // may give the top frame the type "object" instead of "main_frame".
@@ -493,13 +288,10 @@
           frames[details.frameId].parent = frameId;
       }
 
-      var frame = new Frame({id: frameId, tab: tab});
+      var frame = new Frame({id: frameId, tabId: details.tabId});
 
-      for (var i = 0; i < ext.webRequest.onBeforeRequest._listeners.length; i++)
-      {
-        if (ext.webRequest.onBeforeRequest._listeners[i](details.url, details.type, tab, frame) === false)
-          return {cancel: true};
-      }
+      if (!ext.webRequest.onBeforeRequest._dispatch(details.url, details.type, page, frame))
+        return {cancel: true};
     }
     catch (e)
     {
@@ -511,49 +303,11 @@
   }, {urls: ["<all_urls>"]}, ["blocking"]);
 
 
-  /* API */
-
-  ext.windows = {
-    getAll: function(callback)
-    {
-      chrome.windows.getAll(function(windows)
-      {
-        callback(windows.map(function(win)
-        {
-          return new Window(win);
-        }));
-      });
-    },
-    getLastFocused: function(callback)
-    {
-      chrome.windows.getLastFocused(function(win)
-      {
-        callback(new Window(win));
-      });
-    }
-  };
-
-  ext.tabs = {
-    onLoading: new LoadingTabEventTarget(),
-    onCompleted: new CompletedTabEventTarget(),
-    onActivated: new ActivatedTabEventTarget(),
-    onRemoved: new RemovedTabEventTarget(),
-
-    // the "beforeNavigate" event in Safari isn't dispatched when a new URL
-    // was entered into the address bar. So we can only use it only on Chrome,
-    // but we have to hide it from the browser-independent high level code.
-    _onBeforeNavigate: new BeforeNavigateTabEventTarget()
-  };
-
-  ext.webRequest = {
-    onBeforeRequest: new SimpleEventTarget(),
-    handlerBehaviorChanged: chrome.webRequest.handlerBehaviorChanged
-  };
-
-  ext.storage = localStorage;
+  /* Context menus */
 
   var contextMenuItems = [];
   var isContextMenuHidden = true;
+
   ext.contextMenus = {
     addMenuItem: function(title, contexts, onclick)
     {
@@ -562,7 +316,7 @@
         contexts: contexts,
         onclick: function(info, tab)
         {
-          onclick(info.srcUrl, new Tab(tab));
+          onclick(info.srcUrl, new Page(tab));
         }
       });
       this.showMenuItems();
@@ -601,5 +355,19 @@
     }
   };
 
-  ext.onMessage = new BackgroundMessageEventTarget();
+
+  /* Message passing */
+
+  ext._setupMessageListener(function(sender)
+  {
+    return {
+      page: new Page(sender.tab),
+      frame: new Frame({url: sender.url, tabId: sender.tab.id})
+    };
+  });
+
+
+  /* Storage */
+
+  ext.storage = localStorage;
 })();
Index: chrome/ext/common.js
===================================================================
--- a/chrome/ext/common.js
+++ b/chrome/ext/common.js
@@ -17,99 +17,48 @@
 
 (function()
 {
-  /* Events */
+  /* Message passing */
 
-  SimpleEventTarget = function()
+  var sendMessage;
+  if ("runtime" in chrome && "sendMessage" in chrome.runtime)
+    sendMessage = chrome.runtime.sendMessage;
+  else if ("sendMessage" in chrome.extension)
+    sendMessage = chrome.extension.sendMessage;
+  else
+    sendMessage = chrome.extension.sendRequest;
+
+  ext._setupMessageListener = function(wrapSender)
   {
-    this._listeners = [];
+    var onMessage;
+    if ("runtime" in chrome && "onMessage" in chrome.runtime)
+      onMessage = chrome.runtime.onMessage;
+    else if ("onMessage" in chrome.extension)
+      onMessage = chrome.extension.onMessage;
+    else
+      onMessage = chrome.extension.onRequest;
+
+    onMessage.addListener(function(message, sender, sendResponse)
+    {
+      ext.onMessage._dispatch(message, wrapSender(sender), sendResponse);
+    });
   };
-  SimpleEventTarget.prototype = {
-    _onListenerAdded: function(listener, idx) {},
-    _onListenerRemoved: function(listener, idx) {},
 
-    addListener: function(listener)
+  ext.onMessage = new ext._EventTarget();
+
+
+  /* Background page */
+
+  ext.backgroundPage = {
+    sendMessage: sendMessage,
+    getWindow: function()
     {
-      var idx = this._listeners.push(listener) - 1;
-      this._onListenerAdded(listener, idx);
-    },
-    removeListener: function(listener)
-    {
-      var idx = this._listeners.indexOf(listener);
-      if (idx != -1)
-      {
-        this._listeners.splice(idx, 1);
-        this._onListenerRemoved(listener, idx);
-      }
+      return chrome.extension.getBackgroundPage();
     }
   };
 
-  WrappedEventTarget = function(target)
-  {
-    SimpleEventTarget.call(this);
 
-    this._wrappedListeners = [];
-    this._target = target;
-  };
-  WrappedEventTarget.prototype = {
-    __proto__: SimpleEventTarget.prototype,
-    _onListenerAdded: function(listener, idx)
-    {
-      var wrappedListener = this._wrapListener(listener);
+  /* Utils */
 
-      this._wrappedListeners[idx] = wrappedListener;
-      this._target.addListener.call(this._target, wrappedListener);
-    },
-    _onListenerRemoved: function(listener, idx)
-    {
-      this._target.removeListener(this._wrappedListeners[idx]);
-      this._wrappedListeners.splice(idx, 1);
-    }
-  };
-
-  MessageEventTarget = function()
-  {
-    var target;
-    if ("runtime" in chrome && "onMessage" in chrome.runtime)
-      target = chrome.runtime.onMessage;
-    else if ("onMessage" in chrome.extension)
-      target = chrome.extension.onMessage;
-    else
-      target = chrome.extension.onRequest;
-    WrappedEventTarget.call(this, target);
-  };
-  MessageEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _wrapSender: function(sender)
-    {
-      return {};
-    },
-    _wrapListener: function(listener)
-    {
-      return function(message, sender, sendResponse)
-      {
-        return listener(message, this._wrapSender(sender), sendResponse);
-      }.bind(this);
-    }
-  };
-
-
-  /* API */
-
-  ext = {
-    backgroundPage: {
-      getWindow: function()
-      {
-        return chrome.extension.getBackgroundPage();
-      }
-    },
-    getURL: chrome.extension.getURL,
-    i18n: chrome.i18n
-  };
-
-  if ("runtime" in chrome && "sendMessage" in chrome.runtime)
-    ext.backgroundPage.sendMessage = chrome.runtime.sendMessage;
-  else if ("sendMessage" in chrome.extension)
-    ext.backgroundPage.sendMessage = chrome.extension.sendMessage;
-  else
-    ext.backgroundPage.sendMessage = chrome.extension.sendRequest;
+  ext.getURL = chrome.extension.getURL;
+  ext.i18n = chrome.i18n;
 })();
Index: chrome/ext/content.js
===================================================================
--- a/chrome/ext/content.js
+++ b/chrome/ext/content.js
@@ -1,1 +1,1 @@
-ext.onMessage = new MessageEventTarget();
+ext._setupMessageListener(function() { return {}; });
Index: chrome/ext/popup.js
===================================================================
--- a/chrome/ext/popup.js
+++ b/chrome/ext/popup.js
@@ -1,12 +1,8 @@
-(function()
-{
-  var backgroundPage = chrome.extension.getBackgroundPage();
-  window.ext = {
-    __proto__: backgroundPage.ext,
-    closePopup: function()
-    {
-      window.close();
-    }
-  };
-  window.TabMap = backgroundPage.TabMap;
-})();
+window.ext = {
+  __proto__: chrome.extension.getBackgroundPage().ext,
+
+  closePopup: function()
+  {
+    window.close();
+  }
+};
Index: ext/background.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/ext/background.js
@@ -0,0 +1,65 @@
+/*
+ * This file is part of Adblock Plus <http://adblockplus.org/>,
+ * Copyright (C) 2006-2014 Eyeo GmbH
+ *
+ * Adblock Plus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Adblock Plus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+(function()
+{
+  var nonEmptyPageMaps = {__proto__: null};
+  var pageMapCounter = 0;
+
+  var PageMap = ext.PageMap = function()
+  {
+    this._map = {__proto__: null};
+    this._id = ++pageMapCounter;
+  };
+  PageMap.prototype = {
+    _delete: function(id)
+    {
+      delete this._map[id];
+
+      if (Object.keys(this._map).length == 0)
+        delete nonEmptyPageMaps[this._id];
+    },
+    get: function(page)
+    {
+      return this._map[page._id];
+    },
+    set: function(page, value)
+    {
+      this._map[page._id] = value;
+      nonEmptyPageMaps[this._id] = this;
+    },
+    has: function(page)
+    {
+      return page._id in this._map;
+    },
+    clear: function()
+    {
+      for (var id in this._map)
+        this._delete(id);
+    },
+    delete: function(page)
+    {
+      this._delete(page._id);
+    }
+  };
+
+  ext._removeFromAllPageMaps = function(pageId)
+  {
+    for (var pageMapId in nonEmptyPageMaps)
+      nonEmptyPageMaps[pageMapId]._delete(pageId);
+  };
+})();
Index: ext/common.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/ext/common.js
@@ -0,0 +1,51 @@
+/*
+ * This file is part of Adblock Plus <http://adblockplus.org/>,
+ * Copyright (C) 2006-2014 Eyeo GmbH
+ *
+ * Adblock Plus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Adblock Plus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+(function()
+{
+  window.ext = {};
+
+  var EventTarget = ext._EventTarget = function(cancelable)
+  {
+    this._listeners = [];
+    this._cancelable = cancelable;
+  };
+  EventTarget.prototype = {
+    addListener: function(listener)
+    {
+      if (this._listeners.indexOf(listener) == -1)
+        this._listeners.push(listener);
+    },
+    removeListener: function(listener)
+    {
+      var idx = this._listeners.indexOf(listener);
+      if (idx != -1)
+        this._listeners.splice(idx, 1);
+    },
+    _dispatch: function(cancelable, args)
+    {
+      for (var i = 0; i < this._listeners.length; i++)
+      {
+        var result = this._listeners[i].apply(null, arguments);
+        if (this._cancelable && result === false)
+          return false;
+      }
+      return true;
+    }
+  };
+})();
Index: iconAnimation.js
===================================================================
--- a/iconAnimation.js
+++ b/iconAnimation.js
@@ -16,8 +16,8 @@
  */
 
 iconAnimation = {
-  _icons: new TabMap(),
-  _animatedTabs: new TabMap(),
+  _icons: new ext.PageMap(),
+  _animatedPages: new ext.PageMap(),
   _step: 0,
 
   update: function(type)
@@ -37,31 +37,31 @@
     delete this._interval;
     delete this._type;
 
-    this._animatedTabs.clear();
+    this._animatedPages.clear();
   },
-  registerTab: function(tab, icon)
+  registerPage: function(page, icon)
   {
-    this._icons.set(tab, icon);
+    this._icons.set(page, icon);
 
-    if (this._animatedTabs.has(tab))
-      this._updateIcon(tab);
+    if (this._animatedPages.has(page))
+      this._updateIcon(page);
   },
   _start: function()
   {
     this._interval = setInterval(function()
     {
-      this._getVisibleTabs(function(tabs)
+      ext.pages.query({active: true}, function(pages)
       {
-        if (tabs.length == 0)
+        if (pages.length == 0)
           return;
 
-        for (var i = 0; i < tabs.length; i++)
-          this._animatedTabs.set(tabs[i], null);
+        for (var i = 0; i < pages.length; i++)
+          this._animatedPages.set(pages[i], null);
 
         var interval = setInterval(function()
         {
           this._step++;
-          tabs.forEach(this._updateIcon.bind(this));
+          pages.forEach(this._updateIcon.bind(this));
 
           if (this._step < 10)
             return;
@@ -72,49 +72,22 @@
             interval = setInterval(function()
             {
               this._step--;
-              tabs.forEach(this._updateIcon.bind(this));
+              pages.forEach(this._updateIcon.bind(this));
 
               if (this._step > 0)
                 return;
 
               clearInterval(interval);
-              this._animatedTabs.clear();
+              this._animatedPages.clear();
             }.bind(this), 100);
           }.bind(this), 1000);
         }.bind(this), 100);
       }.bind(this));
     }.bind(this), 15000);
   },
-  _getVisibleTabs: function(callback)
+  _updateIcon: function(page)
   {
-    ext.windows.getAll(function(windows)
-    {
-      var tabs = [];
-      var visibleWindows = windows.length;
-
-      for (var i = 0; i < windows.length; i++)
-      {
-        if (!windows[i].visible)
-        {
-          if (--visibleWindows == 0)
-            callback(tabs);
-
-          continue;
-        }
-
-        windows[i].getActiveTab(function(tab)
-        {
-          tabs.push(tab);
-
-          if (tabs.length == visibleWindows)
-            callback(tabs);
-        });
-      }
-    });
-  },
-  _updateIcon: function(tab)
-  {
-    var path = this._icons.get(tab);
+    var path = this._icons.get(page);
 
     if (!path)
       return;
@@ -129,6 +102,6 @@
       path = path.replace(/(?=\..+$)/, suffix);
     }
 
-    tab.browserAction.setIcon(path);
+    page.browserAction.setIcon(path);
   }
 };
Index: lib/stats.js
===================================================================
--- a/lib/stats.js
+++ b/lib/stats.js
@@ -24,26 +24,26 @@
 let {FilterNotifier} = require("filterNotifier");
 
 let badgeColor = "#646464";
-let statsPerTab = new TabMap(true);
+let statsPerPage = new ext.PageMap();
 
 /**
- * Get statistics for specified tab
+ * Get statistics for specified page
  * @param  {String} key   field key
- * @param  {Number} tabId tab ID (leave undefined for total stats)
+ * @param  {Page}   page  field page
  * @return {Number}       field value
  */
-let getStats = exports.getStats = function getStats(key, tab)
+let getStats = exports.getStats = function getStats(key, page)
 {
-  if (!tab)
+  if (!page)
     return (key in Prefs.stats_total ? Prefs.stats_total[key] : 0);
 
-  let tabStats = statsPerTab.get(tab);
-  return tabStats ? tabStats.blocked : 0;
+  let pageStats = statsPerPage.get(page);
+  return pageStats ? pageStats.blocked : 0;
 };
 
-FilterNotifier.addListener(function(action, item, newValue, oldValue, tab)
+FilterNotifier.addListener(function(action, item, newValue, oldValue, page)
 {
-  if (action != "filter.hitCount" || !tab)
+  if (action != "filter.hitCount" || !page)
     return;
 
   let blocked = item instanceof BlockingFilter;
@@ -57,66 +57,47 @@
       Prefs.stats_total.blocked = 1;
     Prefs.stats_total = Prefs.stats_total;
 
-    let tabStats = statsPerTab.get(tab);
-    if (!tabStats)
+    let pageStats = statsPerPage.get(page);
+    if (!pageStats)
     {
-      tabStats = {};
-      statsPerTab.set(tab, tabStats);
+      pageStats = {};
+      statsPerPage.set(page, pageStats);
     }
-    if ("blocked" in tabStats)
-      tabStats.blocked++;
+    if ("blocked" in pageStats)
+      pageStats.blocked++;
     else
-      tabStats.blocked = 1;
+      pageStats.blocked = 1;
 
     // Update number in icon
     if (Prefs.show_statsinicon)
     {
-      tab.browserAction.setBadge({
+      page.browserAction.setBadge({
         color: badgeColor,
-        number: tabStats.blocked
+        number: pageStats.blocked
       });
     }
   }
 });
 
-/**
- * Execute function for each tab in any window
- * @param {Function} func function to be executed
- */
-function forEachTab(func)
-{
-  ext.windows.getAll(function(windows)
-  {
-    for each (let window in windows)
-    {
-      window.getAllTabs(function(tabs)
-      {
-        for (let i = 0; i < tabs.length; i++)
-          func(tabs[i]);
-      });
-    }
-  });
-}
-
 Prefs.addListener(function(name)
 {
   if (name != "show_statsinicon")
     return;
 
-  forEachTab(function(tab)
+  ext.pages.query({}, function(page)
   {
     let badge = null;
     if (Prefs.show_statsinicon)
     {
-      let tabStats = statsPerTab.get(tab);
-      if (tabStats && "blocked" in tabStats)
+      let pageStats = statsPerPage.get(page);
+      if (pageStats && "blocked" in pageStats)
       {
         badge = {
           color: badgeColor,
-          number: tabStats.blocked
+          number: pageStats.blocked
         };
       }
     }
-    tab.browserAction.setBadge(badge);
+    page.browserAction.setBadge(badge);
   });
 });
Index: lib/whitelisting.js
===================================================================
--- a/lib/whitelisting.js
+++ b/lib/whitelisting.js
@@ -18,7 +18,7 @@
 let {defaultMatcher} = require("matcher");
 let {WhitelistFilter} = require("filterClasses");
 
-let tabsWithKeyException = new TabMap(true);
+let pagesWithKeyException = new ext.PageMap();
 
 let isWhitelisted = exports.isWhitelisted = function(url, parentUrl, type)
 {
@@ -32,9 +32,9 @@
   return (filter instanceof WhitelistFilter ? filter : null);
 };
 
-let isFrameWhitelisted = exports.isFrameWhitelisted = function(tab, frame, type)
+let isFrameWhitelisted = exports.isFrameWhitelisted = function(page, frame, type)
 {
-  let urlsWithKeyException = tabsWithKeyException.get(tab);
+  let urlsWithKeyException = pagesWithKeyException.get(page);
 
   for (; frame != null; frame = frame.parent)
   {
@@ -69,24 +69,24 @@
   return verifySignature(key, signature, params.join("\0"));
 };
 
-let recordKeyException = function(tab, url)
+let recordKeyException = function(page, url)
 {
-  let urlsWithKeyException = tabsWithKeyException.get(tab);
+  let urlsWithKeyException = pagesWithKeyException.get(page);
 
   if (!urlsWithKeyException)
   {
     urlsWithKeyException = {__proto__: null};
-    tabsWithKeyException.set(tab, urlsWithKeyException);
+    pagesWithKeyException.set(page, urlsWithKeyException);
   }
 
   urlsWithKeyException[url] = null;
 };
 
-let processKeyException = exports.processKeyException = function(token, tab, frame)
+let processKeyException = exports.processKeyException = function(token, page, frame)
 {
   let url = stripFragmentFromURL(frame.url);
   let docDomain = extractHostFromURL((frame.parent || frame).url);
 
   if (verifyKeyException(token, url, docDomain))
-    recordKeyException(tab, url);
+    recordKeyException(page, url);
 };
Index: metadata.chrome
===================================================================
--- a/metadata.chrome
+++ b/metadata.chrome
@@ -18,7 +18,7 @@
 webAccessible = block.html
 
 [compat]
-chrome = 18.0
+chrome = 19.0
 
 [convert_js]
 lib/io.js = lib/filesystem/io.js
Index: metadata.common
===================================================================
--- a/metadata.common
+++ b/metadata.common
@@ -23,8 +23,6 @@
 document_end = include.postload.js
 
 [mapping]
-ext/common.js = chrome/ext/common.js
-ext/background.js = chrome/ext/background.js
 ext/content.js = chrome/ext/content.js
 ext/popup.js = chrome/ext/popup.js
 notification.html = chrome/notification.html
@@ -73,6 +71,8 @@
   lib/stats.js
   lib/whitelisting.js
   --arg module=true source_repo=https://hg.adblockplus.org/adblockplus/
+ext/common.js = ext/common.js chrome/ext/common.js
+ext/background.js = ext/background.js chrome/ext/background.js --arg brace_style=expand
 
 qunit/tests/adblockplus.js = adblockplustests/chrome/content/tests/domainRestrictions.js
   adblockplustests/chrome/content/tests/filterClasses.js
Index: metadata.safari
===================================================================
--- a/metadata.safari
+++ b/metadata.safari
@@ -10,8 +10,6 @@
 document_start = ext/common.js ext/content.js include.preload.js include.youtube.js
 
 [mapping]
-ext/common.js = safari/ext/common.js
-ext/background.js = safari/ext/background.js
 ext/content.js = safari/ext/content.js
 ext/popup.js = safari/ext/popup.js
 include.youtube.js = safari/include.youtube.js
@@ -20,6 +18,8 @@
 [convert_js]
 lib/io.js = lib/storage/io.js
   --arg module=true
+ext/common.js = ext/common.js safari/ext/common.js
+ext/background.js = ext/background.js safari/ext/background.js --arg brace_style=expand
 
 [popovers]
 popup_filename = popup.html
Index: notification.js
===================================================================
--- a/notification.js
+++ b/notification.js
@@ -88,7 +88,7 @@
       return;
     event.preventDefault();
     event.stopPropagation();
-    ext.windows.getLastFocused(function(win) { win.openTab(link.href); });
+    ext.pages.open(link.href);
   });
 
   if (notification.type == "question")
Index: popup.js
===================================================================
--- a/popup.js
+++ b/popup.js
@@ -25,18 +25,33 @@
 var Prefs = require("prefs").Prefs;
 var isWhitelisted = require("whitelisting").isWhitelisted;
 
-var tab = null;
+var page = null;
 
 function init()
 {
-  // Mark page as local to hide non-relevant elements
-  ext.windows.getLastFocused(function(win)
+  ext.pages.query({active: true, lastFocusedWindow: true}, function(pages)
   {
-    win.getActiveTab(function(tab)
+    page = pages[0];
+
+    // Mark page as local to hide non-relevant elements
+    if (!page || !/^https?:\/\//.test(page.url))
+      document.body.classList.add("local");
+
+    // Ask content script whether clickhide is active. If so, show cancel button.
+    // If that isn't the case, ask background.html whether it has cached filters. If so,
+    // ask the user whether she wants those filters.
+    // Otherwise, we are in default state.
+    if (page)
     {
-      if (!/^https?:\/\//.exec(tab.url))
-        document.body.classList.add("local");
-    });
+      if (isWhitelisted(page.url))
+        document.getElementById("enabled").classList.add("off");
+
+      page.sendMessage({type: "get-clickhide-state"}, function(response)
+      {
+        if (response && response.active)
+          document.body.classList.add("clickhide-active");
+      });
+    }
   });
 
   // Attach event listeners
@@ -57,26 +72,6 @@
     if (!Prefs[collapser.dataset.option])
       document.getElementById(collapser.dataset.collapsable).classList.add("collapsed");
   }
-
-  // Ask content script whether clickhide is active. If so, show cancel button.
-  // If that isn't the case, ask background.html whether it has cached filters. If so,
-  // ask the user whether she wants those filters.
-  // Otherwise, we are in default state.
-  ext.windows.getLastFocused(function(win)
-  {
-    win.getActiveTab(function(t)
-    {
-      tab = t;
-      if (isWhitelisted(tab.url))
-        document.getElementById("enabled").classList.add("off");
-
-      tab.sendMessage({type: "get-clickhide-state"}, function(response)
-      {
-        if (response && response.active)
-          document.body.classList.add("clickhide-active");
-      });
-    });
-  });
 }
 window.addEventListener("DOMContentLoaded", init, false);
 
@@ -86,7 +81,7 @@
   var disabled = enabledButton.classList.toggle("off");
   if (disabled)
   {
-    var host = extractHostFromURL(tab.url).replace(/^www\./, "");
+    var host = extractHostFromURL(page.url).replace(/^www\./, "");
     var filter = Filter.fromText("@@||" + host + "^$document");
     if (filter.subscriptions.length && filter.disabled)
       filter.disabled = false;
@@ -99,13 +94,13 @@
   else
   {
     // Remove any exception rules applying to this URL
-    var filter = isWhitelisted(tab.url);
+    var filter = isWhitelisted(page.url);
     while (filter)
     {
       FilterStorage.removeFilter(filter);
       if (filter.subscriptions.length)
         filter.disabled = true;
-      filter = isWhitelisted(tab.url);
+      filter = isWhitelisted(page.url);
     }
   }
 }
@@ -113,7 +108,7 @@
 function activateClickHide()
 {
   document.body.classList.add("clickhide-active");
-  tab.sendMessage({type: "clickhide-activate"});
+  page.sendMessage({type: "clickhide-activate"});
 
   // Close the popup after a few seconds, so user doesn't have to
   activateClickHide.timeout = window.setTimeout(ext.closePopup, 5000);
@@ -127,7 +122,7 @@
     activateClickHide.timeout = null;
   }
   document.body.classList.remove("clickhide-active");
-  tab.sendMessage({type: "clickhide-deactivate"});
+  page.sendMessage({type: "clickhide-deactivate"});
 }
 
 function toggleCollapse(event)
Index: popupBlocker.js
===================================================================
--- a/popupBlocker.js
+++ b/popupBlocker.js
@@ -21,10 +21,10 @@
 
   chrome.webNavigation.onCreatedNavigationTarget.addListener(function(details)
   {
-    var sourceTab = new Tab({id: details.sourceTabId});
-    var sourceFrame = new Frame({id: details.sourceFrameId, tab: sourceTab});
+    var sourcePage = new ext.Page({id: details.sourceTabId});
+    var sourceFrame = new ext.Frame({frameId: details.sourceFrameId, tabId: details.sourceTabId});
 
-    if (isFrameWhitelisted(sourceTab, sourceFrame))
+    if (isFrameWhitelisted(sourcePage, sourceFrame))
       return;
 
     var openerUrl = sourceFrame.url;
Index: safari/ext/background.js
===================================================================
--- a/safari/ext/background.js
+++ b/safari/ext/background.js
@@ -17,85 +17,35 @@
 
 (function()
 {
-  /* Events */
+  /* Pages */
 
-  var TabEventTarget = function()
+  var pages = {__proto__: null};
+  var pageCounter = 0;
+
+  var Page = function(id, tab, url, prerendered)
   {
-    WrappedEventTarget.apply(this, arguments);
-  };
-  TabEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(event)
-      {
-        if (event.target instanceof SafariBrowserTab)
-          listener(new Tab(event.target));
+    this._id = id;
+    this._tab = tab;
+    this._frames = [{url: url, parent: null}];
+    this._prerendered = prerendered;
+
+    if (tab.page)
+      this._messageProxy = new ext._MessageProxy(tab.page);
+    else
+      // while the new tab page is shown on Safari 7, the 'page' property
+      // of the tab is undefined, and we can't send messages to that page
+      this._messageProxy = {
+        handleRequest: function() {},
+        handleResponse: function() {},
+        sendMessage: function() {}
       };
-    }
-  };
-
-  var LoadingTabEventTarget = function(target)
-  {
-    WrappedEventTarget.call(this, target, "message", false);
-  };
-  LoadingTabEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function (event)
-      {
-        if (event.name == "loading")
-          listener(new Tab(event.target));
-      };
-    }
-  };
-
-  var BackgroundMessageEventTarget = function()
-  {
-    MessageEventTarget.call(this, safari.application);
-  };
-  BackgroundMessageEventTarget.prototype = {
-    __proto__: MessageEventTarget.prototype,
-    _getResponseDispatcher: function(event)
-    {
-      return event.target.page;
-    },
-    _getSenderDetails: function(event)
-    {
-      return {
-        tab: new Tab(event.target),
-        frame: new Frame(
-          event.message.documentUrl,
-          event.message.isTopLevel,
-          event.target
-        )
-      };
-    }
-  };
-
-
-  /* Tabs */
-
-  Tab = function(tab)
-  {
-    this._tab = tab;
 
     this.browserAction = new BrowserAction(this);
-
-    this.onLoading = new LoadingTabEventTarget(tab);
-    this.onCompleted = new TabEventTarget(tab, "navigate", false);
-    this.onActivated = new TabEventTarget(tab, "activate", false);
-    this.onRemoved = new TabEventTarget(tab, "close", false);
   };
-  Tab.prototype = {
+  Page.prototype = {
     get url()
     {
-      return this._tab.url;
-    },
-    close: function()
-    {
-      this._tab.close();
+      return this._frames[0].url;
     },
     activate: function()
     {
@@ -103,106 +53,98 @@
     },
     sendMessage: function(message, responseCallback)
     {
-      _sendMessage(
-        message, responseCallback,
-        this._tab.page, this._tab
-      );
+      this._messageProxy.sendMessage(message, responseCallback, {pageId: this._id});
     }
   };
 
-  TabMap = function(deleteOnPageUnload)
+  var isPageActive = function(page)
   {
-    this._data = [];
-    this._deleteOnPageUnload = deleteOnPageUnload;
+    return page._tab == page._tab.browserWindow.activeTab && !page._prerendered;
+  };
 
-    this.delete = this.delete.bind(this);
-    this._delete = this._delete.bind(this);
+  var forgetPage = function(id)
+  {
+    ext._removeFromAllPageMaps(id);
+    delete pages[id];
   };
-  TabMap.prototype =
+
+  var replacePage = function(page)
   {
-    _indexOf: function(tab)
+    for (var id in pages)
     {
-      for (var i = 0; i < this._data.length; i++)
-        if (this._data[i].tab._tab == tab._tab)
-          return i;
+      if (id != page._id && pages[id]._tab == page._tab)
+        forgetPage(id);
+    }
 
-      return -1;
-    },
-    _delete: function(tab)
+    if (isPageActive(page))
+      updateToolbarItemForPage(page);
+  };
+
+  ext.pages = {
+    open: function(url, callback)
     {
-      // delay so that other onClosed listeners can still look this tab up
-      setTimeout(this.delete.bind(this, tab), 0);
-    },
-    get: function(tab) {
-      var idx;
+      var tab = safari.application.activeBrowserWindow.openTab();
+      tab.url = url;
 
-      if (!tab || (idx = this._indexOf(tab)) == -1)
-        return null;
-
-      return this._data[idx].value;
-    },
-    set: function(tab, value)
-    {
-      var idx = this._indexOf(tab);
-
-      if (idx != -1)
-        this._data[idx].value = value;
-      else
+      if (callback)
       {
-        this._data.push({value: value, tab: tab});
-
-        tab.onRemoved.addListener(this._delete);
-        if (this._deleteOnPageUnload)
-          tab.onLoading.addListener(this.delete);
+        var onLoading = function(page)
+        {
+          if (page._tab == tab)
+          {
+            ext.pages.onLoading.removeListener(onLoading);
+            callback(page);
+          }
+        };
+        ext.pages.onLoading.addListener(onLoading);
       }
     },
-    has: function(tab)
+    query: function(info, callback)
     {
-      return this._indexOf(tab) != -1;
+      var matchedPages = [];
+
+      for (var id in pages)
+      {
+        var page = pages[id];
+        var win = page._tab.browserWindow;
+
+        if ("active" in info && info.active != isPageActive(page))
+          continue;
+        if ("lastFocusedWindow" in info && info.lastFocusedWindow != (win == safari.application.activeBrowserWindow))
+          continue;
+
+        matchedPages.push(page);
+      };
+
+      callback(matchedPages);
     },
-    clear: function()
-    {
-      while (this._data.length > 0)
-        this.delete(this._data[0].tab);
-    },
-    delete: function(tab)
-    {
-      var idx = this._indexOf(tab);
-
-      if (idx != -1)
-      {
-        tab = this._data[idx].tab;
-        this._data.splice(idx, 1);
-
-        tab.onRemoved.removeListener(this._delete);
-        tab.onLoading.removeListener(this.delete);
-      }
-    }
+    onLoading: new ext._EventTarget()
   };
 
-  ext.tabs = {
-    onLoading: new LoadingTabEventTarget(safari.application),
-    onCompleted: new TabEventTarget(safari.application, "navigate", true),
-    onActivated: new TabEventTarget(safari.application, "activate", true),
-    onRemoved: new TabEventTarget(safari.application, "close", true)
-  };
+  safari.application.addEventListener("close", function(event)
+  {
+    // this event is dispatched on closing windows and tabs. However when a
+    // window is closed, it is first dispatched on each tab in the window and
+    // then on the window itself. But we are only interested in closed tabs.
+    if (!(event.target instanceof SafariBrowserTab))
+      return;
+
+    // when a tab is closed, forget the previous page associated with that
+    // tab. Note that it wouldn't be sufficient do that when the old page
+    // is unloading, because Safari dispatches window.onunload only when
+    // reloading the page or following links, but not when closing the tab.
+    for (var id in pages)
+    {
+      if (pages[id]._tab == event.target)
+        forgetPage(id);
+    }
+  }, true);
 
 
   /* Browser actions */
 
   var toolbarItemProperties = {};
 
-  var getToolbarItemProperty = function(name)
-  {
-    var property = toolbarItemProperties[name];
-    if (!property)
-    {
-      property = {tabs: new TabMap()};
-      toolbarItemProperties[name] = property;
-    }
-    return property;
-  };
-
   var getToolbarItemForWindow = function(win)
   {
     for (var i = 0; i < safari.extension.toolbarItems.length; i++)
@@ -216,27 +158,44 @@
     return null;
   };
 
-  var BrowserAction = function(tab)
+  var updateToolbarItemForPage = function(page, win) {
+    var toolbarItem = getToolbarItemForWindow(win || page._tab.browserWindow);
+    if (!toolbarItem)
+      return;
+
+    for (var name in toolbarItemProperties)
+    {
+      var property = toolbarItemProperties[name];
+
+      if (page && property.pages.has(page))
+        toolbarItem[name] = property.pages.get(page);
+      else
+        toolbarItem[name] = property.global;
+    }
+  };
+
+  var BrowserAction = function(page)
   {
-    this._tab = tab;
+    this._page = page;
   };
   BrowserAction.prototype = {
     _set: function(name, value)
     {
-      var currentWindow = this._tab._tab.browserWindow;
-      var toolbarItem = getToolbarItemForWindow(currentWindow);
+      var toolbarItem = getToolbarItemForWindow(this._page._tab.browserWindow);
+      if (!toolbarItem)
+        return;
 
-      if (toolbarItem)
-      {
-        var property = getToolbarItemProperty(name);
-        property.tabs.set(this._tab, value);
+      var property = toolbarItemProperties[name];
+      if (!property)
+        property = toolbarItemProperties[name] = {
+          pages: new ext.PageMap(),
+          global: toolbarItem[name]
+        };
 
-        if (!("global" in property))
-          property.global = toolbarItem[name];
+      property.pages.set(this._page, value);
 
-        if (this._tab._tab == currentWindow.activeTab)
-          toolbarItem[name] = value;
-      }
+      if (isPageActive(this._page))
+        toolbarItem[name] = value;
     },
     setIcon: function(path)
     {
@@ -251,96 +210,121 @@
     }
   };
 
-  ext.tabs.onActivated.addListener(function(tab)
+  safari.application.addEventListener("activate", function(event)
   {
-    var toolbarItem = getToolbarItemForWindow(tab._tab.browserWindow);
-
-    if (!toolbarItem)
+    // this event is also dispatched on windows that got focused. But we
+    // are only interested in tabs, which became active in their window.
+    if (!(event.target instanceof SafariBrowserTab))
       return;
 
-    for (var name in toolbarItemProperties)
+    // update the toolbar item for the page visible in the tab that just
+    // became active. If we can't find that page (e.g. when a page was
+    // opened in a new tab, and our content script didn't run yet), the
+    // toolbar item of the window, is reset to its intial configuration.
+    var activePage = null;
+    for (var id in pages)
     {
-      var property = toolbarItemProperties[name];
+      var page = pages[id];
+      if (page._tab == event.target && !page._prerendered)
+      {
+        activePage = page;
+        break;
+      }
+    }
 
-      if (property.tabs.has(tab))
-        toolbarItem[name] = property.tabs.get(tab);
-      else
-        toolbarItem[name] = property.global;
+    updateToolbarItemForPage(activePage, event.target.browserWindow);
+  }, true);
+
+
+  /* Web requests */
+
+  ext.webRequest = {
+    onBeforeRequest: new ext._EventTarget(true),
+    handlerBehaviorChanged: function() {}
+  };
+
+
+  /* Context menus */
+
+  var contextMenuItems = [];
+  var isContextMenuHidden = true;
+
+  ext.contextMenus = {
+    addMenuItem: function(title, contexts, onclick)
+    {
+      contextMenuItems.push({
+        id: String(contextMenuItems.length),
+        title: title,
+        item: null,
+        contexts: contexts,
+        onclick: onclick
+      });
+      this.showMenuItems();
+    },
+    removeMenuItems: function()
+    {
+      contextMenuItems = [];
+      this.hideMenuItems();
+    },
+    showMenuItems: function()
+    {
+      isContextMenuHidden = false;
+    },
+    hideMenuItems: function()
+    {
+      isContextMenuHidden = true;
+    }
+  };
+
+  safari.application.addEventListener("contextmenu", function(event)
+  {
+    if (isContextMenuHidden)
+      return;
+
+    var context = event.userInfo.tagName;
+    if (context == "img")
+      context = "image";
+    if (!event.userInfo.srcUrl)
+      context = null;
+
+    for (var i = 0; i < contextMenuItems.length; i++)
+    {
+      // Supported contexts are: all, audio, image, video
+      var menuItem = contextMenuItems[i];
+      if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(context) == -1)
+        continue;
+
+      event.contextMenu.appendContextMenuItem(menuItem.id, menuItem.title);
     }
   });
 
-  ext.tabs.onLoading.addListener(function(tab)
+  safari.application.addEventListener("command", function(event)
   {
-    var currentWindow = tab._tab.browserWindow;
-
-    var toolbarItem;
-    if (tab._tab == currentWindow.activeTab)
-      toolbarItem = getToolbarItemForWindow(currentWindow);
-    else
-      toolbarItem = null;
-
-    for (var name in toolbarItemProperties)
+    for (var i = 0; i < contextMenuItems.length; i++)
     {
-      var property = toolbarItemProperties[name];
-      property.tabs.delete(tab);
-
-      if (toolbarItem)
-        toolbarItem[name] = property.global;
+      if (contextMenuItems[i].id == event.command)
+      {
+        contextMenuItems[i].onclick(event.userInfo.srcUrl, pages[event.userInfo.pageId]);
+        break;
+      }
     }
   });
 
 
-  /* Windows */
+  /* Background page */
 
-  Window = function(win)
-  {
-    this._win = win;
-  }
-  Window.prototype = {
-    get visible()
+  ext.backgroundPage = {
+    getWindow: function()
     {
-      return this._win.visible;
-    },
-    getAllTabs: function(callback)
-    {
-      callback(this._win.tabs.map(function(tab) { return new Tab(tab); }));
-    },
-    getActiveTab: function(callback)
-    {
-      callback(new Tab(this._win.activeTab));
-    },
-    openTab: function(url, callback)
-    {
-      var tab = this._win.openTab();
-      tab.url = url;
-
-      if (callback)
-        callback(new Tab(tab));
+      return window;
     }
   };
 
 
-  /* Frames */
+  /* Background page proxy (for access from content scripts) */
 
-  Frame = function(url, isTopLevel, tab)
-  {
-    this.url = url;
-
-    // there is no way to discover frames with Safari's API.
-    // so if this isn't the top level frame, assume that the parent is.
-    // this is the best we can do for Safari. :(
-    if (!isTopLevel)
-      this.parent = new Frame(tab.url, true);
-    else
-      this.parent = null;
-  };
-
-
-  /* Background page proxy */
-
-  var proxy = {
-    tabs: [],
-    objects: [],
+  var backgroundPageProxy = {
+    cache: new ext.PageMap(),
 
     registerObject: function(obj, objects)
     {
@@ -389,27 +373,31 @@
 
       return {type: "value", value: obj};
     },
-    createCallback: function(callbackId, tab)
+    createCallback: function(callbackId, pageId, frameId)
     {
       var proxy = this;
 
       return function()
       {
-        var idx = proxy.tabs.indexOf(tab);
+        var page = pages[pageId];
+        if (!page)
+          return;
 
-        if (idx != -1) {
-          var objects = proxy.objects[idx];
+        var objects = proxy.cache.get(page);
+        if (!objects)
+          return;
 
-          tab.page.dispatchMessage("proxyCallback",
-          {
-            callbackId: callbackId,
-            contextId: proxy.registerObject(this, objects),
-            args: proxy.serializeSequence(arguments, objects)
-          });
-        }
+        page._tab.page.dispatchMessage("proxyCallback",
+        {
+          pageId: pageId,
+          frameId: frameId,
+          callbackId: callbackId,
+          contextId: proxy.registerObject(this, objects),
+          args: proxy.serializeSequence(arguments, objects)
+        });
       };
     },
-    deserialize: function(spec, objects, tab, memo)
+    deserialize: function(spec, objects, pageId, memo)
     {
       switch (spec.type)
       {
@@ -418,7 +406,7 @@
         case "hosted":
           return objects[spec.objectId];
         case "callback":
-          return this.createCallback(spec.callbackId, tab);
+          return this.createCallback(spec.callbackId, pageId, spec.frameId);
         case "object":
         case "array":
           if (!memo)
@@ -439,44 +427,22 @@
 
           if (spec.type == "array")
             for (var i = 0; i < spec.items.length; i++)
-              obj.push(this.deserialize(spec.items[i], objects, tab, memo));
+              obj.push(this.deserialize(spec.items[i], objects, pageId, memo));
           else
             for (var k in spec.properties)
-              obj[k] = this.deserialize(spec.properties[k], objects, tab, memo);
+              obj[k] = this.deserialize(spec.properties[k], objects, pageId, memo);
 
           return obj;
       }
     },
-    createObjectCache: function(tab)
+    getObjectCache: function(page)
     {
-      var objects = [window];
-
-      this.tabs.push(tab);
-      this.objects.push(objects);
-
-      tab.addEventListener("close", function()
+      var objects = this.cache.get(page);
+      if (!objects)
       {
-        var idx = this.tabs.indexOf(tab);
-
-        if (idx != -1)
-        {
-          this.tabs.splice(idx, 1);
-          this.objects.splice(idx, 1);
-        }
-      }.bind(this));
-
-      return objects;
-    },
-    getObjectCache: function(tab)
-    {
-      var idx = this.tabs.indexOf(tab);
-      var objects;
-
-      if (idx != -1)
-        objects = this.objects[idx];
-      else
-        objects = this.objects[idx] = this.createObjectCache(tab);
-
+        objects = [window];
+        this.cache.set(page, objects);
+      }
       return objects;
     },
     fail: function(error)
@@ -485,9 +451,9 @@
         error = error.message;
       return {succeed: false, error: error};
     },
-    _handleMessage: function(message, tab)
+    handleMessage: function(message)
     {
-      var objects = this.getObjectCache(tab);
+      var objects = this.getObjectCache(pages[message.pageId]);
 
       switch (message.type)
       {
@@ -506,7 +472,7 @@
           return {succeed: true, result: this.serialize(value, objects)};
         case "setProperty":
           var obj = objects[message.objectId];
-          var value = this.deserialize(message.value, objects, tab);
+          var value = this.deserialize(message.value, objects, message.pageId);
 
           try
           {
@@ -524,7 +490,7 @@
 
           var args = [];
           for (var i = 0; i < message.args.length; i++)
-            args.push(this.deserialize(message.args[i], objects, tab));
+            args.push(this.deserialize(message.args[i], objects, message.pageId));
 
           try
           {
@@ -561,151 +527,140 @@
   };
 
 
-  /* Web request blocking */
-
-  ext.webRequest = {
-    onBeforeRequest: {
-      _listeners: [],
-
-      _handleMessage: function(message, rawTab)
-      {
-        var tab = new Tab(rawTab);
-        var frame = new Frame(message.documentUrl, message.isTopLevel, rawTab);
-
-        for (var i = 0; i < this._listeners.length; i++)
-        {
-          if (this._listeners[i](message.url, message.type, tab, frame) === false)
-            return false;
-        }
-
-        return true;
-      },
-      addListener: function(listener)
-      {
-        this._listeners.push(listener);
-      },
-      removeListener: function(listener)
-      {
-        var idx = this._listeners.indexOf(listener);
-        if (idx != -1)
-          this._listeners.splice(idx, 1);
-      }
-    },
-    handlerBehaviorChanged: function() {}
-  };
-
-
-  /* Synchronous messaging */
+  /* Message processing */
 
   safari.application.addEventListener("message", function(event)
   {
-    if (event.name == "canLoad")
+    switch (event.name)
     {
-      var handler;
+      case "canLoad":
+        switch (event.message.category)
+        {
+          case "loading":
+            var pageId;
+            var frameId;
 
-      switch (event.message.type)
-      {
-        case "proxy":
-          handler = proxy;
-          break;
-        case "webRequest":
-          handler = ext.webRequest.onBeforeRequest;
-          break;
-      }
+            if (event.message.isTopLevel)
+            {
+              pageId = ++pageCounter;
+              frameId = 0;
 
-      event.message = handler._handleMessage(event.message.payload, event.target);
+              var isPrerendered = event.message.isPrerendered;
+              var page = pages[pageId] = new Page(
+                pageId,
+                event.target,
+                event.message.url,
+                isPrerendered
+              );
+
+              // when a new page is shown, forget the previous page associated
+              // with its tab, and reset the toolbar item if necessary.
+              // Note that it wouldn't be sufficient to do that when the old
+              // page is unloading, because Safari dispatches window.onunload
+              // only when reloading the page or following links, but not when
+              // you enter a new URL in the address bar.
+              if (!isPrerendered)
+                replacePage(page);
+
+              ext.pages.onLoading._dispatch(page);
+            }
+            else
+            {
+              var page;
+              var parentFrame;
+
+              var lastPageId;
+              var lastPage;
+              var lastPageTopLevelFrame;
+
+              // find the parent frame and its page for this sub frame,
+              // by matching its referrer with the URL of frames previously
+              // loaded in the same tab. If there is more than one match,
+              // the most recent loaded page and frame is preferred.
+              for (var curPageId in pages)
+              {
+                var curPage = pages[curPageId];
+                if (curPage._tab != event.target)
+                  continue;
+
+                for (var i = 0; i < curPage._frames.length; i++)
+                {
+                  var curFrame = curPage._frames[i];
+
+                  if (curFrame.url == event.message.referrer)
+                  {
+                    pageId = curPageId;
+                    page = curPage;
+                    parentFrame = curFrame;
+                  }
+
+                  if (i == 0)
+                  {
+                    lastPageId = curPageId;
+                    lastPage = curPage;
+                    lastPageTopLevelFrame = curFrame;
+                  }
+                }
+              }
+
+              // if we can't find the parent frame and its page, fall back to
+              // the page most recently loaded in the tab and its top level frame
+              if (!page)
+              {
+                pageId = lastPageId;
+                page = lastPage;
+                parentFrame = lastPageTopLevelFrame;
+              }
+
+              frameId = page._frames.length;
+              page._frames.push({
+                url: event.message.url,
+                parent: parentFrame
+              });
+            }
+
+            event.message = {pageId: pageId, frameId: frameId};
+            break;
+          case "webRequest":
+            var page = pages[event.message.pageId];
+
+            event.message = ext.webRequest.onBeforeRequest._dispatch(
+              event.message.url,
+              event.message.type,
+              page,
+              page._frames[event.message.frameId]
+            );
+            break;
+          case "proxy":
+            event.message = backgroundPageProxy.handleMessage(event.message);
+            break;
+        }
+        break;
+      case "request":
+        var page = pages[event.message.pageId];
+        var sender = {page: page, frame: page._frames[event.message.frameId]};
+        page._messageProxy.handleRequest(event.message, sender);
+        break;
+      case "response":
+        pages[event.message.pageId]._messageProxy.handleResponse(event.message);
+        break;
+      case "replaced":
+        var page = pages[event.message.pageId];
+        page._prerendered = false;
+
+        // when a prerendered page is shown, forget the previous page
+        // associated with its tab, and reset the toolbar item if necessary.
+        // Note that it wouldn't be sufficient to do that when the old
+        // page is unloading, because Safari dispatches window.onunload
+        // only when reloading the page or following links, but not when
+        // the current page is replaced with a prerendered page.
+        replacePage(page);
+        break;
     }
-  }, true);
+  });
 
 
-  /* API */
+  /* Storage */
 
-  ext.windows = {
-    getAll: function(callback)
-    {
-      callback(safari.application.browserWindows.map(function(win)
-      {
-        return new Window(win);
-      }));
-    },
-    getLastFocused: function(callback)
-    {
-      callback(new Window(safari.application.activeBrowserWindow));
-    }
-  };
-
-  ext.backgroundPage = {
-    getWindow: function()
-    {
-      return safari.extension.globalPage.contentWindow;
-    }
-  };
-
-  ext.onMessage = new BackgroundMessageEventTarget();
   ext.storage = safari.extension.settings;
-
-  var contextMenuItems = [];
-  var isContextMenuHidden = true;
-  ext.contextMenus = {
-    addMenuItem: function(title, contexts, onclick)
-    {
-      contextMenuItems.push({
-        id: String(contextMenuItems.length), 
-        title: title,
-        item: null,
-        contexts: contexts,
-        onclick: onclick
-      });
-      this.showMenuItems();
-    },
-    removeMenuItems: function()
-    {
-      contextMenuItems = [];
-      this.hideMenuItems();
-    },
-    showMenuItems: function()
-    {
-      isContextMenuHidden = false;
-    },
-    hideMenuItems: function()
-    {
-      isContextMenuHidden = true;
-    }
-  };
-
-  // Create context menu items
-  safari.application.addEventListener("contextmenu", function(event)
-  {
-    if (isContextMenuHidden)
-      return;
-
-    var context = event.userInfo.tagName;
-    if (context == "img")
-      context = "image";
-    if (!event.userInfo.srcUrl)
-      context = null;
-
-    for (var i = 0; i < contextMenuItems.length; i++)
-    {
-      // Supported contexts are: all, audio, image, video
-      var menuItem = contextMenuItems[i];
-      if (menuItem.contexts.indexOf("all") == -1 && menuItem.contexts.indexOf(context) == -1)
-        continue;
-      
-      event.contextMenu.appendContextMenuItem(menuItem.id, menuItem.title);
-    }
-  }, false);
-
-  // Handle context menu item clicks
-  safari.application.addEventListener("command", function(event)
-  {
-    for (var i = 0; i < contextMenuItems.length; i++)
-    {
-      if (contextMenuItems[i].id == event.command)
-      {
-        contextMenuItems[i].onclick(event.userInfo.srcUrl, new Tab(safari.application.activeBrowserWindow.activeTab));
-        break;
-      }
-    }
-  }, false);
 })();
Index: safari/ext/common.js
===================================================================
--- a/safari/ext/common.js
+++ b/safari/ext/common.js
@@ -15,160 +15,130 @@
  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-(function() {
-  /* Events */
+(function()
+{
+  /* Message passing */
 
-  WrappedEventTarget = function(target, eventName, capture)
+  var MessageProxy = ext._MessageProxy = function(messageDispatcher)
   {
-    this._listeners = [];
-    this._wrappedListeners = [];
+    this._messageDispatcher = messageDispatcher;
+    this._responseCallbacks = {__proto__: null};
+    this._responseCallbackCounter = 0;
+  };
+  MessageProxy.prototype = {
+    _sendResponse: function(request, message)
+    {
+      var response = {};
+      for (var prop in request)
+        response[prop] = request[prop];
+      response.payload = message;
 
-    this._target = target;
-    this._eventName = eventName;
-    this._capture = capture;
-  };
-  WrappedEventTarget.prototype = {
-    addListener: function(listener)
+      this._messageDispatcher.dispatchMessage("response", response);
+    },
+    handleRequest: function(request, sender)
     {
-      var wrappedListener = this._wrapListener(listener);
+      var sendResponse;
+      if ("callbackId" in request)
+        sendResponse = this._sendResponse.bind(this, request);
+      else
+        sendResponse = function() {};
 
-      this._listeners.push(listener);
-      this._wrappedListeners.push(wrappedListener);
+      ext.onMessage._dispatch(request.payload, sender, sendResponse);
+    },
+    handleResponse: function(response)
+    {
+      var callbackId = response.callbackId;
+      var callback = this._responseCallbacks[callbackId];
+      if (callback)
+      {
+        delete this._responseCallbacks[callbackId];
+        callback(response.payload);
+      }
+    },
+    sendMessage: function(message, responseCallback, extra)
+    {
+      var request = {payload: message};
 
-      this._target.addEventListener(
-        this._eventName,
-        wrappedListener,
-        this._capture
-      );
-    },
-    removeListener: function(listener)
-    {
-      var idx = this._listeners.indexOf(listener);
+      if (responseCallback)
+      {
+        request.callbackId = ++this._responseCallbackCounter;
+        this._responseCallbacks[request.callbackId] = responseCallback;
+      }
 
-      if (idx != -1)
-      {
-        this._target.removeEventListener(
-          this._eventName,
-          this._wrappedListeners[idx],
-          this._capture
-        );
+      for (var prop in extra)
+        request[prop] = extra[prop];
 
-        this._listeners.splice(idx, 1);
-        this._wrappedListeners.splice(idx, 1);
-      }
+      this._messageDispatcher.dispatchMessage("request", request);
     }
   };
 
-  MessageEventTarget = function(target)
-  {
-    WrappedEventTarget.call(this, target, "message", false);
-  };
-  MessageEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(event)
-      {
-        if (event.name == "request")
-          listener(event.message.payload, this._getSenderDetails(event), function(message)
-          {
-            this._getResponseDispatcher(event).dispatchMessage("response",
-            {
-              requestId: event.message.requestId,
-              payload: message
-            });
-          }.bind(this));
-      }.bind(this);
-    }
-  };
-
-
-  /* Message passing */
-
-  var requestCounter = 0;
-
-  _sendMessage = function(message, responseCallback, messageDispatcher, responseEventTarget, extra)
-  {
-    var requestId = ++requestCounter;
-
-    if (responseCallback)
-    {
-      var responseListener = function(event)
-      {
-        if (event.name == "response" && event.message.requestId == requestId)
-        {
-          responseEventTarget.removeEventListener("message", responseListener, false);
-          responseCallback(event.message.payload);
-        }
-      };
-      responseEventTarget.addEventListener("message", responseListener, false);
-    }
-
-    var rawMessage = {requestId: requestId, payload: message};
-    for (var k in extra)
-      rawMessage[k] = extra[k];
-    messageDispatcher.dispatchMessage("request", rawMessage);
-  };
+  ext.onMessage = new ext._EventTarget();
 
 
   /* I18n */
 
-  var I18n = function()
+  var localeCandidates = null;
+  var uiLocale;
+
+  var getLocaleCandidates = function()
   {
-    this._localeCandidates = this._getLocaleCandidates();
-    this._uiLocale = this._localeCandidates[0];
+    var candidates = [];
+    var defaultLocale = "en_US";
+
+    var bits, i;
+    for (i = (bits = navigator.language.split("-")).length; i > 0; i--)
+    {
+      var locale = bits.slice(0, i).join("_");
+      candidates.push(locale);
+
+      if (locale == defaultLocale)
+        return candidates;
+    }
+
+    candidates.push(defaultLocale);
+    return candidates;
   };
-  I18n.prototype = {
-    _getLocaleCandidates: function()
+
+  var getCatalog = function(locale)
+  {
+    var xhr = new XMLHttpRequest();
+
+    xhr.open("GET", safari.extension.baseURI + "_locales/" + locale + "/messages.json", false);
+
+    try {
+      xhr.send();
+    }
+    catch (e)
     {
-      var candidates = [];
-      var defaultLocale = "en_US";
+      return null;
+    }
 
-      var bits, i;
-      for (i = (bits = navigator.language.split("-")).length; i > 0; i--)
+    if (xhr.status != 200 && xhr.status != 0)
+      return null;
+
+    return JSON.parse(xhr.responseText);
+  };
+
+  ext.i18n = {
+    getMessage: function(msgId, substitutions)
+    {
+      if (!localeCandidates)
       {
-        var locale = bits.slice(0, i).join("_");
-        candidates.push(locale);
-
-        if (locale == defaultLocale)
-          return candidates;
+        localeCandidates = getLocaleCandidates();
+        uiLocale = localeCandidates[0];
       }
 
-      candidates.push(defaultLocale);
-      return candidates;
-    },
-    _getCatalog: function(locale)
-    {
-      var xhr = new XMLHttpRequest();
-      
-      xhr.open("GET", safari.extension.baseURI + "_locales/" + locale + "/messages.json", false);
+      if (msgId == "@@ui_locale")
+        return uiLocale;
 
-      try {
-        xhr.send();
-      }
-      catch (e)
+      for (var i = 0; i < localeCandidates.length; i++)
       {
-        return null;
-      }
-
-      if (xhr.status != 200 && xhr.status != 0)
-        return null;
-
-      return JSON.parse(xhr.responseText);
-    },
-    getMessage: function(msgId, substitutions)
-    {
-      if (msgId == "@@ui_locale")
-        return this._uiLocale;
-
-      for (var i = 0; i < this._localeCandidates.length; i++)
-      {
-        var catalog = this._getCatalog(this._localeCandidates[i]);
+        var catalog = getCatalog(localeCandidates[i]);
         if (!catalog)
         {
           // if there is no catalog for this locale
           // candidate, don't try to load it again
-          this._localeCandidates.splice(i--, 1);
+          localeCandidates.splice(i--, 1);
           continue;
         }
 
@@ -193,7 +163,7 @@
             continue;
 
           var placeholderValue;
-          if (Object.prototype.toString.call(substitutions) == "[object Array]")
+          if (typeof substitutions != "string")
             placeholderValue = substitutions[placeholderIdx - 1];
           else if (placeholderIdx == 1)
             placeholderValue = substitutions;
@@ -209,13 +179,10 @@
   };
 
 
-  /* API */
+  /* Utils */
 
-  ext = {
-    getURL: function(path)
-    {
-      return safari.extension.baseURI + path;
-    },
-    i18n: new I18n()
+  ext.getURL = function(path)
+  {
+    return safari.extension.baseURI + path;
   };
 })();
Index: safari/ext/content.js
===================================================================
--- a/safari/ext/content.js
+++ b/safari/ext/content.js
@@ -22,39 +22,125 @@
   if (!("safari" in window))
     window.safari = window.parent.safari;
 
-  if (window == window.top)
-    safari.self.tab.dispatchMessage("loading");
 
+  /* Intialization */
 
-  /* Events */
+  var beforeLoadEvent = document.createEvent("Event");
+  beforeLoadEvent.initEvent("beforeload");
 
-  var ContentMessageEventTarget = function()
+  var isTopLevel = window == window.top;
+  var isPrerendered = document.visibilityState == "prerender";
+
+  var documentInfo = safari.self.tab.canLoad(
+    beforeLoadEvent,
+    {
+      category: "loading",
+      url: document.location.href,
+      referrer: document.referrer,
+      isTopLevel: isTopLevel,
+      isPrerendered: isPrerendered
+    }
+  );
+
+  if (isTopLevel && isPrerendered)
   {
-    MessageEventTarget.call(this, safari.self);
-  };
-  ContentMessageEventTarget.prototype = {
-    __proto__: MessageEventTarget.prototype,
-    _getResponseDispatcher: function(event)
+    var onVisibilitychange = function()
     {
-      return event.target.tab;
-    },
-    _getSenderDetails: function(event)
+      safari.self.tab.dispatchMessage("replaced", {pageId: documentInfo.pageId});
+      document.removeEventListener("visibilitychange", onVisibilitychange);
+    };
+    document.addEventListener("visibilitychange", onVisibilitychange);
+  }
+
+
+  /* Web requests */
+
+  document.addEventListener("beforeload", function(event)
+  {
+    // we don't block non-HTTP requests anyway, so we can bail out
+    // without asking the background page. This is even necessary
+    // because passing large data (like a photo encoded as data: URL)
+    // to the background page, freezes Safari.
+    if (!/^https?:/.test(event.url))
+      return;
+
+    var type;
+    switch(event.target.localName)
     {
-      return {};
+      case "frame":
+      case "iframe":
+        type = "sub_frame";
+        break;
+      case "img":
+        type = "image";
+        break;
+      case "object":
+      case "embed":
+        type = "object";
+        break;
+      case "script":
+        type = "script";
+        break;
+      case "link":
+        if (/\bstylesheet\b/i.test(event.target.rel))
+        {
+          type = "stylesheet";
+          break;
+        }
+      default:
+        type = "other";
     }
-  };
 
+    if (!safari.self.tab.canLoad(
+      event, {
+        category: "webRequest",
+        url: event.url,
+        type: type,
+        pageId: documentInfo.pageId,
+        frameId: documentInfo.frameId
+      }
+    ))
+    {
+      event.preventDefault();
 
-  /* Background page proxy */
-  var proxy = {
+      // Safari doesn't dispatch an "error" event when preventing an element
+      // from loading by cancelling the "beforeload" event. So we have to
+      // dispatch it manually. Otherwise element collapsing wouldn't work.
+      if (type != "sub_frame")
+      {
+        var evt = document.createEvent("Event");
+        evt.initEvent("error");
+        event.target.dispatchEvent(evt);
+      }
+    }
+  }, true);
+
+
+  /* Context menus */
+
+  document.addEventListener("contextmenu", function(event)
+  {
+    var element = event.srcElement;
+    safari.self.tab.setContextMenuEventUserInfo(event, {
+      pageId: documentInfo.pageId,
+      srcUrl: ("src" in element) ? element.src : null,
+      tagName: element.localName
+    });
+  });
+
+
+  /* Background page */
+
+  var backgroundPageProxy = {
     objects: [],
     callbacks: [],
 
     send: function(message)
     {
-      var evt = document.createEvent("Event");
-      evt.initEvent("beforeload");
-      return safari.self.tab.canLoad(evt, {type: "proxy", payload: message});
+      message.category = "proxy";
+      message.pageId = documentInfo.pageId;
+
+      return safari.self.tab.canLoad(beforeLoadEvent, message);
     },
     checkResult: function(result)
     {
@@ -75,23 +161,10 @@
       if (typeof obj == "function")
       {
         var callbackId = this.callbacks.indexOf(obj);
-
         if (callbackId == -1)
-        {
           callbackId = this.callbacks.push(obj) - 1;
 
-          safari.self.addEventListener("message", function(event)
-          {
-            if (event.name == "proxyCallback")
-            if (event.message.callbackId == callbackId)
-              obj.apply(
-                this.getObject(event.message.contextId),
-                this.deserializeSequence(event.message.args)
-              );
-          }.bind(this));
-        }
-
-        return {type: "callback", callbackId: callbackId};
+        return {type: "callback", callbackId: callbackId, frameId: documentInfo.frameId};
       }
 
       if (typeof obj == "object" &&
@@ -226,7 +299,15 @@
         );
       };
     },
-    getObject: function(objectId) {
+    handleCallback: function(message)
+    {
+      this.callbacks[message.callbackId].apply(
+        this.getObject(message.contextId),
+        this.deserializeSequence(message.args)
+      );
+    },
+    getObject: function(objectId)
+    {
       var objectInfo = this.send({
         type: "inspectObject",
         objectId: objectId
@@ -281,104 +362,44 @@
     }
   };
 
-
-  /* Web request blocking */
-
-  document.addEventListener("beforeload", function(event)
-  {
-    // we don't block non-HTTP requests anyway, so we can bail out
-    // without asking the background page. This is even necessary
-    // because passing large data (like a photo encoded as data: URL)
-    // to the background page, freezes Safari.
-    if (!/^https?:/.test(event.url))
-      return;
-
-    var type;
-
-    switch(event.target.localName)
-    {
-      case "frame":
-      case "iframe":
-        type = "sub_frame";
-        break;
-      case "img":
-        type = "image";
-        break;
-      case "object":
-      case "embed":
-        type = "object";
-        break;
-      case "script":
-        type = "script";
-        break;
-      case "link":
-        if (/\bstylesheet\b/i.test(event.target.rel))
-        {
-          type = "stylesheet";
-          break;
-        }
-      default:
-        type = "other";
-    }
-
-    if (!safari.self.tab.canLoad(
-      event, {
-        type: "webRequest",
-        payload: {
-          url: event.url,
-          type: type,
-          documentUrl: document.location.href,
-          isTopLevel: window == window.top
-        }
-      }
-    ))
-    {
-      event.preventDefault();
-
-      // Safari doesn't dispatch an "error" event when preventing an element
-      // from loading by cancelling the "beforeload" event. So we have to
-      // dispatch it manually. Otherwise element collapsing wouldn't work.
-      if (type != "sub_frame")
-      {
-        var evt = document.createEvent("Event");
-        evt.initEvent("error");
-        event.target.dispatchEvent(evt);
-      }
-    }
-  }, true);
-
-
-  /* API */
-
   ext.backgroundPage = {
     sendMessage: function(message, responseCallback)
     {
-      _sendMessage(
-        message, responseCallback,
-        safari.self.tab, safari.self,
-        {
-          documentUrl: document.location.href,
-          isTopLevel: window == window.top
-        }
-      );
+      messageProxy.sendMessage(message, responseCallback, documentInfo);
     },
     getWindow: function()
     {
-      return proxy.getObject(0);
+      return backgroundPageProxy.getObject(0);
     }
   };
 
-  ext.onMessage = new ContentMessageEventTarget();
 
+  /* Message processing */
 
-  // Safari does not pass the element which the context menu is refering to
-  // so we need to add it to the event's user info.
-  document.addEventListener("contextmenu", function(event)
+  var messageProxy = new ext._MessageProxy(safari.self.tab);
+
+  safari.self.addEventListener("message", function(event)
   {
-    var element = event.srcElement;
-    safari.self.tab.setContextMenuEventUserInfo(event, {
-      srcUrl: ("src" in element) ? element.src : null,
-      tagName: element.localName
-    });
-  }, false);
+    if (event.message.pageId == documentInfo.pageId)
+    {
+      if (event.name == "request")
+      {
+        messageProxy.handleRequest(event.message, {});
+        return;
+      }
+
+      if (event.message.frameId == documentInfo.frameId)
+      {
+        switch (event.name)
+        {
+          case "response":
+            messageProxy.handleResponse(event.message);
+            break;
+          case "proxyCallback":
+            backgroundPageProxy.handleCallback(event.message);
+            break;
+        }
+      }
+    }
+  });
 })();
Index: safari/ext/popup.js
===================================================================
--- a/safari/ext/popup.js
+++ b/safari/ext/popup.js
@@ -63,14 +63,12 @@
   // import ext into the javascript context of the popover. This code might fail,
   // when the background page isn't ready yet. So it is important to put it below
   // the reloading code above.
-  var backgroundPage = safari.extension.globalPage.contentWindow;
+  window.ext = {
+    __proto__: safari.extension.globalPage.contentWindow.ext,
 
-  window.ext = {
-    __proto__: backgroundPage.ext,
     closePopup: function()
     {
       safari.self.hide();
     }
   };
-  window.TabMap = backgroundPage.TabMap;
 })();
Index: stats.js
===================================================================
--- a/stats.js
+++ b/stats.js
@@ -23,7 +23,7 @@
   var FilterNotifier = require("filterNotifier").FilterNotifier;
   var Prefs = require("prefs").Prefs;
   
-  var currentTab;
+  var currentPage;
   var shareURL = "https://adblockplus.org/";
   
   var messageMark = {};
@@ -76,17 +76,14 @@
     document.querySelector("label[for='show-iconnumber']").addEventListener("click", toggleIconNumber, false);
     
     // Update stats
-    ext.windows.getLastFocused(function(win)
+    ext.pages.query({active: true, lastFocusedWindow: true}, function(pages)
     {
-      win.getActiveTab(function(tab)
-      {
-        currentTab = tab;
-        updateStats();
+      currentPage = pages[0];
+      updateStats();
 
-        FilterNotifier.addListener(onNotify);
+      FilterNotifier.addListener(onNotify);
 
-        document.getElementById("stats-container").removeAttribute("hidden");
-      });
+      document.getElementById("stats-container").removeAttribute("hidden");
     });
   }
   
@@ -104,7 +101,7 @@
   function updateStats()
   {
     var statsPage = document.getElementById("stats-page");
-    var blockedPage = getStats("blocked", currentTab).toLocaleString();
+    var blockedPage = getStats("blocked", currentPage).toLocaleString();
     i18n.setElementText(statsPage, "stats_label_page", [blockedPage]);
     
     var statsTotal = document.getElementById("stats-total");
@@ -121,8 +118,7 @@
     else
       blocked = i18n.getMessage("stats_over", (9000).toLocaleString());
     
-    var url = createShareLink(ev.target.dataset.social, blocked);
-    ext.windows.getLastFocused(function(win) { win.openTab(url); });
+    ext.pages.open(createShareLink(ev.target.dataset.social, blocked));
   }
   
   function toggleIconNumber()
Index: webrequest.js
===================================================================
--- a/webrequest.js
+++ b/webrequest.js
@@ -47,9 +47,9 @@
   }
 });
 
-function onBeforeRequest(url, type, tab, frame)
+function onBeforeRequest(url, type, page, frame)
 {
-  if (isFrameWhitelisted(tab, frame))
+  if (isFrameWhitelisted(page, frame))
     return true;
 
   var docDomain = extractHostFromURL(frame.url);
@@ -69,7 +69,7 @@
       showNotification(notificationToShow);
   }
 
-  FilterNotifier.triggerListeners("filter.hitCount", filter, 0, 0, tab);
+  FilterNotifier.triggerListeners("filter.hitCount", filter, 0, 0, page);
   return !(filter instanceof BlockingFilter);
 }
 
@@ -85,8 +85,8 @@
     if (details.type != "main_frame" && details.type != "sub_frame")
       return;
 
-    var tab = new Tab({id: details.tabId});
-    var frame = new Frame({id: details.frameId, tab: tab});
+    var page = new ext.Page({id: details.tabId});
+    var frame = new ext.Frame({frameId: details.frameId, tabId: details.tabId});
 
     if (frame.url != details.url)
       return;
@@ -95,7 +95,7 @@
     {
       var header = details.responseHeaders[i];
       if (header.name.toLowerCase() == "x-adblock-key" && header.value)
-        processKeyException(header.value, tab, frame);
+        processKeyException(header.value, page, frame);
     }
 
     var notificationToShow = Notification.getNextToShow(details.url);
