Updates

Introduced a new attribute for script tags to pin a script block and not allow it to be moved to the bottom

Pin attribute format changed

Introduction

A Web page can load a lot faster and feel faster if the JavaScript files referenced on the page can be loaded after the visible content has been loaded and multiple JavaScript files can be batched into one download. Browsers download one external JavaScript at a time and sometimes pause rendering while a script is being downloaded and executed. This makes Web pages load and render slowly when there are multiple external JavaScript references on the page. For every JavaScript reference, the browser stops downloading and processing of any other content on the page and some browsers (like Internet Explorer 6) pause rendering while it processes the JavaScript. This gives a slow loading experience and the Web page kind of gets 'stuck' frequently. As a result, a Web page can only load fast when there are small number of external scripts on the page and the scripts are loaded after the visible content of the page has loaded.

Here's an example, when you visit Dropthings.com, you see a lot of JavaScripts downloading. Majority of these are from the ASP.NET AJAX Framework and the ASP.NET AJAX Control Toolkit project.

Figure: Many scripts downloaded on a typical ASP.NET AJAX page having ASP.NET AJAX Control Toolkit.

There are a total of 15 JavaScript references on the page. As you see, the browser gets stuck 15 times as it downloads and processes all these external scripts. This makes the page loading "feel" slower. The actual loading time is also pretty bad because these 15 HTTP requests waste 15*100ms = 1500ms on the network latency inside USA. Outside USA, the latency is even higher. Asia gets about 270ms and Australia gets about 380ms latency from any server in USA. So, users outside USA waste 4 to 6 seconds on network latency where no data is being downloaded. This is an unacceptable performance for any website.

You pay for such a high number of script downloads only because you use two extenders from AJAX Control Toolkit and the UpdatePanel of ASP.NET AJAX.

If we can batch the multiple individual script calls into one call like Scripts.ashx as shown in the picture below and download several scripts together in one shot using an HTTP Handler, it saves us a lot of HTTP connections which could be spent doing other valuable work like downloading CSS for the page to show content properly or downloading images on the page that is visible to user.

Figure: Download several JavaScripts over one connection and save call and latency.

The Scripts.ashx handler cannot only download multiple scripts in one shot, but also has a very short URL form. For example:

The benefits of downloading multiple JavaScript over one HTTP call are:

Saves expensive network roundtrip latency where neither browser nor the origin server is doing anything, not even a single byte is being transmitted during the latency.

Create less "pause" moments for the browser. So, browser can fluently render the content of the page and thus give the user a fast loading feel.

Give browser move time and free HTTP connections to download visible artifacts of the page and thus give the user a "something's happening" feel.

When IIS compression is enabled, the total size of individually compressed files is greater than multiple files compressed after they are combined. This is because each compressed byte stream has a compression header in order to decompress the content.

This reduces the size of the page HTML as there are only a few handful of script tags. So, you can easily saves hundreds of bytes from the page HTML, especially when ASP.NET AJAX produces gigantic WebResource.axd and ScriptResource.axd URLs that have very large query parameters.

The solution is to dynamically parse the response of a page before it is sent to the browser and find out what script references are being sent to the browser. I have built an HTTP module which can parse the generated HTML of a page and find out what the script blocks being sent are. It then parses those script blocks and finds the scripts that can be combined. Then it takes out those individual script tags from the response and adds one script tag that generates the combined response of multiple script tags.

For example, the homepage of Dropthings.com produces the following script tags:

As you see, there are lots of large script tags, in total 15 of them. The solution I will show here will combine the script links and replace it with two script links that download 13 of the individual scripts. I have left two scripts out that are related to ASP.NET AJAX Timer extender.

As you see, 13 of the script links have been combined into two script links. The URL is also smaller than majority of the script references.

There are two steps involved here:

Find out all the script tags being emitted inside generated response HTML and collect them in a buffer. Move them after the visible artifacts in the HTML, especially the <form> tag that contains the generated output of all ASP.NET controls on the page

Parse the buffer and see which script references can be combined into one set. The sets are defined in a configuration file. Replace the individual script references with the combined set reference.

Step 1: Defer All Script Tags After Body Content

