Michael Wallent
Lead Program Manager, DHTML
Microsoft Corporation
September 22, 1998
The following article was originally published in the Site Builder Magazine (now known as MSDN Online Voices) "DHTML Dude" column. Before reading the DHTML Dude column, you need Internet Explorer 4.0 .
Last month, as I sat writing my column in my little office hovel on the sylvan campus of Microsoft, the sun was shining, the birds were singing. Beautiful. Summer. It was a bucolic, relaxed place. In September, things tend to change a little around here. I can summarize in one word: Rain. The summer is great, but then we molder for nine months. The mood changes as well. People are going where they are going in a hurry. No need to wait outside and get wet -- go where you need to go. Which brings us to our topic for this month -- performance. How to make your pages go where they need to go -- and fast.
You may be asking: "Little office hovel?" It's true, for the past two years, the DHTML Dude has been laboring in what is known here in Redmond as an "Inside Inside" office. There are three categories of offices at Microsoft. "Window," as the name implies, means you can enjoy the sunshine. "Inside" is across from a Window office. You get to share a little of the sunshine from the facing office's window, unless you're stymied by mole-like persons, who shun the bounty of the window, performing Rube Goldberg-ian machinations to prevent all natural light from entering their offices. I have seen such photophobes put individual pieces of duct tape over the small holes in the blinds. The third category? The dreaded "Inside Inside" office. These are in internal hallways, and face an office that has no window, either.
Interestingly, offices at Microsoft are allocated purely by seniority. The longer you have been at Microsoft, the better office you get. I have only been working at Microsoft for a little more than two years, so I was near the bottom of the list for all that time. People would come visit my cave, and laugh. Not only did I have an "Inside Inside," but the smallest one on the floor. (Yes, I measured.) Finally, my salvation is at hand. We are moving to another building, and in the new scheme I have a window office. Just in time to watch the rain come down.
As I first wrote about back in December 1997 (Frequent Flyers: Boosting Performance on DHTML Pages), one way to improve performance is to break your sites up into more-reusable components, such Cascading Style Sheets and include files for commonly used script. Last month, as I looked at all the DHTML sites people sent in, it was clear that including common script via the <script src="myscript.js"></script> method is becoming more common.
One problem people seem to be running into is the dreaded asynchrony issue. "Asynchrony"? What's that? For the purposes of HTML, asynchrony means that events do not always occur one after the other in a predictable way.
For example, in a FRAMESET like
<frameset> <frame src="page1.htm"> <frame src="page2.htm"> </frameset>
the order in which the frames load is non-deterministic. Sometimes page1.htm will load first, sometimes page2.htm. The files are loaded asynchronously by the browser. Both load at the same time, and when they finish is a function of their size, their content, and the location from which they come. It would be a bad idea to have some immediate script in page1.htm that blindly depended on page2.htm.
When an HTML page completes, it throws the window.onload event. However, if you want to check to see if a page is loaded, you can check the document.readyState property. If the page is loaded, the value of the readyState property will be "complete". Other HTML elements that include content from another source expose the readyState property as well.
Elements that expose the readyState property are:
FIELDSET
IMG
LINK
OBJECT
SCRIPT
STYLE
Other values that the readyState property exposes are: "unitialized," "loading," "interactive," and "complete".
There is an event called onreadystatechange that all of these elements expose, as does the document. As the name implies, whenever the readyState property changes the onreadystate event is fired.
So, how does this relate to including script? Well, if you need to call a method that's included from a secondary source, you need to make sure that that method is actually there. One way to tell is to check the readyState property of the script tag doing the including. If its not "complete," then don't depend on the methods in the script it's including. You can then use the onreadystatechange event to be notified when the script is ready to be called.
Like this:
<script language="JavaScript" id=SomeFile src="somefile.js"></script> <script language="JavaScript"> function useStuffInSomeFile() { } if (SomeFile.readyState == "complete" ) { useStuffInSomeFile() { } else { SomeFile.onreadystatechange = ifComplete; } function ifComplete() { if (SomeFile.readyState == "complete") { SomeFile.onreadystatechange = null; useStuffInSomeFile(); } } </script>
How does this relate to performance? Making the browser asynchronous means that it can be doing more things at once, and can render your pages more quickly. If you make your script aware of asynchrony, your pages will be more stable, and your customers will be happier.
As an aside, we've added a new event to Internet Explorer 5: onpropertychange. This is exposed on all elements in the document, and is fired whenever any of the properties of an element change. The event object exposes a new property -- event.propertyName, so you can tell which property actually changed. Dynamic Properties uses this internally to track changes.
A caution: You might have a page that "works" on your own machine, running locally, and you haven't gone through the extra steps to protect your code. Don't be fooled. You still need to make your code "asynch-aware." Or someday, the chairman of your company will place an urgent call to you asking why the new payroll site doesn't work when she's on the road in Uruguay. I'm not sure she'd accept as an answer, "Well, boss, let me tell you a little story about asynchrony ."
A pretty common DHTML operation is to drag some chunk of HTML content around on a page using mouse events and CSS positioning. There are many ways to do this -- some of them very slow. The performance of this type of operation tends to be very sensitive, as you are running the same chunk of code every time the mouse moves.
Here's my drag-and-drop page. It will run on Internet Explorer 4.0 and 5.
As you click on any of the colored boxes, the border darkens, and the box can be dragged around. Use ALT+click to multiselect and move.
If you toggle the button for "Click to resize," instead of moving, you can then resize elements.
Here's the source for this page (expurgated slightly):
<html> <style> DIV {cursor: hand} </style> <body style="font-family: verdana"> <H2>Simple Drag and Resize Example</h2> <h4>Use alt-click to multi-select</h4> <input type=button value="Moving, click to resize" onclick="toggleMoveResize(this); return false"> <div moveable=true style="position: absolute; top: 150px; left: 100px; width: 100px; height: 100px; background-color: red; border: solid 2px black"> Click and drag me </div> <div moveable=true style="position: absolute; top: 150px; left: 250px; width: 100px; height: 100px; background-color: blue; border: solid 2px black"> Click and drag me </div> <script language="JavaScript"> var activeElements = new Array(); var activeElementCount = 0; var lTop, lLeft; var doMove = true; var doResize = false; function toggleMoveResize(e) { if (doMove) { doMove = false; doResize = true; e.value = "Resizing, Click to Move"; } else { doMove = true; doResize = false; e.value = "Moving, Click to Resize"; } } function mousedown() { var mp; mp = findMoveable(window.event.srcElement); if (!window.event.altKey) { for (i=0; i<activeElementCount; i++) { if (activeElements[i] != mp) { activeElements[i].style.borderWidth = "2px"; } } if (mp) { activeElements[0] = mp; activeElementCount = 1; mp.style.borderWidth = "4px"; } else { activeElementCount = 0; } } else { if (mp) { if (mp.style.borderWidth != "4px") { activeElements[activeElementCount] = mp; activeElementCount++; mp.style.borderWidth = "4px"; } } } window.status = activeElementCount; lLeft = window.event.x; lTop = window.event.y; } document.onmousedown = mousedown; function mousemove() { var i, dLeft, dTop; if (window.event.button == 1) { sx = window.event.x; sy = window.event.y; dLeft = sx - lLeft; dTop = sy - lTop; for (i=0; i<activeElementCount; i++) { if (doMove) moveElement(activeElements[i], dLeft, dTop); if (doResize) resizeElement(activeElements[i], dLeft, dTop); } lLeft = sx; lTop = sy; return false; } } function moveElement(mp, dLeft, dTop) { var e e = mp; e.style.posTop += dTop; e.style.posLeft += dLeft; } function resizeElement(mp, dLeft, dTop) { var e; e = mp; e.style.posHeight += dTop; e.style.posWidth += dLeft; } function findMoveable(e) { if (e.moveable == 'true') return e; if (e.tagName == "BODY") return null; return findMoveable(e.parentElement); } function findDefinedMoveable(e) { if ((e.moveable == 'true') || (e.moveable == 'false')) return e; if (e.tagName == "BODY") return null; return findDefinedMoveable(e.parentElement); } function rfalse() { return false; } document.onmousemove = mousemove; document.onselectstart = rfalse; </script> </body> </html>
There are three primary functions in this page: maintaining selection, moving, and resizing.
Maintaining selection is done by first trapping the document.onmousedown event. The mousedown() function is called whenever the mouse goes down. Note that I'm not using onclick here, because that notifies on the mouseup -- and that would be too late for a user who was mousing down on an object, and starting to drag. The onclick wouldn't fire until after the drag was complete. A simple array is used to keep a list of all the objects in the selection list.
I'm not simply selecting the window.event.srcElement of the click. I have the findMoveable() function that finds the first container (if any) of the srcElement that has the "moveable" expando property set. I did this because if I wanted the moveable elements to contain rich HTML, the onmousedown may have occurred on one of the inner HTML elements, and not the container. The findMoveable() method quickly tells me if I have clicked anywhere within an element that I want to move around, and gives me that outer container.
Once the selection list is up to date, the actual movement of the elements is done based on the onmousemove event.
Let's take a closer look at that code.
function mousemove() { var i, dLeft, dTop; if (window.event.button == 1) { sx = window.event.x; sy = window.event.y; dLeft = sx - lLeft; dTop = sy - lTop; for (i=0; i<activeElementCount; i++) { if (doMove) moveElement(activeElements[i], dLeft, dTop); if (doResize) resizeElement(activeElements[i], dLeft, dTop); } lLeft = sx; lTop = sy; return false; } } function moveElement(e, dLeft, dTop) { e.style.posTop += dTop; e.style.posLeft += dLeft; } function resizeElement(e, dLeft, dTop) { e.style.posHeight += dTop; e.style.posWidth += dLeft; }
In the mouse-move handler, the first thing I check for is to see if the left button is down. After that, I get the current mouse position. A dx/dy is created from the last mouse position. This delta establishes how much each object will be moved or resized. The code then loops through the array of selected elements. Note that the array contains the elements directly. I do not store the IDs of the elements, as that would be slower than to have to reference and de-reference the document.all collection. The appropriate move or resize function is called, based on the currently selected status.
For the moveElement function, the mechanism for incrementing the position is very simple. I auto increment posTop and posLeft. This is significantly faster than using the top and left properties. Since top and left are strings, they can't be auto-incremented. The value needs to be collected, and parsed into a number and then incremented. Using the posTop and posLeft values doesn't require any string operations or conversions and is faster.
The resizeElement function is very similar, except it increments the posHeight and posWidth values instead.
When building tightly looping code, removing even a single expression can be important. This page was specifically designed to limit the number of operations performed in tight loops, and it's pretty fast as a result.
Drip, drip, drip
Michael Wallent is Microsoft's group program manager for DHTML and an exceedingly proud new papa.