The blog of dlaa.me

Script combining made easy [Overview of the AJAX Control Toolkit's ToolkitScriptManager]

Note: Recent versions of ASP.NET AJAX include the ScriptManager.CompositeScript property which performs much the same functionality as discussed here. More information is available in the article Combining Client Scripts into a Composite Script.

 

Introduction

The 10606 release of the AJAX Control Toolkit includes ToolkitScriptManager, a new class that derives from the ASP.NET AJAX ScriptManager and performs automatic script combining.

What is meant by "script combining" and why is it desirable?

ASP.NET AJAX Behaviors are typically implemented by JavaScript (JS) files that are downloaded by the browser as part of the web page's content/resources (like CSS files, images, etc.). Each JS file typically contains a single Behavior, so if a web page uses lots of Behaviors, it's going to download lots of Behavior JS files. The cost to download a single JS file is fairly minimal, but when there are many of them on a page, the serialized nature of Behaviors (later ones may depend on earlier ones and can't be loaded until the earlier ones have finished) means that it may take a bit of time for all the JS a page needs to download to the user's browser. The time in question is typically on the order of milliseconds, but every little bit helps when you're looking to give users the best possible experience!

Script combining is beneficial because fewer JS files means fewer request/response operations by the browser - which translates directly into quicker page load times for users and less load on the web server. Furthermore, there will be less network traffic because the HTTP headers associated with each unnecessary request/response operation don't need to be transmitted (saving around 750 bytes for each combined script).

At the extreme, one could combine all (~40) the Behaviors in the Toolkit into a single, monolithic JS file (either manually or as part of the build process) and always send that file to the browser. While there are certain benefits to this, we chose not to do so for two main reasons: 1) the ASP.NET AJAX framework the Toolkit builds upon is an object-oriented framework and it's beneficial to maintain the mental/physical separation that comes from keeping each behavior isolated and 2) any page that used any part of the Toolkit would be forced to download the entire set of scripts in the Toolkit.

ToolkitScriptManager gives us the best of both worlds by combining exactly the relevant JS files used by each page into one file so the browser downloads only what's necessary for each page. ToolkitScriptManager's script combining is more than a simple concatenation of all the script files in the Toolkit; it's a dynamic merge of only the scripts that are actually being used by a page each time it's loaded by the browser. If a page has an ASP.NET AJAX UpdatePanel on it and additional scripts need to be sent as part of an async postback, then ToolkitScriptManager will automatically generate a combined script file containing only those scripts that the browser hasn't already downloaded.

ToolkitScriptManager automatically compresses the combined script file if the browser indicates it supports compression - achieving slightly better compression in the process because most adaptive dictionary-based compression techniques (like HTTP's GZIP and Deflate) tend to compress data better in one chunk than in multiple chunks (because the dictionary doesn't keep getting reset). The combined script file is cached and reused by the browser just like the corresponding script files would have been if ToolkitScriptManager weren't being used.

Basically, script combining effortlessly creates faster loading web pages - and happier users by extension!

How do I use it?

Simple: Just replace <asp:ScriptManager ... /> with <ajaxToolkit:ToolkitScriptManager ... /> in your ASPX page and you're done! (Of course, if you're not using the default namespaces "asp" and "ajaxToolkit", you'll need to substitute your own namespaces.) The scripts in the AJAX Control Toolkit are already enabled for combining, so it's really that easy!

ToolkitScriptManager derives from ScriptManager, so it can be substituted trivially. Configuration-wise, it exposes a single new property beyond what ScriptManager already has: bool CombineScripts. The default value of "true" means that scripts will be automatically combined - specifying the value "false" disables the combining. (Alternatively, just switch back to ScriptManager for the same result.)

How do I enable combining for my custom Behavior's scripts?

As a security precaution to prevent malicious users from taking advantage of ToolkitScriptManager to access embedded resources from unrelated DLLs, the new assembly-level ScriptCombine attribute must be used to indicate that a particular assembly/script is allowed to take part in the script combining process. By default, none of the scripts in an assembly without the ScriptCombine attribute will take part in script combining. Adding the ScriptCombine attribute to an assembly indicates that all of its scripts can take part in script combining and ToolkitScriptManager will automatically include the relevant ones when generating a combined script file. For finer control over individual scripts in an assembly, the ScriptCombine attribute exposes an ExcludeScripts property and an IncludeScripts property - both are comma-delimited lists of individual script files. As stated, when neither property is specified, the default behavior is that all scripts are combinable. Once the IncludeScripts property is set, only the scripts explicitly specified by it are combinable. The ExcludeScripts property excludes any listed scripts from combining (whether or not IncludeScripts is set). Of course, most people will simply add the ScriptCombine attribute to their assembly and the default behavior does what they want. (As mentioned above, Toolkit scripts are already enabled for script combining.)

How do scripts actually get combined? [Technical]

Script combining is a two-stage process.

The first stage takes place during the normal ASP.NET page lifecycle when ToolkitScriptManager overrides ScriptManager's OnLoad method to initialize its state and its OnResolveScriptReference method to find out when script references are being resolved. The initialization code in OnLoad consists of adding a HiddenField to the page and using it to track which scripts have already been loaded by the browser. The handling of OnResolveScriptReferences is a little more involved: the script reference is checked for combinability (i.e., does the assembly's ScriptCombine attribute allow the script to take part in script combining) and the script reference is changed to point to the URL of a combined script file. In this manner, all scripts that are part of the same combined script file get the same URL and the ScriptManager class outputs that URL to the page exactly once. Notably, because scripts may have a strict ordering, the presence of an uncombinable script in the middle of combinable scripts will result in two combined script files being generated (the first consisting of the scripts coming before the uncombinable script and the second consisting of the scripts after it).