ASP.NET ScriptManager control has a property LoadScriptsBeforeUI, when set to false, should load all AJAX framework scripts after the content of the page. But it does not effectively push down all scripts after the content. Some framework scripts, extender scripts and other scripts registered by Ajax Control Toolkit still load before the page content loads. The following screen taken from www.dropthings.com shows several script tags are still added at the beginning of <form> which forces them to download first before the page content is loaded and displayed on the page. Script tags pause rendering on several browsers especially in Internet Explorer until the scripts download and execute. As a result, it gives user a slow loading impression as user stares at a white screen for some time until the scripts before the content download and execute completely. If browser could render the HTML before it downloads any script, user would see the page content immediately after visiting the site and not see a white screen. This will give user an impression that the Web site is blazingly fast (just like Google homepage) because user will ideally see the page content, if it's not too large, immediately after hitting the URL.

Figure: Script blocks being delivered before the content.

From the above screen shot, you see that there are some scripts from ASP.NET AJAX Framework and some scripts from Ajax Control Toolkit that are added before the content of the page. Until these scripts download, browsers don't see anything on the UI and thus you get a pause in rendering giving the user a slow load feeling. Each script to external URL adds about 200ms average network roundtrip delay outside USA while it tries to fetch the script. So, the user basically stares at a white screen for at least 1.5 sec no matter how fast an Internet connection he/she has.

These scripts are rendered at the beginning of form tag because they are registered using Page.ClientScript.RegisterClientScriptBlock. Inside the Page class of System.Web, there's a method BeginFormRender which renders the client script blocks immediately after the form tag.

Here you see that several script blocks including scripts registered by calling ClientScript.RegisterClientScriptBlock are rendered right after form tag starts.

There's no easy work around to override the BeginFormRender method and defer rendering of these scripts. These rendering functions are buried inside System.Web and none of these are overridable. So, the only solution seems to be using a Response Filter to capture the HTML being written and suppress rendering the script blocks until it's the end of the body tag. When the </body> tag is about to be rendered, we can safely assume page content has been successfully delivered and now all suppressed script blocks can be rendered at once.

In ASP.NET 2.0, you need to create a Response Filter which is an implementation of a Stream. You can replace default Response.Filter with your own stream and then ASP.NET will use your filter to write the final rendered HTML. When Response.Write is called or Page's Render method fires, the response is written to the output stream via the filter. So, you can intercept every byte that's going to be sent to the client (browser) and modify it the way you like. Response Filters can be used in a variety of ways to optimize Page output like stripping off all white spaces or doing some formatting on the generated content, or manipulating the characters being sent to the browser and so on.

I have created a Response filter which captures all characters being sent to the browser. If it finds that script blocks are being rendered, instead of rendering it to the Response.OutputStream, it will extract the script blocks out of the buffer being written and render the rest of the content. It stores all script blocks, both internal and external, in a string buffer. When it detects </body> tag is about to be written to the response, it flushes all the captured script blocks from the string buffer.

Here's the beginning of the Filter class. When it initializes, it takes the original Response Filter. Then it overrides the Write method of the Stream so that it can capture the buffers being written and do its own processing.

There are several situations to consider here. The Write method is called several times during the Page render process because the generated HTML can be quite big. So, it will contain partial HTML. So, it's possible the first Write call contains a start of a script block, but no ending script tag. The following Write call may or may not have the ending script block. So, we need to preserve state to make sure we don't overlook any script block. Each Write call can have several script blocks in the buffer as well. It can also have no script block and only page content.

The idea here is to go through each character and see if there's any starting script tag. If there is, remember the start position of the script tag. If script end tag is found within the buffer, then extract out the whole script block from the buffer and render the remaining HTML. If there's no ending tag found, but a script tag did start within the buffer, then suppress output and capture the remaining content within the script buffer so that the next call to Write method can grab the remaining script and extract it out from the output.

There are four other privatefunctions that are basically helper functions and do not do anything interesting. However, the RenderAllScriptBlocks() function combines all the script tags using a configuration setting and emits one script tag for a group of script tags. This trick is explained later in step 2.

The isScriptTag and isBodyTag functions may look weird. The reason for such weird code is pure performance. Instead of doing fancy checks like taking a part of the array out and doing stringcomparison, this is the fastest way of doing the check. Best thing about .NET IL is that it's optimized, if any of the conditions in the && pairs fail, it won't even execute the rest. So, this is as best as it can get to check for certain characters.

