diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -10,7 +10,7 @@
     'conpherence.pkg.css' => '0e3cf785',
     'conpherence.pkg.js' => '020aebcf',
     'core.pkg.css' => '970b3ceb',
-    'core.pkg.js' => 'adc34883',
+    'core.pkg.js' => '2fe70e3d',
     'dark-console.pkg.js' => '187792c2',
     'differential.pkg.css' => '5c459f92',
     'differential.pkg.js' => '5080baf4',
@@ -460,7 +460,8 @@
     'rsrc/js/core/DraggableList.js' => '0169e425',
     'rsrc/js/core/Favicon.js' => '7930776a',
     'rsrc/js/core/FileUpload.js' => 'ab85e184',
-    'rsrc/js/core/Hovercard.js' => '074f0783',
+    'rsrc/js/core/Hovercard.js' => 'd9d29a5f',
+    'rsrc/js/core/HovercardList.js' => '10a5f4bf',
     'rsrc/js/core/KeyboardShortcut.js' => '1a844c06',
     'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48',
     'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
@@ -485,7 +486,7 @@
     'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a',
     'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b',
     'rsrc/js/core/behavior-history-install.js' => '6a1583a8',
-    'rsrc/js/core/behavior-hovercard.js' => '6c379000',
+    'rsrc/js/core/behavior-hovercard.js' => '3f446c72',
     'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731',
     'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b',
     'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf',
@@ -670,7 +671,7 @@
     'javelin-behavior-pholio-mock-view' => '5aa1544e',
     'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
     'javelin-behavior-phui-file-upload' => 'e150bd50',
-    'javelin-behavior-phui-hovercards' => '6c379000',
+    'javelin-behavior-phui-hovercards' => '3f446c72',
     'javelin-behavior-phui-selectable-list' => 'b26a41e4',
     'javelin-behavior-phui-submenu' => 'b5e9bff9',
     'javelin-behavior-phui-tab-group' => '242aa08b',
@@ -858,7 +859,8 @@
     'phui-formation-view-css' => 'd2dec8ed',
     'phui-head-thing-view-css' => 'd7f293df',
     'phui-header-view-css' => '36c86a58',
-    'phui-hovercard' => '074f0783',
+    'phui-hovercard' => 'd9d29a5f',
+    'phui-hovercard-list' => '10a5f4bf',
     'phui-hovercard-view-css' => '6ca90fa0',
     'phui-icon-set-selector-css' => '7aa5f3ec',
     'phui-icon-view-css' => '4cbc684a',
@@ -986,13 +988,6 @@
       'javelin-uri',
       'phabricator-notification',
     ),
-    '074f0783' => array(
-      'javelin-install',
-      'javelin-dom',
-      'javelin-vector',
-      'javelin-request',
-      'javelin-uri',
-    ),
     '0889b835' => array(
       'javelin-install',
       'javelin-event',
@@ -1030,6 +1025,14 @@
       'javelin-workflow',
       'phuix-icon-view',
     ),
+    '10a5f4bf' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-vector',
+      'javelin-request',
+      'javelin-uri',
+      'phui-hovercard',
+    ),
     '111bfd2d' => array(
       'javelin-install',
     ),
@@ -1266,6 +1269,14 @@
       'phabricator-drag-and-drop-file-upload',
       'phabricator-draggable-list',
     ),
+    '3f446c72' => array(
+      'javelin-behavior',
+      'javelin-behavior-device',
+      'javelin-stratcom',
+      'javelin-vector',
+      'phui-hovercard',
+      'phui-hovercard-list',
+    ),
     '407ee861' => array(
       'javelin-behavior',
       'javelin-uri',
@@ -1557,13 +1568,6 @@
       'javelin-workflow',
       'javelin-magical-init',
     ),
