Scott Hanselman

ASP.NET Ajax - Script Combining and moving ScriptResource.axd's to Static Scripts

June 24, '09 Comments [25] Posted in ASP.NET | ASP.NET Ajax | Javascript
Sponsored By

I've got a little something I'm doing and I wanted to take control over some scripts that were being added by ASP.NET WebForms. Remember that ASP.NET WebForms is designed around a control/component model, so you don't get 100% control over your markup. When you drag a control onto the page in WebForms, you expect it to work.

ScriptManager Basics

For example, if I'm going to do so stuff with GridView and an UpdatePanel, I might do this:













and this will cause some Web- and ScriptResources to be added to the generated HTML of my page, something like this:

 


Basically, ScriptResource.axd?d=blob&t=timestamp...these are JavaScript files that you don't need to deploy as they live inside the assemblies. They are managed by the ScriptManager tag/control in my source above.

Overriding ScriptResource and Hosting Static JavaScript Files

However, I might want to put them in static files and manage them myself. I can override their paths like this:







This will give me HTML like this:

 


NOTE: There're a few controls that don't use the ScriptManager, so they can't have their JavaScript suppressed. So far the Validators are the main culprits. I'm talking to the team and we'll see if we can't get that fixed in 4.0.

NEW IN 4.0: In 3.5 you also can't use the ScriptManager to suppress or set the path of WebResource.axd, but in 4.0 you will be able to by using ScriptReference. WebResource.axd is for non-Ajax scripts that use the Page.ClientScript.RegisterX APIs. It'll be nice to be able to use ScriptReference as the ScriptManager is smarter and gzip compresses as well.

In .NET 4.0 using the ScriptManager to suppress both ScriptResource and WebResource will allow you to get your pages down to a single script. We're looking also at a CDN (Content Distribution Network) option to get that static script hosted elsewhere as well. I'll show Script Combining in a second.

The name="" attribute has to line up with the name of the resource the script is stored in. I used Reflector to figure them out. There's a few like MicrosoftAjaxTimer.js, MicrosoftAjax.js, MicrosoftAjaxWebForms.js in System.Web.Extensions, and DetailsView.js, Focus.js, GridView.js, Menu.js, SmartNav.js, TreeView.js, WebForms.js, WebParts.js and WebUIValidation.js in System.Web.dll.

Remember, these ARE NOT ALL NEEDED. You only want these on an as-needed basis. When a control needs one, it'll ask for it. Just do a view-source on your resulting HTML and take control of the ones you want.

ScriptCombining in 3.5 SP1

Now, if I want to combine those 3 scripts into one, I can do this:









I've wrapped the scripts in a CompositeScript control and I get a single GZipped automatically combined script. I'll save that combined script away and host it at http://www.example.com/1.js statically. Now, I'll add the path attribute:









While not a direct feature of .NET 3.5, I'm able to greatly reduce the number of scripts and take control using a few simple techniques.

ScriptManager and CDNs in .NET 4.0

In .NET 4.0 we're trying to make this more formal and possibly get the page down to a single script that's hostable on a CDN. That will probably look something like this. Just enable CDN (Content Delivery Network) and all your ASP.NET Ajax scripts will come from a CDN that you can configure in global.asax once:

Pretty slick, and nicer than my hacks. For 4.0, the goal is for this to work with ScriptResource AND WebResource making your scripts quite tidy.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Wednesday, June 24, 2009 1:57:19 AM UTC
It would be good if organizations had the option to setup their own CDN internally. I work in the health care industry and there are strict requirements that user workstation should not have access to the Internet. I would assume that <asp:ScriptManager runat="server" EnableCdn="true"/> would allow the user to specify their own CDN network??
Phil Bolduc
Wednesday, June 24, 2009 1:59:11 AM UTC
This is cool. It would also be very useful to enable having the composite script used conditionally based on the current build config (Release or Debug). That way, at dev time it is easier to debug the JS script in the browser as individual files, but at release time it all gets combined nicely. This can be achieved in code by sub-classing the ScriptManager and adding this bevhaiour but it would be nice if it did it for us.
Wednesday, June 24, 2009 2:00:05 AM UTC
Taking the CDN option one step further, wouldn't it be better for Microsoft to manage the .NET CDN and ensure everyone/everywhere always has the latest files?

We've all become pretty spoiled and lazy with the ability to add scripts like this:

<script type=”text/javascript” src=”http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js”></script>
Wednesday, June 24, 2009 3:28:59 AM UTC
Phil, yes, as I mentioned, you setup *your* CDN URLs in the Global.asax.

