Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-settings.php on line 472

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-settings.php on line 487

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-settings.php on line 494

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-settings.php on line 530

Strict Standards: Declaration of Walker_Page::start_lvl() should be compatible with Walker::start_lvl(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 594

Strict Standards: Declaration of Walker_Page::end_lvl() should be compatible with Walker::end_lvl(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 594

Strict Standards: Declaration of Walker_Page::start_el() should be compatible with Walker::start_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 594

Strict Standards: Declaration of Walker_Page::end_el() should be compatible with Walker::end_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 594

Strict Standards: Declaration of Walker_PageDropdown::start_el() should be compatible with Walker::start_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 611

Strict Standards: Declaration of Walker_Category::start_lvl() should be compatible with Walker::start_lvl(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 705

Strict Standards: Declaration of Walker_Category::end_lvl() should be compatible with Walker::end_lvl(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 705

Strict Standards: Declaration of Walker_Category::start_el() should be compatible with Walker::start_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 705

Strict Standards: Declaration of Walker_Category::end_el() should be compatible with Walker::end_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 705

Strict Standards: Declaration of Walker_CategoryDropdown::start_el() should be compatible with Walker::start_el(&$output) in /home/edelabar/ericdelabar.com/wp-includes/classes.php on line 728

Strict Standards: Redefining already defined constructor for class wpdb in /home/edelabar/ericdelabar.com/wp-includes/wp-db.php on line 306

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-includes/cache.php on line 103

Strict Standards: Redefining already defined constructor for class WP_Object_Cache in /home/edelabar/ericdelabar.com/wp-includes/cache.php on line 425

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-includes/query.php on line 21

Deprecated: Assigning the return value of new by reference is deprecated in /home/edelabar/ericdelabar.com/wp-includes/theme.php on line 623

Strict Standards: Redefining already defined constructor for class WP_Dependencies in /home/edelabar/ericdelabar.com/wp-includes/class.wp-dependencies.php on line 15

Strict Standards: call_user_func_array() expects parameter 1 to be a valid callback, non-static method GoogleSitemapGeneratorLoader::Enable() should not be called statically in /home/edelabar/ericdelabar.com/wp-includes/plugin.php on line 311
Found Code: Optimizing Large Form Performance in JavaScript at Eric DeLabar

Found Code: Optimizing Large Form Performance in JavaScript

As I’ve covered before, ill-used JavaScript can lead to some serious performance problems, most of which are caused by simply not thinking about what the code is really doing. Recently I came across a site that provided digital photo printing, This site had a nice interface that allowed my to upload close to three hundred photos. On the resulting page, each photo was displayed with all of the available sizes as input boxes, which looked something like this. I liked the interface, but came across a very serious problem. The event handlers that updated the totals box ran on the keyup event and recalculated the total of the entire form! This worked fine with ten or twenty photos, but the 300 that I provided brought my browser to a screeching halt.

I’ve taken the liberty of creating a very simplistic mock-up of the form and a simplified version of the JavaScript, which is available in my examples section. The demo uses Firebug and Firebug Lite for logging just like I did in my dollar function article, and the benchmark class from that article as well. The site’s JavaScript was a bit more complex and actually did an AJAX lookup of the price on each keyup, but I’m more concerned with the JavaScript performance here, so I simplified the code to something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var PhotoSelector = Class.create();
PhotoSelector.prototype = {
	initialize: function( name ) {
		var init = new Benchmark();
		init.start();
		console.debug( "Beginning Initialization!" );
 
		$A(document.getElementsByTagName("input")).each( 
			function( inp ) {
				inp.value = 0;
				if( Element.hasClassName(inp,"qtyInput") ) {
					inp.onkeyup = this.recalculate.bindAsEventListener( this );
				}
			}, this
		);
 
		init.end();
		console.debug( "Initialization Complete in " + init.inMillis() + " milli(s)." );
	},
 
	recalculate: function(e) {
		var calc = new Benchmark();
		calc.start();
 
		$("fourby").value = 0;
		$("fiveby").value = 0;
		$("eightby").value = 0;
		$("wallet").value = 0;
 
		var inputs = $("pictures").getElementsByTagName("input");
		for( var i = 0; i < inputs.length; i++ ) {
			var totalId = inputs[i].id.match(/([a-z]+)[0-9]+/)[1];
			var total = $(totalId);
			total.value = parseInt(total.value) + parseInt($(inputs[i]).value);
		}
 
		calc.end();
		console.debug( "Recalculation Complete in " + calc.inMillis() + " milli(s)." );
	}
}
var ps;
Event.observe(window,"load",function(e){ps = new PhotoSelector()});

Basically, on window load, this code grabs every input element, sets its value to zero, and binds an event handler to it. The event handler runs on key up and loops through every input box in the “pictures” list, and updates the totals inputs at the top of the page. As I said above, this code works fine with 20 pictures, but it starts getting slow around 300, and becomes almost unusable at 1000. Care to try 10,000? (Be careful, it crashes my browser!) To test it, simply enter values in the photo inputs and watch the totals boxes increment.

The main problem with this code comes from the recalculate function. Problem number one is my personal pet peeve, the dollar sign function is called at least six times! Well, I guess six times wouldn’t be terrible for the entire page, but it’s called at least six times on every key up event! Problem number two, the biggest problem, is the fact that this code re-crawls what amounts to the entire DOM every time the event fires. Obviously the larger the DOM, the more time this is going to take.

So, how do we fix it? Well, here’s how I fixed it, I’ll explain the details below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var PhotoSelector = Class.create();
PhotoSelector.prototype = {
	initialize: function( name ) {
		var init = new Benchmark();
		init.start();
		console.debug( "Beginning Initialization!" );
 
		this.old = 0;
		var totals = {
			fourby: $("fourby"),
			fiveby: $("fiveby"),
			eightby: $("eightby"),
			wallet: $("wallet")
		};
		totals.fourby.value = 0;
		totals.fiveby.value = 0;
		totals.eightby.value = 0;
		totals.wallet.value = 0;
 
		$$(".qtyInput").each( 
			function( inp, index ) {
				inp.onfocus = this.enter( inp, this ).bindAsEventListener( this );
				inp.onblur = this.recalculate( inp, totals, this ).bindAsEventListener( this );
			}, this
		);
 
		init.end();
		console.debug( "Initialization Complete in " + init.inMillis() + " milli(s)." );
	},
 
	enter: function( inp, me ) {
		return function(e) {
			me.old = parseInt(inp.value);
		}
	},
 
	recalculate: function( inp, totals, me ) {
		var type = inp.id.match(/([a-z]+)[0-9]+/)[1];
		var total = totals[type];
		inp.value = 0;
		return function(e) {
			var calc = new Benchmark();
			calc.start();
 
			var newVal = parseInt(inp.value);
			if( me.old > newVal ) {
				newVal = ( me.old - newVal ) * -1;
			}
			total.value = parseInt(total.value) + newVal;
 
			calc.end();
			console.debug( "Recalculation Complete in " + calc.inMillis() + " milli(s)." );
		}
	}
}
var ps;
Event.observe(window,"load",function(e){ps = new PhotoSelector()});

To solve problem number one from above I created a simple object for storing references to all of the total input boxes (lines 9-14), now we have a simple associative array lookup whenever we need to update a total. Problem number two is mainly solved by recording the original value of the input on focus (line 22, 31-35), and then comparing them on blur (line 23, 37-54). Because we’re doing this on blur, we can update only the necessary total input (lines 45-49) instead of recalculating the entire form. I made one final tweak, mainly to make solving problem number one easier, and that is the recalculate function now returns a specific event handler for the given input so that the event handler itself does not need to call the dollar function.

So, comparing these in my regular, not-very-scientific fashion, I came up with the following results. I chose to measure the startup time, which will increase with the size of the page, as well as the event handler time. I also measured these times across a pretty decent amount of pictures, and across a few browsers.

Safari (OS X)

Optimized Time Unoptimized Time
Pictures Load Handler Load Handler
10 4 ms 0 ms 6 ms 3 ms
50 17 ms 0 ms 14 ms 13 ms
100 33 ms 0 ms 24 ms 26 ms
1000 365 ms 0 ms 452 ms 178 ms

Internet Explorer 7 (Windows Vista)

Optimized Time Unoptimized Time
Pictures Load Handler Load Handler
10 56 ms 0 ms 48 ms 9 ms
50 238 ms 0 ms 213 ms 76 ms
100 457 ms 0 ms 424 ms 235 ms
1000 4642 ms 0 ms 4584 ms 28110 ms

28 seconds!? Why!?

Firefox 2 (Windows Vista)

Optimized Time Unoptimized Time
Pictures Load Handler Load Handler
10 12 ms 0 ms 8 ms 7 ms
50 45 ms 0 ms 31 ms 30 ms
100 87 ms 1 ms 60 ms 59 ms
1000 985 ms 3 ms 584 ms 581 ms

The results pretty obviously speak for themselves, but there is one caveat, be sure to notice the initial load time. Since the event handlers still need to be assigned to each input on the page the more inputs there are the longer the page load takes, and the load time is even slightly slower on the optimized page. Be sure to consider this time, possibly by capping the number of inputs displayed, since the code itself is very processor intensive and appears to actually hang the entire computer while processing. Obviously, these fixes become more important as the number of inputs grows, but any speed increase when the user is directly interacting with the page is a good one!

2 Responses to “Found Code: Optimizing Large Form Performance in JavaScript”

  1. website design:

    A very usefull article well worth a read.

    June 10, 2008 4:34 pm

  2. Justin Meyer:

    Great article. You might try event delegation to lower the memory use of all those event handlers.

    June 10, 2008 10:32 pm

Trackback URI | Comments RSS

Leave a Reply