The URL of the combined script file is currently of the form: .../Page.aspx?_scriptcombiner_=;Assembly1.dll Version=1:MVID1:Script.Name.1.js:Script.Name.2.js;Assembly2.dll Version=2:MVID1:Script.Name.3.js. What this means is that the ASPX page itself is referenced with the special request parameter "_scriptcombiner_" and a semicolon-delimited list of assemblies with a colon-delimited list of the required scripts from each of them. The strong name of the assembly is used to avoid potential confusion if multiple versions of an assembly are present and the ModuleVersionID (MVID) is used to ensure that any changes to the assembly itself automatically invalidate all combined script files that reference it. In this manner, recompiling one of the assemblies contributing to a combined script file will cause the new scripts to be downloaded by the browser next time the page is loaded.

The second stage of script combining takes place when the page is referenced with the "_scriptcombiner_" request parameter. ToolkitScriptManager overrides OnInit (one of the first parts of the page lifecycle) and uses that opportunity to generate the combined script file based on the value of the request parameter, outputs the combined script file to the browser, and stops further processing of the page lifecycle. Of note, the cache settings of the combined script file are set to the same values that the individual script files would have had if ToolkitScriptManager weren't being used and the combined script file is automatically compressed according to the browser's wishes. Similarly, any localized script resources for a script file that is in the process of being combined are loaded and sent to the browser as part of the combined script file. After all combined scripts are output, ToolkitScriptManager appends a small bit of script to the end of the file to update the page's HiddenField with the scripts that have just been added. In this manner, any additional scripts added during an async postback are automatically tracked by the page and subsequent async postbacks will know exactly which scripts have already been loaded by the browser.

Why piggyback a request parameter on the same page instead of using an IHttpHandler? [Technical]

ScriptManager uses ScriptResourceHandler (an IHttpHandler) to serve (uncombined) scripts, so it's natural to wonder why ToolkitScriptManager wouldn't do the same. The reason is that the AjaxControlToolkit DLL is often run in partial trust scenarios where it couldn't add such a handler to the system itself - and because we don't want people to have to modify their web.config file just to enable script combining. By making use of the same page for serving combined script files, ToolkitScriptManager offers a seamless experience that's simple to configure, simple to manage, and that works even for folks who don't have control over the web server that's hosting their content.

Are there any tradeoffs when switching to ToolkitScriptManager?

There are no significant tradeoffs that we know of, but there are a couple of implications it's good to be aware of. For one, the current combined script URL format is currently pretty verbose and can lead to unusually long URLs. While this hasn't been a problem so far, it will be easy to change the format in the future (with no impact to users) and we're already considering ways of doing so. Another thing to be aware of is that reusing the page to serve the combined script file means that there is some additional server processing that happens before/during the OnInit stage of the page lifecycle when processing a combined script file. (Though the additional work here is offset by the savings of not having to serve multiple JS files.) Again, this hasn't been an issue, but it's something to keep in mind if things behave differently after adding ToolkitScriptManager to a page.

So just how risky is it to switch to ToolkitScriptManager?

That's a loaded question [ :) ], but it's informative to note that all AJAX Control Toolkit sample pages (including the sample web site, automated tests, manual tests, etc.) have been converted over to use ToolkitScriptManager with only one issue: The Slider's SliderBehavior.js script uses a fairly obscure feature enabled by the PerformSubstitution property of the WebResource attribute that allows <%= WebResource/ScriptResource %> tags to be embedded in JS files and get resolved before the script is sent to the browser. This behavior isn't currently supported by ToolkitScriptManager (it will throw an informative Exception if it detects the presence of this construct), so the ExcludeScripts property of the ScriptCombine attribute on the AjaxControlToolkit DLL has been used to exclude the SliderBehavior.js file from being combined.

Summary

ToolkitScriptManager works seamlessly in every page of the AJAX Control Toolkit, so we encourage folks to give it a try if they're interested in the benefits it offers! As always, if you encounter any problems, please let us know by posting a detailed description of the problem to the AJAX Control Toolkit support forum.

Happy script combining!!