Mike, automatic, no, but you should be able to use whatever cloud you like in one line.
Wednesday, June 24, 2009 5:23:09 AM UTC
Actually, no need to use Reflector to figure out the names of the scripts. I wrote a control that you can add to your page and that will give you the list of script references you have on the page. It is then very easy to cut and paste that list into your script manager to make them explicit. The control even gives you links to the combined static versions of your debug and release scripts...
http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=13356
Wednesday, June 24, 2009 8:07:22 AM UTC
Meh, that's why WebForms are flawed by design. Luckily there's MVC. :)
Wednesday, June 24, 2009 9:21:56 AM UTC
So glad to see these kinds of features in ASP.NET! Could you update the AJAX Control Toolkit too to use these features instead of the custom (and incompatible) ToolkitScriptManager? Thanks!
Joe Chung
Wednesday, June 24, 2009 10:19:50 AM UTC
Thanks for explaining this, and your contribution in approvements.
Allowing Script- and WebResource to be moved to Static Pages will for sure have a nice inpact on our security, as
we are using a third-party security tool to protect our servers from a.o. SQL injection attacks.
One common request the tool tends to block is request parameters containing double-dashes. So we had to add a rule to make sure Script- and WebResource would never be blocked.


Amph
Wednesday, June 24, 2009 1:28:54 PM UTC
Is it possible to have ScriptManager combine all the ScriptResource.axd scripts without moving them to static files? I don't want to manually manage all the script files, but I still want the performance improvement of only one request.
Wednesday, June 24, 2009 1:37:45 PM UTC
I still think that the biggest problem with ScriptManager and Page.ClientScript is that they rely on a server form. If you don't have a server form on your page, you won't get an error, but you won't get your scripts either.

You can include the scripts manually, but you might end up with dozens of references to the same script file scattered throughout the page. You can't use script combining or zipping, and you can't use app-relative paths (~/scripts/1.js) unless you opt for the in-line code block (src="<%= ResolveClientUrl("~/scripts/1.js") %>"), which can cause other problems.

This problem is not limited to MVC - I have hundreds of web-forms pages which don't have a server form, either because I don't need a form at all, or because I'm using my own custom client form control.


I think the following would make sense, at least for web-forms pages:

1. If the page has a server form, output the script in the same way as v1/2/3.5;

2. If the page has a ScriptPlaceholder control (which should be a singleton, like the ScriptManager), output all script within that control;

3. If the page has a body tag with runat="server", output all script before the closing tag;

4. As a final fall-back, if the page has a head tag with runat="server", output the script before the closing tag.
Richard
Wednesday, June 24, 2009 3:25:35 PM UTC
Thanks for the info. Debugging the built-in scripts has been a royal pain.
PRMan
Wednesday, June 24, 2009 3:50:43 PM UTC
I agree with Damian above. I've been pulling my hair out for a day now trying to get a Composite version of my *minified* script, but only when it's in release mode. Does anyone have any suggestions?

Also, what exactly does ScriptMode *do* ? I changed it from Debug to Release and viewed the resulting javascript in IE8's Developer Tools -- it looks the same to me. I read somewhere that if ScriptMode="Release" it would remove all whitespace (which would be enough minification for me). Is that true? If so, I must be doing something wrong.
Wednesday, June 24, 2009 4:46:43 PM UTC
@Joe: you don't need to use the Toolkit's script manager. Actually, I'd recommend not using it. You can use the .NET script manager (including script combination) with the Toolkit.

@Hitchhiker: sure, if you omit the path attribute, the script manager will combine the scripts for you. There is a limit in the number of scripts you can combine this way though.

@Dave: ScriptMode enables you to choose among the available scripts. It does not modify the scripts. The minimization must be done at build time, not at runtime. If you embed a ".debug.js" resource (or include a ".debug.js" file next to your release ".js"), the script manager will pick the right version.
Wednesday, June 24, 2009 5:41:56 PM UTC
@Bertrand: If I have the .debug.js file in Solution Explorer, does the minified .js file also have to be there (in Solution Explorer, I mean)? I have a Post Build Event that's minifying the .debug.js files and producing the release .js files, but I don't really want to include the minified versions in the solution itself.

Also, I must be doing something wrong, as it doesn't seem to be picking up the .debug.js version of my script:

<CompositeScript ScriptMode="Debug">
<Scripts>
<asp:ScriptReference Path="~/js/TreeView.js" />
... more scripts ...
</Scripts>
</CompositeScript>

