Fast page loading by moving ASP.NET AJAX scripts after visible 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 IE 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 website 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 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, browser don't see anything on the UI and thus you get a pause in rendering giving user a slow load feeling. Each script to external URL adds about 200ms avg network roundtrip delay outside USA while it tries to fetch the script. So, user basically stares at a white screen for at least 1.5 sec no matter how fast internet connection he/she has.
These scripts are rendered at the beginning of form tag because they are registered using Page.ClientScript.RegisterClientScriptBlock. Inside Page class of System.Web, there's a method BeginFormRender which renders the client script blocks immediately after the form tag.
1: internal void BeginFormRender(HtmlTextWriter writer, string formUniqueID)
2: {
3: ...
4: this.ClientScript.RenderHiddenFields(writer);
5: this.RenderViewStateFields(writer);
6: ...
7: if (this.ClientSupportsJavaScript)
8: {
9: ...
10: if (this._fRequirePostBackScript)
11: {
12: this.RenderPostBackScript(writer, formUniqueID);
13: }
14: if (this._fRequireWebFormsScript)
15: {
16: this.RenderWebFormsScript(writer);
17: }
18: }
19: this.ClientScript.RenderClientScriptBlocks(writer);
20: }
Figure: Decompiled code from System.Web.Page class
Here you see 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 to create 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 variety 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. It 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.
1: public class ScriptDeferFilter : Stream
2: {
3: Stream responseStream;
4: long position;
5:
6: /// <summary>
7: /// When this is true, script blocks are suppressed and captured for
8: /// later rendering
9: /// </summary>
10: bool captureScripts;
11:
12: /// <summary>
13: /// Holds all script blocks that are injected by the controls
14: /// The script blocks will be moved after the form tag renders
15: /// </summary>
16: StringBuilder scriptBlocks;
17:
18: Encoding encoding;
19:
20: public ScriptDeferFilter(Stream inputStream, HttpResponse response)
21: {
22: this.encoding = response.Output.Encoding;
23: this.responseStream = response.Filter;
24:
25: this.scriptBlocks = new StringBuilder(5000);
26: // When this is on, script blocks are captured and not written to output
27: this.captureScripts = true;
28: }
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 it's own processing.
1: public override void Write(byte[] buffer, int offset, int count)
2: {
3: // If we are not capturing script blocks anymore, just redirect to response stream
4: if (!this.captureScripts)
5: {
6: this.responseStream.Write(buffer, offset, count);
7: return;
8: }
9:
10: /*
11: * Script and HTML can be in one of the following combinations in the specified buffer:
12: * .....<script ....>.....</script>.....
13: * <script ....>.....</script>.....
14: * <script ....>.....</script>
15: * <script ....>.....</script> .....
16: * ....<script ....>.....
17: * <script ....>.....
18: * .....</script>.....
19: * .....</script>
20: * <script>.....
21: * .... </script>
22: * ......
23: * Here, "...." means html content between and outside script tags
24: */
25:
26: char[] content = this.encoding.GetChars(buffer, offset, count);
27:
28: int scriptTagStart = 0;
29: int lastScriptTagEnd = 0;
30: bool scriptTagStarted = false;
31:
32: for (int pos = 0; pos < content.Length; pos++)
33: {
34: // See if tag start
35: char c = content[pos];
36: if (c == '<')
37: {
38: int tagStart = pos;
39: // Check if it's a tag ending
40: if (content[pos+1] == '/')
41: {
42: pos+=2; // go past the </
43:
44: // See if script tag is ending
45: if (isScriptTag(content, pos))
46: {
47: /// Script tag just ended. Get the whole script
48: /// and store in buffer
49: pos = pos + "script>".Length;
50: scriptBlocks.Append(content, scriptTagStart, pos - scriptTagStart);
51: scriptBlocks.Append(Environment.NewLine);
52: lastScriptTagEnd = pos;
53:
54: scriptTagStarted = false;
55: continue;
56: }
57: else if (isBodyTag(content, pos))
58: {
59: /// body tag has just end. Time for rendering all the script
60: /// blocks we have suppressed so far and stop capturing script blocks
61:
62: if (this.scriptBlocks.Length > 0)
63: {
64: // Render all pending html output till now
65: this.WriteOutput(content, lastScriptTagEnd, tagStart - lastScriptTagEnd);
66:
67: // Render the script blocks
68: byte[] scriptBytes = this.encoding.GetBytes(this.scriptBlocks.ToString());
69: this.responseStream.Write(scriptBytes, 0, scriptBytes.Length);
70:
71: // Stop capturing for script blocks
72: this.captureScripts = false;
73:
74: // Write from the body tag start to the end of the inut buffer and return
75: // from the function. We are done.
76: this.WriteOutput(content, tagStart, content.Length - tagStart);
77: return;
78: }
79: }
80: else
81: {
82: // some other tag's closing. safely skip one character as smallest
83: // html tag is one character e.g. <b>. just an optimization to save one loop
84: pos++;
85: }
86: }
87: else
88: {
89: if (isScriptTag(content, pos+1))
90: {
91: /// Script tag started. Record the position as we will
92: /// capture the whole script tag including its content
93: /// and store in an internal buffer.
94: scriptTagStart = pos;
95:
96: // Write html content since last script tag closing upto this script tag
97: this.WriteOutput(content, lastScriptTagEnd, scriptTagStart - lastScriptTagEnd);
98:
99: // Skip the tag start to save some loops
100: pos += "<script".Length;
101:
102: scriptTagStarted = true;
103: }
104: else
105: {
106: // some other tag started
107: // safely skip 2 character because the smallest tag is one character e.g. <b>
108: // just an optimization to eliminate one loop
109: pos++;
110: }
111: }
112: }
113: }
114:
115: // If a script tag is partially sent to buffer, then the remaining content
116: // is part of the last script block
117: if (scriptTagStarted)
118: {
119:
120: this.scriptBlocks.Append(content, scriptTagStart, content.Length - scriptTagStart);
121: }
122: else
123: {
124: /// Render the characters since the last script tag ending
125: this.WriteOutput(content, lastScriptTagEnd, content.Length - lastScriptTagEnd);
126: }
127: }
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 block 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 next call to Write method can grab the remaining script and extract it out from the output.
There are two other private functions that are basically helper functions and does not do anything interesting:
1: private void WriteOutput(char[] content, int pos, int length)
2: {
3: if (length == 0) return;
4:
5: byte[] buffer = this.encoding.GetBytes(content, pos, length);
6: this.responseStream.Write(buffer, 0, buffer.Length);
7: }
8:
9: private bool isScriptTag(char[] content, int pos)
10: {
11: if (pos + 5 < content.Length)
12: return ((content[pos] == 's' || content[pos] == 'S')
13: && (content[pos + 1] == 'c' || content[pos + 1] == 'C')
14: && (content[pos + 2] == 'r' || content[pos + 2] == 'R')
15: && (content[pos + 3] == 'i' || content[pos + 3] == 'I')
16: && (content[pos + 4] == 'p' || content[pos + 4] == 'P')
17: && (content[pos + 5] == 't' || content[pos + 5] == 'T'));
18: else
19: return false;
20:
21: }
22:
23: private bool isBodyTag(char[] content, int pos)
24: {
25: if (pos + 3 < content.Length)
26: return ((content[pos] == 'b' || content[pos] == 'B')
27: && (content[pos + 1] == 'o' || content[pos + 1] == 'O')
28: && (content[pos + 2] == 'd' || content[pos + 2] == 'D')
29: && (content[pos + 3] == 'y' || content[pos + 3] == 'Y'));
30: else
31: return false;
32: }
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 string comparison, this is the fastest way of doing the check. Best thing about .NET IL is that it's optimized, if any of the condition 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 not 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 next buffer is sent with the remaining characters like "ipt src="..." >.....</scrip". In such case, the script tag won't be taken out. One way to handle this would be to make sure you always have enough characters left in the buffer to do a complete tag name check. If not found, store the half finished buffer somewhere and on 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 in the Global.asax BeginRequest or some other event that's fired before the Response is generated.
1: protected void Application_BeginRequest(object sender, EventArgs e)
2: {
3: if (Request.HttpMethod == "GET")
4: {
5: if (Request.AppRelativeCurrentExecutionFilePath.EndsWith(".aspx"))
6: {
7: Response.Filter = new ScriptDeferFilter(Response);
8: }
9: }
10: }
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 when ContentType is text/html.
When this filter is installed, www.dropthings.com defers all script loading after the <form> tag completes.
Figure: Script tags are moved after the <form> tag when the filter is used
You can grab the Filter class from the App_Code\ScriptDeferFilter.cs of the Dropthings project. Go to CodePlex site and download the latest code for the latest filter.