Search code examples
jqueryperformancedelegationevent-delegation

jQuery: Does selector specificity increase performance with .on() delegation?


Is there a performance gain when increasing specificity in delegated event binding with .on()?

How does one test for this?

For example, is the second statement more performant than the first:

$('.foo').on('mouseenter', ':has(.other)', function (e) {
    console.log(e);
});

$('.bar').on('mouseenter', 'button:has(.other)', function (e) {
    console.log(e);
});

$('.foo').on('mouseenter', ':has(.other)', function (e) {
  console.log(e);
});

$('.bar').on('mouseenter', 'button:has(.other)', function (e) {
  console.log(e);
});
.foo, .bar {
  margin: 2em auto;
  text-align: center;
}
<div class="foo">
  <button type="button">Hello
    <span class="other">World</span>
  </button>
</div>

<div class="bar">
  <button type="button">Hello
    <span class="other">World</span>
  </button>
</div>

My main concern is that listening for mouseenter events with delegation is a performance drain and if there is a way to test for performance on this type of event handler.

Update

I should clarify that I'm not looking to understand the performance implications of using delegation in general. I'm hoping to understand the performance implications of using delegation with mouseenter during user interaction (as the user's mouse enters both delegated and non-delegated elements within the bound element) and if there is a performance gain by using a more specific selector for delegation.

My inclination is to assume there isn't because every event has to bubble up to the bound element before being checked against the delegated selector.

But is there a way to test for this?


Solution

  • Specificity helps… a little.

    According to this JSPerf test, adding specificity to the selector in the posted examples consistently improves performance in repeated tests, but the gains could conceivably be considered negligible at the user interaction level where events are occurring sporadically in relatively small iterations.

    In some tests, the difference was small enough and within the margin of error to be considered statistically equivalent.

    Selector filtering in the handler hurts

    But the most surprising result is that of an ancillary third test suggested by one of the commenters above which takes :has() out of the selector and replaces it with a .has() check in the callback function, which consistently performs slower than using :has() in the selector — often by 100 operations per second or more.

    This is arguably less negligible and especially remarkable considering that the jQuery documentation for :has() indicates that you should expect better performance here:

    Because :has() is a jQuery extension and not part of the CSS specification, queries using :has() cannot take advantage of the performance boost provided by the native DOM querySelectorAll() method. For better performance in modern browsers, use $( "your-pure-css-selector" ).has( selector/DOMElement ) instead.

    I don't doubt that in a selector statement where .has() is simply used to reduce the set of matched elements, that one could expect better performance; but I suspect the performance drain demonstrated in the test is the result of breaking that convention by moving the .has() check into an event handler where event data is being passed around, a new jQuery object is being constructed from this, and .length is being accessed and checked in an if statement.


    Test formulation

    To formulate the test, three event binding benchmarks were configured and applied to the same HTML markup, then the same event-triggering test case — simulating a user mousing over various delegated and non-delegated elements on the screen — was applied to each of the three benchmarks for comparison.

    Benchmark 1:

        $('.foo').on('mouseenter', ':has(.other)', function(e) {
          console.log(e);
        });
    

    Benchmark 2:

        $('.foo').on('mouseenter', 'button:has(.other)', function(e) {
          console.log(e);
        });
    

    Benchmark 3:

        $('.foo').on('mouseenter', 'button', function(e) {
          if ($(this).has('.other').length) {
            console.log(e);
          }
        });
    

    HTML:

    <div class="foo">
        <button type="button" id="A">some text</button>
        <button type="button" id="B"><span class="other">some text</span></button>
        <div id="C">some text</div>
    </div>
    

    Test case:

    $('#A').trigger('mouseenter');
    $('#B').trigger('mouseenter');
    $('#C').trigger('mouseenter');
    

    Screenshot of table showing JSPerf test results