I have both TreeView.js (minified) and TreeView.debug.js in the same folder, and it keeps grabbing the minified one, no matter what I do, whether I'm debugging or not.

So that should grab TreeView.debug.js version, right? It doesn't appear to be doing that. :-(
Wednesday, June 24, 2009 6:23:11 PM UTC
Okay, here's what I've figured out. I have to set ScriptMode="Debug" specifically on a ScriptReference in order to get it to use the ".debug.js" version. I can't set it on the <CompositeScript> tag.

<asp:ScriptReference Path="~/js/TreeView.js" ScriptMode="Debug" />

That's the only way it will work for me. If I set ScriptMode="Auto", it renders the minified ".js" version, even if I'm debugging. I have to explicitly set ScriptMode="Debug".

I'm afraid I will forget to change that back to "Release" when the time comes to deploy it. I really wish it was more automatic.
Wednesday, June 24, 2009 7:21:33 PM UTC
Dave, note this from the docs:

Auto - The Web site uses the debug version of client script libraries when the retail attribute of the deployment Element (ASP.NET Settings Schema) configuration element is set to false. Otherwise, the Web site uses the release version of client script libraries

Wednesday, June 24, 2009 7:39:31 PM UTC
In my web.config and machine.config, I have retail="false", yet it continues to use the release versions. A file-based ScriptReference seems to always pickup the release version *unless* you explicitly specify ScriptMode="Debug" on the ScriptReference itself.
Wednesday, June 24, 2009 9:28:27 PM UTC
@Dave: for resource-based scripts, we can inspect the assembly and detect debug and release versions. For path-based resources, we can't do that (the file might even be on a different server for all we know). This is why the ScriptMode=Auto setting is equivalent in that case to Release. If you do know that there is a debug version, you can use ScriptMode=Inherit on the script reference. This will inherit the value from the script manager.
Thursday, June 25, 2009 3:17:51 PM UTC
I think one more important issue you guys have missed is able to put the ScriptManager at the bottom of the page which is required to make a fast responsive page and as much important as the script combining.
Thursday, June 25, 2009 5:56:12 PM UTC
Scott,

Will ScriptManager work with asp.net mvc (i've yet dare to try it, mostly from lack of time)? and what about combining css files in the header?

I know this is slightly off topic but it would be cool to see a generic CDN solution that handled js and css files along with versioning similar to how Yahoo handles different versions of YUI.
Friday, June 26, 2009 3:01:23 PM UTC
Scott,

I have been looking at many different ways to do this whole combine/minify/compress/gzip thing and there doesn't seem to be a good way to do everything with one technique that works.

At first, I thought the best way to do it was to make an HTTP Handler (.ashx file) to take in all the scripts from a web.config file and do it's thing to make one script as seen here http://www.codeproject.com/KB/aspnet/HttpCombine.aspx

But when I talked it over with my co-worker, he said that doesn't work well with our control pattern. In our control pattern, we have a user control that has server side code and front end JS code, and we want the ascx page to control when the JS code for that control is loaded on the page. We basically want the control to dictate when it needs the JS instead of just including all of our JS on the page even when it's not being used because that control isn't even present.

Then, I found this article (http://madskristensen.net/post/Optimize-WebResourceaxd-and-ScriptResourceaxd.aspx) about combining axd files together. This seems like the best idea.

After all that, this is my question...Is there a way to write an HTTPHandler and HTTPModule to basically scan the source code for all script tags with js (or .axd) in them and combine/minify/compress them and display only one script tag to the browser? This would basically allow the dev environment to have everything look normal and when the page is rendered, it finds all the .js, .axd, etc files and combines them into one.

The other main problem I usually run into when I use these techniques is errors that involve something like "<object> not defined" because the JS needs to load in a specific order and it's not there.

Is it possible to make something like this that isn't buggy?
Wednesday, July 01, 2009 1:29:04 AM UTC
Takes too much time. You can do this much simpler. See this post...

http://dotnetrush.blogspot.com/2008/07/combine-minify-compress-and-cache.html
Wednesday, July 15, 2009 8:03:55 PM UTC
Is there any way to conditionally not load the script manager scripts? The only AJAX on the page is in admin mode, so for the public view, I'd rather not load any of those extraneous scripts. I tried the obvious,wrapping the scriptmanager tag in a panel and changing its visibility based on whether or not the user was an admin and that did nothing.

Any ideas would be appreciated!

Thanks!
Thomas L
Tuesday, July 28, 2009 4:41:45 AM UTC
Nice article
Martin
Tuesday, July 28, 2009 4:45:58 AM UTC
Very good article
jayaraj
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.