-    '6c379000' => array(
-      'javelin-behavior',
-      'javelin-behavior-device',
-      'javelin-stratcom',
-      'javelin-vector',
-      'phui-hovercard',
-    ),
     '6cfa0008' => array(
       'javelin-dom',
       'javelin-dynval',
@@ -2132,6 +2136,13 @@
       'javelin-util',
       'phabricator-shaped-request',
     ),
+    'd9d29a5f' => array(
+      'javelin-install',
+      'javelin-dom',
+      'javelin-vector',
+      'javelin-request',
+      'javelin-uri',
+    ),
     'da15d3dc' => array(
       'phui-oi-list-view-css',
     ),
@@ -2367,6 +2378,7 @@
       'javelin-behavior-global-drag-and-drop',
       'javelin-behavior-phabricator-reveal-content',
       'phui-hovercard',
+      'phui-hovercard-list',
       'javelin-behavior-phui-hovercards',
       'javelin-color',
       'javelin-fx',
diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php
--- a/resources/celerity/packages.php
+++ b/resources/celerity/packages.php
@@ -60,6 +60,7 @@
     'javelin-behavior-global-drag-and-drop',
     'javelin-behavior-phabricator-reveal-content',
     'phui-hovercard',
+    'phui-hovercard-list',
     'javelin-behavior-phui-hovercards',
     'javelin-color',
     'javelin-fx',
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -224,6 +224,43 @@
   }
 
 
+  /**
+   * @task data
+   */
+  public function getJSONMap($name, $default = array()) {
+    if (!isset($this->requestData[$name])) {
+      return $default;
+    }
+
+    $raw_data = phutil_string_cast($this->requestData[$name]);
+    $raw_data = trim($raw_data);
+    if (!strlen($raw_data)) {
+      return $default;
+    }
+
+    if ($raw_data[0] !== '{') {
+      throw new Exception(
+        pht(
+          'Request parameter "%s" is not formatted properly. Expected a '.
+          'JSON object, but value does not start with "{".',
+          $name));
+    }
+
+    try {
+      $json_object = phutil_json_decode($raw_data);
+    } catch (PhutilJSONParserException $ex) {
+      throw new Exception(
+        pht(
+          'Request parameter "%s" is not formatted properly. Expected a '.
+          'JSON object, but encountered a syntax error: %s.',
+          $name,
+          $ex->getMessage()));
+    }
+
+    return $json_object;
+  }
+
+
   /**
    * @task data
    */
diff --git a/src/applications/search/controller/PhabricatorSearchHovercardController.php b/src/applications/search/controller/PhabricatorSearchHovercardController.php
--- a/src/applications/search/controller/PhabricatorSearchHovercardController.php
+++ b/src/applications/search/controller/PhabricatorSearchHovercardController.php
@@ -9,7 +9,8 @@
 
   public function handleRequest(AphrontRequest $request) {
     $viewer = $this->getViewer();
-    $phids = $request->getArr('phids');
+
+    $cards = $request->getJSONMap('cards');
 
     // If object names are provided, look them up and pretend they were
     // passed as additional PHIDs. This is primarily useful for debugging,
@@ -23,18 +24,29 @@
         ->execute();
 
       foreach ($named_objects as $object) {
-        $phids[] = $object->getPHID();
+        $cards[] = array(
+          'objectPHID' => $object->getPHID(),
+        );
       }
     }
 
+    $object_phids = array();
+    $handle_phids = array();
+    foreach ($cards as $card) {
+      $object_phid = idx($card, 'objectPHID');
+
+      $handle_phids[] = $object_phid;
+      $object_phids[] = $object_phid;
+    }
+
     $handles = id(new PhabricatorHandleQuery())
       ->setViewer($viewer)
-      ->withPHIDs($phids)
+      ->withPHIDs($handle_phids)
       ->execute();
 
     $objects = id(new PhabricatorObjectQuery())
       ->setViewer($viewer)
-      ->withPHIDs($phids)
+      ->withPHIDs($object_phids)
       ->execute();
     $objects = mpull($objects, null, 'getPHID');
 
@@ -67,10 +79,12 @@
         array_select_keys($objects, $extension_phids));
     }
 
-    $cards = array();
-    foreach ($phids as $phid) {
-      $handle = $handles[$phid];
-      $object = idx($objects, $phid);
+    $results = array();
+    foreach ($cards as $card_key => $card) {
+      $object_phid = $card['objectPHID'];
+
+      $handle = $handles[$object_phid];
+      $object = idx($objects, $object_phid);
 
       $hovercard = id(new PHUIHovercardView())
         ->setUser($viewer)
@@ -90,18 +104,18 @@
         }
       }
 
-      $cards[$phid] = $hovercard;
+      $results[$card_key] = $hovercard;
     }
 
     if ($request->isAjax()) {
       return id(new AphrontAjaxResponse())->setContent(
         array(
-          'cards' => $cards,
+          'cards' => $results,
         ));
     }
 
-    foreach ($cards as $key => $hovercard) {
-      $cards[$key] = phutil_tag('div',
+    foreach ($results as $key => $hovercard) {
+      $results[$key] = phutil_tag('div',
         array(
           'class' => 'ml',
         ),
@@ -109,7 +123,7 @@
     }
 
     return $this->newPage()
-      ->appendChild($cards)
+      ->appendChild($results)
       ->setShowFooter(false);
   }
 
diff --git a/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php
--- a/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php
+++ b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php
@@ -42,7 +42,7 @@
     $submenu[] = id(new PhabricatorActionView())
       ->setIcon('fa-address-card-o')
       ->setName(pht('View Hovercard'))
-      ->setHref(urisprintf('/search/hovercard/?phids[]=%s', $phid));
+      ->setHref(urisprintf('/search/hovercard/?names=%s', $phid));
 
     $developer_action = id(new PhabricatorActionView())
       ->setName(pht('Advanced/Developer...'))
diff --git a/webroot/rsrc/js/core/Hovercard.js b/webroot/rsrc/js/core/Hovercard.js
--- a/webroot/rsrc/js/core/Hovercard.js
+++ b/webroot/rsrc/js/core/Hovercard.js
@@ -10,163 +10,18 @@
 
 JX.install('Hovercard', {
 
-  statics : {
-    _node : null,
-    _activeRoot : null,
-    _visiblePHID : null,
-    _alignment: null,
-
-    fetchUrl : '/search/hovercard/',
-
-    /**
-     * Hovercard storage. {"PHID-XXXX-YYYY":"<...>", ...}
-     */
-    _cards : {},
-
-    getAnchor : function() {
-      return this._activeRoot;
-    },
-
-    getCard : function() {
-      var self = JX.Hovercard;
-      return self._node;
-    },
-
-    getAlignment: function() {
-      var self = JX.Hovercard;
-      return self._alignment;
-    },
-
-    show : function(root, phid) {
-      var self = JX.Hovercard;
-
-      if (root === this._activeRoot) {
-        return;
-      }
-
-      self.hide();
-
-      self._visiblePHID = phid;
-      self._activeRoot = root;
-
-      if (!(phid in self._cards)) {
-        self._load([phid]);
-      } else {
-        self._drawCard(phid);
-      }
-    },
-
-    _drawCard : function(phid) {
-      var self = JX.Hovercard;
-      // card is loading...
-      if (self._cards[phid] === true) {
-        return;
-      }
-      // Not the current requested card
-      if (phid != self._visiblePHID) {
-        return;
-      }
-      // Not loaded
-      if (!(phid in self._cards)) {
-        return;
-      }
-
-      var root = self._activeRoot;
-      var node = JX.$N('div',
-        { className: 'jx-hovercard-container' },
-        JX.$H(self._cards[phid]));
-
-      self._node = node;
-
-      // Append the card to the document, but offscreen, so we can measure it.
-      node.style.left = '-10000px';
-      document.body.appendChild(node);
-
-      // Retrieve size from child (wrapper), since node gives wrong dimensions?
-      var child = node.firstChild;
-      var p = JX.$V(root);
-      var d = JX.Vector.getDim(root);
-      var n = JX.Vector.getDim(child);
-      var v = JX.Vector.getViewport();
-      var s = JX.Vector.getScroll();
-
-      // Move the tip so it's nicely aligned.
-      var margin = 20;
-
-
-      // Try to align the card directly above the link, with left borders
-      // touching.
-      var x = p.x;
-
-      // If this would push us off the right side of the viewport, push things
-      // back to the left.
-      if ((x + n.x + margin) > (s.x + v.x)) {
-        x = (s.x + v.x) - n.x - margin;
-      }
-
-      // Try to put the card above the link.
-      var y = p.y - n.y - margin;
-      self._alignment = 'north';
-
-      // If the card is near the top of the window, show it beneath the
-      // link we're hovering over instead.
-      if ((y - margin) < s.y) {
-        y = p.y + d.y + margin;
-        self._alignment = 'south';
-      }
-
-      node.style.left = x + 'px';
-      node.style.top  = y + 'px';
-    },
-
-    hide : function() {
-      var self = JX.Hovercard;
-      self._visiblePHID = null;
-      self._activeRoot = null;
-      if (self._node) {
-        JX.DOM.remove(self._node);
-        self._node = null;
-      }
-    },
-
-    /**
-     * Pass it an array of phids to load them into storage
-     *
-     * @param list phids
-     */
-    _load : function(phids) {
-      var self = JX.Hovercard;
-      var uri = JX.$U(self.fetchUrl);
-
-      var send = false;
-      for (var ii = 0; ii < phids.length; ii++) {
-        var phid = phids[ii];
-        if (phid in self._cards) {
-          continue;
-        }
-        self._cards[phid] = true; // means "loading"
-        uri.setQueryParam('phids['+ii+']', phids[ii]);
-        send = true;
-      }
-
-      if (!send) {
-        // already loaded / loading everything!
-        return;
-      }
-
-      new JX.Request(uri, function(r) {
-        for (var phid in r.cards) {
-          self._cards[phid] = r.cards[phid];
-
-          // Don't draw if the user is faster than the browser
-          // Only draw if the user is still requesting the original card
-          if (self.getCard() && phid != self._visiblePHID) {
-            continue;
-          }
-
-          self._drawCard(phid);
-        }
-      }).send();
+  properties: {
+    hovercardKey: null,
+    objectPHID: null,
+    isLoading: false,
+    isLoaded: false,
+    content: null
+  },
+
+  members: {
+    newContentNode: function() {
+      return JX.$H(this.getContent());
     }
   }
+
 });
diff --git a/webroot/rsrc/js/core/HovercardList.js b/webroot/rsrc/js/core/HovercardList.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/core/HovercardList.js
@@ -0,0 +1,226 @@
+/**
+ * @requires javelin-install
+ *           javelin-dom
+ *           javelin-vector
+ *           javelin-request
+ *           javelin-uri
+ *           phui-hovercard
+ * @provides phui-hovercard-list
+ * @javelin
+ */
+
+JX.install('HovercardList', {
+
+  construct: function() {
+    this._cards = {};
+    this._drawRequest = {};
+  },
+
+  members: {
+    _cardNode: null,
+    _rootNode: null,
+    _cards: null,
+    _drawRequest: null,
+    _visibleCard: null,
+
+    _fetchURI : '/search/hovercard/',
+
+    getCard: function(spec) {
+      var hovercard_key = this._newHovercardKey(spec);
+
+      if (!(hovercard_key in this._cards)) {
+        var card = new JX.Hovercard()
+          .setHovercardKey(hovercard_key)
+          .setObjectPHID(spec.hoverPHID);
+
+        this._cards[hovercard_key] = card;
+      }
+
+      return this._cards[hovercard_key];
+    },
+
+    drawCard: function(card, node) {
+      this._drawRequest = {
+        card: card,
+        node: node
+      };
+
+      if (card.getIsLoaded()) {
+        return this._paintCard(card);
+      }
+
+      if (card.getIsLoading()) {
+        return;
+      }
+
+      var hovercard_key = card.getHovercardKey();
+
+      var request = {};
+      request[hovercard_key] = this._newCardRequest(card);
+      request = JX.JSON.stringify(request);
+
+      var uri = JX.$U(this._fetchURI)
+        .setQueryParam('cards', request);
+
+      var onresponse = JX.bind(this, function(r) {
+        var card = this._cards[hovercard_key];
+
+        this._fillCard(card, r.cards[hovercard_key]);
+        this._paintCard(card);
+      });
+
+      card.setIsLoading(true);
+
+      new JX.Request(uri, onresponse)
+        .send();
+    },
+
+    _newHovercardKey: function(spec) {
+      return 'phid=' + spec.hoverPHID;
+    },
+
+    _newCardRequest: function(card) {
+      return {
+        objectPHID: card.getObjectPHID()
+      };
+    },
+
+    _getCardNode: function() {
+      if (!this._cardNode) {
+        var attributes = {
+          className: 'jx-hovercard-container'
+        };
+
+        this._cardNode = JX.$N('div', attributes);
+      }
+
+      return this._cardNode;
+    },
+
+    _fillCard: function(card, response) {
+      card.setContent(response);
+      card.setIsLoaded(true);
+    },
+
+    _paintCard: function(card) {
+      var request = this._drawRequest;
+
+      if (request.card !== card) {
+        // This paint request is no longer the most recent paint request.
+        return;
+      }
+
+      this.hideCard();
+
+      this._rootNode = request.node;
+      var root = this._rootNode;
+      var node = this._getCardNode();
+
+      JX.DOM.setContent(node, card.newContentNode());
+
+      // Append the card to the document, but offscreen, so we can measure it.
+      node.style.left = '-10000px';
+      document.body.appendChild(node);
+
+      // Retrieve size from child (wrapper), since node gives wrong dimensions?
+      var child = node.firstChild;
+
+      var p = JX.$V(root);
+      var d = JX.Vector.getDim(root);
+      var n = JX.Vector.getDim(child);
+      var v = JX.Vector.getViewport();
+      var s = JX.Vector.getScroll();
+
+      // Move the tip so it's nicely aligned.
+      var margin = 20;
+
+      // Try to align the card directly above the link, with left borders
+      // touching.
+      var x = p.x;
+
+      // If this would push us off the right side of the viewport, push things
+      // back to the left.
+      if ((x + n.x + margin) > (s.x + v.x)) {
+        x = (s.x + v.x) - n.x - margin;
+      }
+
+      // Try to put the card above the link.
+      var y = p.y - n.y - margin;
+
+      var alignment = 'north';
+
+      // If the card is near the top of the window, show it beneath the
+      // link we're hovering over instead.
+      if ((y - margin) < s.y) {
+        y = p.y + d.y + margin;
+        alignment = 'south';
+      }
+
+      this._alignment = alignment;
+      node.style.left = x + 'px';
+      node.style.top  = y + 'px';
+
+      this._visibleCard = card;
+    },
+
+    hideCard: function() {
+      var node = this._getCardNode();
+      JX.DOM.remove(node);
+
+      this._rootNode = null;
+      this._alignment = null;
+      this._visibleCard = null;
+    },
+
+    onMouseMove: function(e) {
+      if (!this._visibleCard) {
+        return;
+      }
+
+      var root = this._rootNode;
+      var node = this._getCardNode();
+      var alignment = this._alignment;
+
+      var mouse = JX.$V(e);
+      var node_pos = JX.$V(node);
+      var node_dim = JX.Vector.getDim(node);
+      var root_pos = JX.$V(root);
+      var root_dim = JX.Vector.getDim(root);
+
+      var margin = 20;
+
+      if (alignment === 'south') {
+        // Cursor is below the node.
+        if (mouse.y > node_pos.y + node_dim.y + margin) {
+          this.hideCard();
+        }
+
+        // Cursor is above the root.
+        if (mouse.y < root_pos.y - margin) {
+          this.hideCard();
+        }
+      } else {
+        // Cursor is above the node.
+        if (mouse.y < node_pos.y - margin) {
+          this.hideCard();
+        }
+
+        // Cursor is below the root.
+        if (mouse.y > root_pos.y + root_dim.y + margin) {
+          this.hideCard();
+        }
+      }
+
+      // Cursor is too far to the left.
+      if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) {
+        this.hideCard();
+      }
+
+       // Cursor is too far to the right.
+      if (mouse.x >
+        Math.max(root_pos.x + root_dim.x, node_pos.x + node_dim.x) + margin) {
+        this.hideCard();
+      }
+    }
+  }
+});
diff --git a/webroot/rsrc/js/core/behavior-hovercard.js b/webroot/rsrc/js/core/behavior-hovercard.js
--- a/webroot/rsrc/js/core/behavior-hovercard.js
+++ b/webroot/rsrc/js/core/behavior-hovercard.js
@@ -5,10 +5,18 @@
  *           javelin-stratcom
  *           javelin-vector
  *           phui-hovercard
+ *           phui-hovercard-list
  * @javelin
  */
 
-JX.behavior('phui-hovercards', function() {
+JX.behavior('phui-hovercards', function(config, statics) {
+  if (statics.hovercardList) {
+    return;
+  }
+
+  var cards = new JX.HovercardList();
+  statics.hovercardList = cards;
+
 
   // We listen for mousemove instead of mouseover to handle the case when user
   // scrolls with keyboard. We don't want to display hovercard if node gets
@@ -23,65 +31,19 @@
         return;
       }
 
+      var node = e.getNode('hovercard');
       var data = e.getNodeData('hovercard');
 
-      JX.Hovercard.show(
-        e.getNode('hovercard'),
-        data.hoverPHID);
+      var card = cards.getCard(data);
+
+      cards.drawCard(card, node);
     });
 
   JX.Stratcom.listen(
     'mousemove',
     null,
     function (e) {
-      if (!JX.Hovercard.getCard()) {
-        return;
-      }
-
-      var root = JX.Hovercard.getAnchor();
-      var node = JX.Hovercard.getCard();
-      var align = JX.Hovercard.getAlignment();
-
-      var mouse = JX.$V(e);
-      var node_pos = JX.$V(node);
-      var node_dim = JX.Vector.getDim(node);
-      var root_pos = JX.$V(root);
-      var root_dim = JX.Vector.getDim(root);
-
-      var margin = 20;
-
-      if (align == 'south') {
-        // Cursor is below the node.
-        if (mouse.y > node_pos.y + node_dim.y + margin) {
-          JX.Hovercard.hide();
-        }
-
-        // Cursor is above the root.
-        if (mouse.y < root_pos.y - margin) {
-          JX.Hovercard.hide();
-        }
-      } else {
-        // Cursor is above the node.
-        if (mouse.y < node_pos.y - margin) {
-          JX.Hovercard.hide();
-        }
-
-        // Cursor is below the root.
-        if (mouse.y > root_pos.y + root_dim.y + margin) {
-          JX.Hovercard.hide();
-        }
-      }
-
-      // Cursor is too far to the left.
-      if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) {
-        JX.Hovercard.hide();
-      }
-
-       // Cursor is too far to the right.
-      if (mouse.x >
-        Math.max(root_pos.x + root_dim.x, node_pos.x + node_dim.x) + margin) {
-        JX.Hovercard.hide();
-      }
+      cards.onMouseMove(e);
     });
 
   // When we leave the page, hide any visible hovercards. If we don't do this,
@@ -91,7 +53,7 @@
     ['unload', 'onresize'],
     null,
     function() {
-      JX.Hovercard.hide();
+      cards.hideCard();
     });
 
 });