YUI Rollover Part 2: Improving JavaScript Performance and Scalability

In my previous article we traded in old school JavaScript habits for unobtrusive methods and the Yahoo! User Interface Library (YUI). The YUI's Event and DOM extensions greatly simplify basic DOM scripting tasks. Let's build on this foundation and improve the rollover script's performance and scalability. Thanks to Nick and Dirk for their comments on event listeners impact on script performance. Nick points out the fact that JavaScript performance will degrade as more buttons are added to the example rollover menu. The use of a DOM scripting technique called event delegation can greatly reduce performance hits sustained through the assignment of event listeners.

Improve Performance through Event Delegation and Bubbling

The current version of the rollover script loops through the menu container and assigns event listeners to each button. Assigning listeners to child elements this way can, and will, have a negative impact on performance.

Illustration of the DOM bubbling effect

Let's use event delegation to simplify things. Event delegation leverages an inherit browser behavior called "event bubbling". By default, when an event happens to an element's descendents, the event "bubbles up" through the DOM to the parent. This means that we can assign event listeners to the menu rather than every button it contains. In our rollover example we can reduce the number of event listeners from 3 to just 1. Wow! We've just cut the work required to assign listeners by 66%. You can see that the performance savings of event delegation add up quickly.

We'll use the same HTML markup and CSS from the previous example.

<div id="menu">
  <div class="btn">
    <a href="#">Link 1</a>
    <p>Description for link 1</p>
  </div>
  <div class="btn">
    <a href="#">Link 2</a>
    <p>Description for link 2</p>
  </div>
  <div class="btn">
    <a href="#">Link 3</a>
    <p>Description for link 3</p>
  </div>
</div>

Here's the updated script using the YUI's Event and DOM extensions. The script now uses Event's handy getTarget() method which captures the target of the current event.

<script type="text/javascript">
// <![CDATA[
// Create the menu object
var roll = {
  // The rollover function
  over : function(e) {
    // Capture the current target element
    var elTarget = YAHOO.util.Event.getTarget(e);
    // Step through this section of the DOM looking for a btn 
    // in the target's ancestory, stop at the menu container
    while (elTarget.id != 'menu') {
      // If we're over a btn, turn it on and stop
      if (YAHOO.util.Dom.hasClass(elTarget, 'btn')) { 
        YAHOO.util.Dom.addClass(elTarget, 'btn-over');
        break;
      // Keep looking one level up
      } else {
        elTarget = elTarget.parentNode;
      }
    }
  },
  // Reset menu styles
  out : function(e) {
    var btns = YAHOO.util.Dom.getElementsByClassName('btn', 'div', this);
    YAHOO.util.Dom.removeClass(btns, 'btn-over');
  }
};
// Assign event listeners to just the menu
YAHOO.util.Event.on('menu', 'mouseover', roll.over);
YAHOO.util.Event.on('menu', 'mouseout', roll.out);
// ]]>
</script>

Take a look at the functioning example.

Allow for Growth

The updated rollover script gives us a big improvement in performance, but it doesn't scale at all. The script includes hardcoded references to a single menu. What if we want to add additional menus? First, let's update the stylesheet to use classes, instead of an ID, to target and style rollover menus.

.rollover {
  font: 85% Arial, Helvetica, san-serif;
}
.rollover .btn,
.rollover .btn-over {
  background-color: #e7e7e7;
  border-width: 2px;
  border-style: solid;
  color: #666;
  float: left;
  margin: 0 5px;
  width: 150px;
}
.rollover .btn {
  border-color: #999;
}
.rollover .btn a,
.rollover .btn-over a {
  color: #fff;
  background-color: #999;
  display: block;
  padding: .3em .5em;
  text-decoration: none;
}
.rollover p {
  clear: both;
  padding-top: 1em;
}
.btn p {
  padding: 0 .5em;
}
/* Rollover styles */
.rollover .btn-over {
  border-color: #666;
  color: #000;
}
.rollover .btn-over a {
  background-color: #666;
}

