May 27th 2008
Optimize UpdatePanel performance by avoiding unnecessary element disposal
I fought using update panels for a long time. Every time someone demonstrates the MS AJAX framework they skip the amazing features and go right to update panels. They’d say “Look how easy they are to use!”, “They integrate right into the code you have now.”, and my personal favorite, “This is how you can AJAX enabled your website”. I thought they were a heavy solution filled with problems like smartnav has in asp.net 1.1.
The MS AJAX framework is much more than just update panels. There’s more to writing AJAX enabled web sites than wrapping your dynamic content inside update panels. However, there’s something I failed to realize- for what they do, update panels are the best tool for the job. If the goal is to asynchronously update content in a page using the same code used to render the content in the first place, updates panels are it. Less obvious features include:
- Only the data being refreshed is transmitted from the web server
- UI code uses the same controls to render synchronously or asynchronously
- The framework manages the loading of additional javascript and css references
- The framework only renders update panels that have been triggered for refresh
- There is a simple and effective way to specify which panels get updated on postback
- Update panels work almost seamlessly with the other parts of the MS AJAX framework - components, controls, and behaviors
I have now accepted the fact that Microsoft has put more thought, time and money into making update panels better than any homegrown solution.
In the primer Asp.Net AJAX IN ACTION, the authors take great care to explain precisely how update panels work. They describe how the ajax framework creates and disposes components when an update panel’s contents are updated. For every element in the update panel being disposed, the ajax framework checks to see if there are any controls or behaviors bound to the element that needs to be disposed.
Note: any components not bound to elements are not disposed when update panels are refreshed. When the page_unload() event calls the dispose method of the components an error may occur because the DOM state has changed and the components don’t reflect it.
Why bother with object disposal? Why not just replace the innerHTML of the update panel’s div? Julien Lecompte (from Yahoo!) explains a few issues here. Basically, expando attributes on elements cause memory leaks when not disposed properly. Douglas Crockford (also from Yahoo!) describes the basic workaround. Fortunately for us, however, the Microsoft framework does this already in part through the component dispose method.
When the update panel is about to be loaded, the framework doesn’t know which objects need to be disposed. It has to iterate over all the elements about to be destroyed and check for objects to dispose. The following code is from MicrosoftAjaxWebForms.debug.js (comments in code are mine):
function Sys$WebForms$PageRequestManager$_updatePanel(updatePanelElement, rendering) {
//some code not germane to this post has been omitted here
//this is where the existing content is disposed
this._destroyTree(updatePanelElement);
//and the content is updated
updatePanelElement.innerHTML = rendering;
}
function Sys$WebForms$PageRequestManager$_destroyTree(element) {
if (element.nodeType === 1) {
var childNodes = element.childNodes;
for (var i = childNodes.length - 1; i >= 0; i--) {
var node = childNodes[i];
if (node.nodeType === 1) {
//if the node has a dispose method, call it
if (node.dispose && typeof(node.dispose) === “function”) {
node.dispose();
}
//if the node has a sys.ui.control associated with it, call its dispose method
else if (node.control && typeof(node.control.dispose) === “function”) {
node.control.dispose();
}
//check for sys.ui.behaviors associated with the node and call dispose on each one
var behaviors = Sys.UI.Behavior.getBehaviors(node);
for (var j = behaviors.length - 1; j >= 0; j–) {
behaviors[j].dispose();
}
//recurse the contents of this node to do it again
this._destroyTree(node);
}
}
}
}
If you know there are no objects bound to the content of the update panel, Asp.Net AJAX In Action suggests that you can avoid the performance penalty imposed by the framework’s memory leak prevention. They suggest removing the update panel div from the DOM after the asynchronous result is returned and before the framework applies the result to the DOM. We can use the pageLoading event to do this.
''' ''' Exposes methods to increase update panel performance when the panel contains content without sys.ui.controls associated with it ''' ''' Public NotInheritable Class UpdatePanelRemoveNodeOnPostback ''' ''' returns string for script to remove the specified control on the client DOM before the page is loaded after an asyncronous postback ''' this saves the atlas framework from having to traverse the existing dom elements and remove all behaviors, thereby increasing performance ''' NOTE: THIS CAN ONLY BE USED WHEN THE CONTROL HAS NO CLIENT SIDE CONTROLS ASSOCIATED WITH IT, specifically, as is the case with some of our ''' gridviews ''' ''' ''' ''' Public Shared Function CreateScriptToRemoveExistingNodeFromUpdatePanelBeforePageLoad(ByVal control As Control) As String Return String.Format(System.Globalization.CultureInfo.InvariantCulture, Resources.UpdatePanelRemoveNodeOnPageLoading.Script, control.ClientID, control.ClientID, control.ClientID) End Function End Class
The function references a script included as a resource:
<script type="text/javascript">
var pageRequestManager = Sys.WebForms.PageRequestManager.getInstance();
pageRequestManager.add_pageLoading(onPageLoading{0});
function onPageLoading{1}(sender, e) {
var itemToRemove = $('{2}');
if (itemToRemove != null) {
itemToRemove.remove();
}
}
</script>
One common place I use this is when an update panel contains a gridview. This is an ideal candidate because the amount of html being disposed can be quite large. In the gridview’s container’s preRender event:
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs) MyBase.OnPreRender(e) If GridView.Visible Then Dim script As String = UpdatePanelRemoveNodeOnPostback.CreateScriptToRemoveExistingNodeFromUpdatePanelBeforePageLoad(GridView) Me.Page.ClientScript.RegisterStartupScript(Me.GetType, "RemoveGridViewPrePostBackLoad", script) End If End Sub
When the update panel is loaded, the javascript code will fire removing the gridview element from the DOM before the disposal code is fired.