There are some corner cases that are also handled here. For example, what if the buffer contains a partial script tag declaration. For example, "....<scr" and that's it. The remaining characters did not finish in the buffer instead the next buffer is sent with the remaining characters like "ipt src="...">.....</scrip". In such a case, the script tag won't be taken out. One way to handle this is to make sure there are enough characters left in the buffer to do a complete tag name check. If not found, store the half finished buffer somewhere and on the next call to Write, combine it with the new buffer sent and do the processing.

In order to install the Filter, you need to hook it in the Global.asax BeginRequest or some other event that's fired before the Response is generated.

Here, I am hooking the Filter only for GET calls to .aspx pages. You can hook it to POST calls as well. But asynchronous postbacks are regular POST and I do not want to do any change in the generated JSON or HTML fragment. Another way is to hook the filter only is when ContentType is text/html.

When this filter is installed, www.dropthings.com defers all script loading after the <form> tag completes.

You can grab the Filter class from the App_Code\ScriptDeferFilter.cs of the Dropthings project besides the source code attachment with this article. Go to CodePlex site and download the latest code of Dropthings for the latest filter. I will keep fixing stuff and make modifications directly on the Dropthings code base.

This filter collects all the script tags. So, we can easily parse the script tags and combine them. The next step is to combine the script tags.

Step 2: Combine Multiple Scripts Tags into One Script Tag

First we need to collect the script tags that will be combined and emitted as one. You cannot just combine all scripts on your page because sometimes a couple of scripts are downloaded and then some inline script blocks need those scripts. Then again couple of scripts are downloaded and then some other inline script blocks use them. In a typical ASP.NET AJAX page, first the ASP.NET AJAX framework, UpdatePanel script and some Extender scripts are downloaded. Then the Web service proxy, postback code and some other inline script tags follow. These script tags need the earlier framework scripts to be available. So, you need to first group those framework script tags into one set. Here's how I did it:

How do I know that these are the script blocks that can be downloaded in one batch? I look at the generated source code and find the script tags. I copy the src="...." and paste it in the XML. While pasting, I XML Encode the URLs which means all & in the URL gets converted to &amp;

I found that these are the script blocks that are loaded before any inline script tag is rendered. So, these are candidates for one batch download.

The next candidates are the ASP.NET AJAX extenders and some ASP.NET AJAX Control Toolkit extenders.

Here you see these scripts that can be downloaded in another batch. After these tags, I found another inline script block expecting these scripts to be available.

One thing to remember here, if your website is running under a virtual directory, all these URLs will have the virtual directory name in front of them. For example, /Dropthings/ScriptResource.axd?....

Also the URL names are case sensitive. Lastly, you will notice all ScriptResource.axd URLs have a parameter t. This parameter comes after &amp; in the URL. When you XML Encode the whole URL, it will become "&amp;amp;t=....". Do not be afraid with the double encoding. That's how it should be.

Here's how the combining works:

Select a set, e.g. "Initial" and see if there's any script tag having one of the URLs defined in the set.

If a URL is found, remember the position. Because this is the position for the combined script tag.

Remove the matched URL and remember the name of the matched URL e.g. D

Once all the URLs defined in the set are searched for, go back to the position where the first match was found. Generate a combined script tag URL. E.g. Scripts.ashx?initial=a,c,f

Next is the HTTP handler that does the work of combining multiple scripts into one. The handler Scripts.ashx looks at the query parameter and sees which set and what URLs in the sets are requested. It then internally downloads the scripts using HttpWebRequest. When all scripts are downloaded, it combines them into one giant stringand emits it to the response.

The handler generates a proper cache header for the browser to cache the generated script for 30 days so that on repeated visits, the scripts are not requested over and over again. Moreover, it internally caches the combined scripts so that it does not need to fetch all those individual scripts repeatedly.

Comments and Discussions

If I have a web form which includes a TextBox and the Ajax CalendarExtender control, then when the form is loaded there are 13 requests (2 WebResources and 11 ScriptResources).

However, if I click on the TextBox and the CalendarExtender displays then there are 15 requests to the server (4 WebResources and 11 ScriptResources). By viewing source, still there are 13 WebResources and ScriptResources.

In addition to the above, I downloaded the update code from Dropthing and couldn't implement if I have 2 identical default.aspx files (one in the root folder and the other in 'abc' folder). I tried to change the script path in the abc/default.aspx as: