Large, multi-part selector versus traversal for identifying first matching child

JavaScript performance comparison

Test case created by Philip Rose

Info

Testing the performance of a large jQuery selector against element traversal at identifying the first candidate element based on a set of conditions (that, in this case, determine whether an element is focusable).

The simple search uses a block of HTML in which a candidate element is present quite near the start of the search.

The complex search uses the same block of HTML, albeit modified slightly such that the first candidate is far deeper in the HTML.

Preparation code

    <div id="simple-root">
        <div id="uni-header-container" class="zindex-t3">
  <div id="uni-header-background"></div><div></div>
  <div id="uni-header" class="yj-clearfix light-bg" data-light-bg="true">
    <div id="uni-header-left">
      <a href="/microsoft.com/" id="uni-logo"><img alt="Yammer" src="https://c64.assets-yammer.com/images/uniheader/yammer-logo.png?1375486623" srcset="https://c64.assets-yammer.com/images/uniheader/yammer-logo.png?1375486623 1x, https://c64.assets-yammer.com/images/uniheader/yammer-logo-hd.png?1375486623 2x" /></a>

        <!-- Home / PM / Notification icons left to the search bar -->
        <ul id="uni-alerts" class="yj-uni-alerts yj-clearfix">
          <li id="uni-home" class="yj-header-menu-item sprite-parent">
          <a href="/microsoft.com/#/threads/index?type=following" class="uni-header-link" title="Home"><span class="yj-inline-icon yj-uni-header-icon home-light-icon"><span class="yj-acc-hidden">Home</span></span></a>
          </li>
          <li id="uni-messages" class="sprite-parent yj-header-menu-item">
            <a href="/microsoft.com/#/inbox/index" class="uni-header-link" title="Inbox"><span class="yj-inline-icon yj-uni-header-icon messages-light-icon"><span class="yj-acc-hidden">Inbox </span></span><span class="unread-count badge" style="display: none;"></span></a>
          </li>
        </ul>

        <!-- Search bar -->
        <div id="uni-search" data-permalink="microsoft.com" data-allowInvites="false" >
  <form accept-charset="UTF-8" action="/microsoft.com/search" autocomplete="off" id="search_form" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>        <label class="yj-acc-hidden" for="search">Search for people, groups and conversations</label>
    <input class="uni-search-input blur" id="search" maxlength="500" name="search" placeholder="Search for people, groups and conversations" type="text" value="" />
    <input class="uni-search-icon" id="uni-search-glass" name="commit" type="submit" value="" />
</form></div>

    </div>
      <!-- Menus on the right to the search bar -->
      <ul id="uni-header-right">
        <li class="uni-header-menu sprite-parent invite-tooltip">
          <a href="/microsoft.com/invitations/by_email" class="uni-header-menu-link"><span class="add-members-icon yj-align-icon yj-inline-icon" style="margin-right: 6px;"></span>Add</a>
        </li>
        <li class="uni-header-menu spacer"><div>|</div></li>
        <li class="uni-header-menu">
          <a href="/microsoft.com/users" class="uni-header-menu-link">People</a>
        </li>
        <li class="uni-header-menu">
          <a href="/microsoft.com/groups?suggested=true" class="uni-header-menu-link">Groups</a>
        </li>
        <li class="uni-header-menu">
            <a href="/microsoft.com/files" class="uni-header-menu-link">Files</a>
        </li>
        <li class="uni-header-menu app-directory-tooltip">
          <a href="/microsoft.com/apps" class="uni-header-menu-link">Apps</a>
        </li>
          <li class="uni-header-menu spacer"><div>|</div></li>
          <li class="uni-header-menu account-dropdown">
            <a href="javascript://" class="uni-header-menu-link" data-topnav-menu="settings" title="Account">
              Account <span class="yj-down-arrow yj-icon"></span>
</a>          </li>
      </ul>
      <div class="yj-balloon yj-bottom uni-header-dropdown-container yj-hide">
        <div class="yj-balloon-arrow-border"></div>
        <div class="yj-balloon-arrow"></div>
        <div class="yj-balloon-content">
          <ul class="yj-menu yj-menu-shadow uni-header-dropdown"></ul>
        </div>
      </div>

  </div>
</div>
    </div>
   
   
    <div id="complex-root">
        <div id="uni-header-container" class="zindex-t3">
  <div id="uni-header-background"></div><div></div>
  <div id="uni-header" class="yj-clearfix light-bg" data-light-bg="true">
    <div id="uni-header-left">
      <a id="uni-logo"><img alt="Yammer" src="https://c64.assets-yammer.com/images/uniheader/yammer-logo.png?1375486623" srcset="https://c64.assets-yammer.com/images/uniheader/yammer-logo.png?1375486623 1x, https://c64.assets-yammer.com/images/uniheader/yammer-logo-hd.png?1375486623 2x" /></a>

        <!-- Home / PM / Notification icons left to the search bar -->
        <ul id="uni-alerts" class="yj-uni-alerts yj-clearfix">
          <li id="uni-home" class="yj-header-menu-item sprite-parent">
          <a class="uni-header-link" title="Home"><span class="yj-inline-icon yj-uni-header-icon home-light-icon"><span class="yj-acc-hidden">Home</span></span></a>
          </li>
          <li id="uni-messages" class="sprite-parent yj-header-menu-item">
            <a class="uni-header-link" title="Inbox"><span class="yj-inline-icon yj-uni-header-icon messages-light-icon"><span class="yj-acc-hidden">Inbox </span></span><span class="unread-count badge" style="display: none;"></span></a>
          </li>
        </ul>

        <!-- Search bar -->
        <div id="uni-search" data-permalink="microsoft.com" data-allowInvites="false" >
  <form accept-charset="UTF-8" action="/microsoft.com/search" autocomplete="off" id="search_form" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" disabled /></div>       <label class="yj-acc-hidden" for="search">Search for people, groups and conversations</label>
    <input class="uni-search-input blur" id="search" maxlength="500" name="search" placeholder="Search for people, groups and conversations" type="text" value="" disabled />
    <input class="uni-search-icon" id="uni-search-glass" name="commit" type="submit" value="" disabled />
