Fast ASP.NET web page loading by downloading multiple javascripts in batch
A web page can load a lot faster and feel faster if the javascripts on the page can be loaded after the visible content has been loaded and multiple javascripts can be batched into one download. Browsers download one external script at a time and sometimes pause rendering while a script is being downloaded and executed. This makes web pages load and render slow when there are multiple javascripts on the page. For every javascript reference, browser stops downloading and processing of any other content on the page and some browsers (like IE6) 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 http://dropthings.omaralzabir.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
As you see, browser gets stuck for 15 times as it downloads and processes external scripts. This makes 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 wastes 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 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 connection 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 can not only download multiple scripts in one shot, but also has a very short URL form. For example:
/scripts.ashx?initial=a,b,c,d,e&/
Compared to conventional ASP.NET ScriptResource URLs like:
/ScriptResource.axd?d=WzuUYZ-Ggi7-B0tkhjPDTmMmgb5FPLmciWEXQLdjNjt
bmek2jgmm3QETspZjKLvHue5em5kVYJGEuf4kofrcKNL9z6AiMhCe3SrJrcBel_c1
&t=633454272919375000
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 user a fast loading feel
- Give browser move time and free http connections to download visible artifacts of the page and thus give 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 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 tag. 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 parameter
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 are the script blocks being sent. It then parses those script blocks and find 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:
<script type="text/javascript">
...
//]]>
</script>
<script src="/Dropthings/WebResource.axd?d=_w65Lg0FVE-htJvl4_zmXw2&t=633403939286875000"
type="text/javascript"></script>
...
<script src="Widgets/FastFlickrWidget.js" type="text/javascript"></script>
<script src="Widgets/FastRssWidget.js" type="text/javascript"></script>
<script src="/Dropthings/ScriptResource.axd?d=WzuUYZ-Ggi7-B0tkhjPDTmMmgb5FPLmciWEXQLdj
Njtbmek2jgmm3QETspZjKLvHue5em5kVYJGEuf4kofrcKNL9z6AiMhCe3SrJrcBel_c1
&t=633454272919375000" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
...
</script>
<script src="/Dropthings/ScriptResource.axd?d=WzuUYZ-Ggi7-B0tkhjPDTmMmgb5FPLmciWEXQLdjNjtbmek2j
gmm3QETspZjKLvHIbaYWwsewvr_eclXZRGNKzWlaVj44lDEdg9CT2tyH-Yo9jFoQij_XIWxZNETQkZ90
&t=633454272919375000" type="text/javascript"></script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">
...
</script>
<script type="text/javascript" charset="utf-8">
...
</script>
<script src="Myframework.js" type="text/javascript"></script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">if( typeof Proxy == "undefined" ) Proxy = ProxyAsync;</script>
<script type="text/javascript">
...
</script>
<script src="/Dropthings/ScriptResource.axd?d=WzuUYZ-Ggi7-B0tkhjPDTmMmgb5FPLmciWEXQLdjN
jtbmek2jgmm3QETspZjKLvH-H5JQeA1OWzBaqnbKRQWwc2hxzZ5M8vtSrMhytbB-Oc1
&t=633454272919375000" type="text/javascript"></script>
<script src="/Dropthings/ScriptResource.axd?d=BXpG1T2rClCdn7QzWc-HrzQ2ECeqBhG6oiVakhRAk
RY6YSaFJsnzqttheoUJJXE4jMUal_1CAxRvbSZ_4_ikAw2
&t=633454540450468750" type="text/javascript"></script>
<script src="/Dropthings/ScriptResource.axd?d=BXpG1T2rClCdn7QzWc-HrzQ2ECeqBhG6oiVakhRA
kRYRhsy_ZxsfsH4NaPtFtpdDEJ8oZaV5wKE16ikC-hinpw2
&t=633454540450468750" type="text/javascript"></script>
<script src="/Dropthings/ScriptResource.axd?d=BXpG1T2rClCdn7QzWc-HrzQ2ECeqBhG6oiVakhRAk
RZbimFWogKpiYN4SVreNyf57osSvFc_f24oloxX4RTFfnfj5QsvJGQanl-pbbMbPf01
&t=633454540450468750" type="text/javascript"></script>
...
<script type="text/javascript">
...
</script>
</body>
</html>
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 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.
<script type="text/javascript">
...
</script>
<script type="text/javascript" src="Scripts.ashx?initial=a,b,c,d,e,f&/dropthings/"></script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">
...
</script>
<script type="text/javascript">if( typeof Proxy == "undefined" ) Proxy = ProxyAsync;</script>
<script type="text/javascript">
...
</script>
<script src="/Dropthings/ScriptResource.axd?d=WzuUYZ-..." type="text/javascript"></script>
<script src="/Dropthings/ScriptResource.axd?d=BXpG1T2..." type="text/javascript"></script>
<script type="text/javascript" src="Scripts.ashx?post=C,D,E,F,G,H,I,J&/dropthings/"></script>
<script type="text/javascript">
...
</script>
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.
The whole solution is explained in this CodeProject article:
Fast ASP.NET web page loading by downloading multiple javascripts after visible content and in batch
http://www.codeproject.com/KB/aspnet/fastload.aspx
You should be able to use this approach in any ASP.NET (even better if AJAX) application and give your site a big performance boost.
If you like the idea, please vote for me.
Note: The latest source code, with any bug fixes, will be available in the Dropthings source code all the time.