Next, apply the rollover class to a menu, or two.

<div id="menu1" class="rollover">
  <div class="btn">
    <a href="#">Link 1</a>
    <p>Description for link 1</p>
  </div>
  <div class="btn">
    <a href="#">Link 2</a>
    <p>Description for link 2</p>
  </div>
  <div class="btn">
    <a href="#">Link 3</a>
    <p>Description for link 3</p>
  </div>
  <p>Here's a menu.</p>
</div>
<div id="menu2" class="rollover">
  <div class="btn">
    <a href="#">Link 3</a>
    <p>Description for link 3</p>
  </div>
  <div class="btn">
    <a href="#">Link 4</a>
    <p>Description for link 4</p>
  </div>
  <div class="btn">
    <a href="#">Link 5</a>
    <p>Description for link 5</p>
  </div>
  <p>Here's another menu.</p>
</div>

The script now needs to include a method for adding event listeners to each of the rollover menus. This task is handled by menu.init() which is run once the page is loaded. The only other change is the use of the rollover class to stop our walk of the DOM.

<script type="text/javascript">
// <![CDATA[
// Create the menu object
var roll = {
  // Assign event listeners to all rollover menus,
  // Isn't getElementsByClassName wonderful?!
  init: function() {
    var menus = YAHOO.util.Dom.getElementsByClassName('rollover', 'div');
    YAHOO.util.Event.on(menus, 'mouseover', roll.over);
    YAHOO.util.Event.on(menus, 'mouseout', roll.out);
  },
  // The rollover function, only change is using className instead of ID
  over: function(e) {
    // Capture the current target element
    var elTarget = YAHOO.util.Event.getTarget(e);
    // Step through this section of the DOM looking for a btn
    // in the target's ancestory, stop at the menu container
    while (elTarget.className != 'rollover') {
      // If we're over a btn, turn it on and stop
      if (YAHOO.util.Dom.hasClass(elTarget, 'btn')) {
        YAHOO.util.Dom.addClass(elTarget, 'btn-over');
        break;
        // Keep looking one level up
      } else {
        elTarget = elTarget.parentNode;
      }
    }
  },
  // Reset menu styles
  out: function(e) {
    var btns = YAHOO.util.Dom.getElementsByClassName('btn', 'div', this);
    YAHOO.util.Dom.removeClass(btns, 'btn-over');
  }
};
// Initialize all rollover menus when the page loads
YAHOO.util.Event.on(window, 'load', roll.init);
// ]]>
</script>

And here's the working example of the rollover script that scales.

Conclusion

So there you have it. A few simple examples illustrating how to improve performance and scalability in JavaScript. These are basic examples and hopefully they'll help you refactor your scripts to meet your site's needs. I highly recommend reading Peter-Paul Koch's explanation of mouse events and event order in the DOM and how each browser handles them. They helped me tremendously.

AttachmentSize
yui-rollover-multi.css622 bytes
yui-rollover-multi.html2.49 KB
yui-rollover2.html1.78 KB

Comments

hi! a nice job you've done!

hi! a nice job you've done! now i finally understood how it works. so thank you very much for this detailed instructions! have a nice day

improved Java Script Performance

Yeah you are correct to avoid the init and out looping problem you can use above instuctions, thanks for sharing.

One more little thing

getElementsByClassName can take a function as a last parameter. For the init and out methods you could do that to avoid looping through the button elements two or three times:


init: function() {
YAHOO.util.Dom.getElementsByClassName('rollover', 'div', function(el) {YAHOO.util.Event.on(el, 'mouseover', roll.over); YAHOO.util.Event.on(el, 'mouseout', roll.out);});
},
...
out: function() {
YAHOO.util.Dom.getElementsByClassName('btn', 'div', this, function(el) {YAHOO.util.Dom.removeClass(el, 'btn-over');});
}

copyright © 2011, 2 tablespoons | Privacy Policy