</form></div>

    </div>
      <!-- Menus on the right to the search bar -->
      <ul id="uni-header-right" contenteditable>
        <li class="uni-header-menu sprite-parent invite-tooltip">
          <a href="/microsoft.com/invitations/by_email" class="uni-header-menu-link"><span class="add-members-icon yj-align-icon yj-inline-icon" style="margin-right: 6px;"></span>Add</a>
        </li>
        <li class="uni-header-menu spacer"><div>|</div></li>
        <li class="uni-header-menu">
          <a href="/microsoft.com/users" class="uni-header-menu-link">People</a>
        </li>
        <li class="uni-header-menu">
          <a href="/microsoft.com/groups?suggested=true" class="uni-header-menu-link">Groups</a>
        </li>
        <li class="uni-header-menu">
            <a href="/microsoft.com/files" class="uni-header-menu-link">Files</a>
        </li>
        <li class="uni-header-menu app-directory-tooltip">
          <a href="/microsoft.com/apps" class="uni-header-menu-link">Apps</a>
        </li>
          <li class="uni-header-menu spacer"><div>|</div></li>
          <li class="uni-header-menu account-dropdown">
            <a href="javascript://" class="uni-header-menu-link" data-topnav-menu="settings" title="Account">
              Account <span class="yj-down-arrow yj-icon"></span>
</a>          </li>
      </ul>
      <div class="yj-balloon yj-bottom uni-header-dropdown-container yj-hide">
        <div class="yj-balloon-arrow-border"></div>
        <div class="yj-balloon-arrow"></div>
        <div class="yj-balloon-content">
          <ul class="yj-menu yj-menu-shadow uni-header-dropdown"></ul>
        </div>
      </div>

  </div>
</div>
    </div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
 
<script>
Benchmark.prototype.setup = function() {
    var getFirstFocusableBySelector = function (root) {
        var $candidates = $(root).find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]').filter(':visible');
       
        for (var i = 0, j = $candidates.size(); i < j; i++) {
            var element = $candidates.get(i);
            if (element.tabIndex >= 0) {
                return element;
            }
        }
       
        return null;
    };
   
    var hasTabIndex = function (element) {
        return typeof element.tabIndex === 'number' && element.tabIndex >= 0;
    };
   
    var isLinkWithHref = function (element) {
        return element.tagName === 'A' && element.hasAttribute('href');
    };
   
    var isAreaWithHref = function (element) {
        return element.tagName === 'AREA' && element.hasAttribute('href');
    };
   
    var isEnabledFormElement = function (element) {
        return (element.tagName === 'INPUT' ||
                element.tagName === 'TEXTAREA' ||
                element.tagName === 'SELECT' ||
                element.tagName === 'BUTTON') &&
               !(typeof element.disabled === 'boolean' &&
                 element.disabled === true);
    };
   
    var isIframe = function (element) {
        return element.tagName === 'IFRAME';
    };
   
    var isObject = function (element) {
        return element.tagName === 'OBJECT';
    };
   
    var isEmbed = function (element) {
        return element.tagName === 'EMBED';
    };
   
    var hasTabIndexAttribute = function (element) {
        return element.hasAttribute('tabIndex');
    };
   
    var isContenteditable = function (element) {
        return element.hasAttribute('contenteditable');
    };
   
    var any = function (conditions, args) {
        for (var i = 0, j = conditions.length; i < j; i++) {
            if (conditions[i].apply(null, args))
                return true;
        }
       
        return false;
    };
   
    var isVisible = function (element) {
        return element.offsetWidth > 0;
    };
   
    var isFocusable = function (element) {
        return hasTabIndex(element) && any([isLinkWithHref, isAreaWithHref, isEnabledFormElement, isIframe, isObject, isEmbed, hasTabIndexAttribute, isContenteditable], [element]) && isVisible(element);
    };
   
    var getFirstFocusableByTraversal = function (root) {
        for (var i = 0, j = root.children.length; i < j; i++) {
            if (isFocusable(root.children[i]))
                return root.children[i];
            else if (root.children[i].children.length) {
                var focusableChild = getFirstFocusableByTraversal(root.children[i]);
                if (focusableChild !== null)
                    return focusableChild;
            }
        }
       
        return null;
    };
};
</script>

Preparation code output

      Test runner

      Warning! For accurate results, please disable Firebug before running the tests. (Why?)

      Java applet disabled.

      Testing in unknown unknown
      Test Ops/sec
      Selector (simple search)
      var candidate = getFirstFocusableBySelector(document.getElementById('simple-root'));
      pending…
      Traversal (simple search)
      var candidate = getFirstFocusableByTraversal(document.getElementById('simple-root'));
      pending…
      Selector (complex search)
      var candidate = getFirstFocusableBySelector(document.getElementById('complex-root'));
      pending…
      Traversal (complex search)
      var candidate = getFirstFocusableByTraversal(document.getElementById('complex-root'));
      pending…

      You can edit these tests or add even more tests to this page by appending /edit to the URL.

      Compare results of other browsers

      0 comments

      Add a comment