Click here to Skip to main content
Licence CPOL
First Posted 17 Oct 2008
Views 66,198
Downloads 494
Bookmarked 94 times

Combining, Compressing, Minifying ASP.NET ScriptResource and HTML Markups

By Moiz Dhanji | 2 Feb 2009
This article is about combining, compressing, minifying the ASP.NET ScriptResource and HTML markups.

1

2
1 vote, 4.8%
3
6 votes, 28.6%
4
14 votes, 66.7%
5
4.76/5 - 21 votes
1 removed
μ 4.58, σa 1.03 [?]

Introduction

We have been developing a large project in ASP.NET 3.5 along with Microsoft AJAX for the last few months and when we started testing in the test environment, we came across a large number of ScriptResource.axd entries making the HTTP calls to download the JavaScript to make it available for the page. Here we had about 80/90 lines keeping the HTTP handler busy, so in this case if we had 100 concurrent users, we would have had about 800/900 concurrent calls to HTTP. In order to optimize the performance/scalability and reducing the HTTP calls, we found out a way to get those calls minimized and thought of sharing with the community who could have the same performance issue.

The Solution

The solution we present here combines multiple JavaScript file declarations in your HTML into a single declaration, meaning our 80/90 JavaScript file references are combined into 1.

Script Profiler

There is a very cool utility contained in this project which gets you a list of all scripts being used on the page. So, before running the project, just set enableProfiler to true and you will get the list of scripts on the page. Those scripts must be placed under optimizerSection tag, so it will help ScriptManager to find the script and combine it.

<optimizerSection enable="true" enableScriptCompression="true" 
	enableHtmlCompression="true" enableScriptMinification="true" 
	enableHtmlMinification="true" enableProfiler="false">
     <add key="1" name="MicrosoftAjax.js"  
         assembly="System.Web.Extensions, Version=3.5.0.0, 
	Culture=neutral, PublicKeyToken=31bf3856ad364e35" path="" />
     <add key="2" name="MicrosoftAjaxWebForms.js"  
         assembly="System.Web.Extensions, Version=3.5.0.0, 
	Culture=neutral, PublicKeyToken=31bf3856ad364e35" path="" />
     <add key="3" name="" assembly="" path="~/Scripts/Script01.js" />
     <add key="4" name="" assembly="" path="~/Scripts/Script02.js" />
     <add key="5" name="" assembly="" path="~/Scripts/Script03.js" />
     <add key="6" name="AspNetPerformanceOptimizer.Controls.Control01Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="7" name="AspNetPerformanceOptimizer.Controls.Control02Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="8" name="AspNetPerformanceOptimizer.Controls.Control03Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="9" name="AspNetPerformanceOptimizer.Controls.Control04Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="10" name="AspNetPerformanceOptimizer.Controls.Control05Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="11" name="AspNetPerformanceOptimizer.Controls.Control06Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="12" name="AspNetPerformanceOptimizer.Controls.Control07Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="13" name="AspNetPerformanceOptimizer.Controls.Control08Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="14" name="AspNetPerformanceOptimizer.Controls.Control09Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="15" name="AspNetPerformanceOptimizer.Controls.Control10Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="16" name="AspNetPerformanceOptimizer.Controls.Control11Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="17" name="AspNetPerformanceOptimizer.Controls.Control12Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="18" name="AspNetPerformanceOptimizer.Controls.Control13Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="19" name="AspNetPerformanceOptimizer.Controls.Control14Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="20" name="AspNetPerformanceOptimizer.Controls.Control15Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="21" name="AspNetPerformanceOptimizer.Controls.Control16Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="22" name="AspNetPerformanceOptimizer.Controls.Control17Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="23" name="AspNetPerformanceOptimizer.Controls.Control18Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
     <add key="24" name="AspNetPerformanceOptimizer.Controls.Control19Client.js"  
         assembly="AspNetPerformanceOptimizer, Version=1.0.0.0, 
	Culture=neutral, PublicKeyToken=null" path="" />
</optimizerSection>

Configuration

We have a file called ScriptCombinerSection.cs which loads the necessary configuration information from the web.config file and makes it available using a static class called OptimizerConfig.

public class OptimizerConfig
{
    protected static Dictionary<string, /> _scripts;
    protected static bool _enable;
    protected static bool _enableProfiler;
    protected static bool _enableScriptCompression;
    protected static bool _enableHtmlCompression;
    protected static bool _enableScriptMinification;
    protected static bool _enableHtmlMinification;
     static OptimizerConfig()
    {
        _scripts = new Dictionary<string, />();
        OptimizerSection sec = null;
        try
        {
            sec = (OptimizerSection)
                  	System.Configuration.ConfigurationManager.GetSection
"optimizerSection");
             foreach (ScriptElement i in sec.Scripts)
            {
                _scripts.Add(i.Key, i);
            }
            _enable = sec.Enable;
            _enableProfiler = sec.EnableProfiler;
            _enableScriptCompression = sec.EnableScriptCompression;
            _enableHtmlCompression = sec.EnableHtmlCompression;
            _enableScriptMinification = sec.EnableScriptMinification;
            _enableHtmlMinification = sec.EnableHtmlMinification;
        }
        catch { }
    }
    public static ScriptElement GetScriptByKey(string key)
    {
        ScriptElement objElement = null;
        try
        {
            objElement = _scripts[key];
        }
        catch { }
        return objElement;
    }
    public static ScriptElement GetScriptByResource(string name, string assembly)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair<string, /> element in _scripts)
        {
            if (element.Value.Name == name && element.Value.Assembly == assembly)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static ScriptElement GetScriptByPath(string path)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair<string, /> element in _scripts)
        {
            if (element.Value.Path == path)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static ScriptElement GetScriptByName(string name)
    {
        ScriptElement objElement = null;
        foreach (KeyValuePair<string, /> element in _scripts)
        {
            if (element.Value.Name == name)
            {
                objElement = element.Value;
                break;
            }
        }
        return objElement;
    }
    public static bool Enable
    {
        get { return _enable; }
    }
    public static bool EnableProfiler
    {
        get { return _enableProfiler; }
    }
    public static bool EnableScriptCompression
    {
        get { return _enableScriptCompression; }
    }
    public static bool EnableHtmlCompression
    {
        get { return _enableHtmlCompression; }
    }
    public static bool EnableScriptMinification
    {
        get { return _enableScriptMinification; }
    }
    public static bool EnableHtmlMinification
    {
        get { return _enableHtmlMinification; }
    }
}

Optimization Types

We can divide the overall performance optimization into five different pieces:

  • Script Combiner: Combines all scriptresource.axd calls into a single call.
  • Script Compressor: Compresses all client side scripts based on the browser capability including gzip/deflate.
  • Script Minifier: Removes comments, indentations, and line breaks.
  • HTML Compressor: Compress all HTML markup based on the browser capability including gzip/deflate.
  • HTML Minification: Writes complete HTML into a single line and minifies it at possible level (under construction).

Script Combiner and Compressor

As mentioned earlier that the basic purpose of developing the script combiner was to minimize the HTTP calls and get the complete client side script in one shot. In order to get it working, we will need to override the ScriptManager class and three of its methods. One of the main methods which must be overridden is OnResolveScriptReference. Whenever each script gets resolved, we get it here and replace it with the script information provided by the web.config. If we enable script profile, the Render method writes the list of profiled scripts on the browser.

public class OptimizeScriptManager : ScriptManager
{
    private const string HANDLER_PATH = "~/ClientScriptCombiner.aspx?keys=";
    private const string BLOCKED_HANDLER_PATH = HANDLER_PATH + "-1";
    private Dictionary<string, /> _scripts = new Dictionary<string, />();
    private List<ScriptReference> _profilerScripts = null;
     protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        if (OptimizerConfig.EnableProfiler) _profilerScripts = 
					new List<ScriptReference>();
    }
    protected override void OnResolveScriptReference(ScriptReferenceEventArgs e)
    {
        try
        {
            base.OnResolveScriptReference(e);
             #region Profiling scripts
            if (OptimizerConfig.EnableProfiler)
            {
                bool isFound = false;
                foreach (ScriptReference reference in _profilerScripts)
                {
                    if (reference.Assembly == e.Script.Assembly && 
                         reference.Name == e.Script.Name && 
			reference.Path == e.Script.Path)
                    {
                        isFound = true;
                        break;
                    }
                }
                if (!isFound)
                {
                    ScriptReference objScrRef = new ScriptReference(e.Script.Name, 
                                                                    e.Script.Assembly);
                    if (!string.IsNullOrEmpty(e.Script.Name) && 
                        string.IsNullOrEmpty(e.Script.Assembly))
                    {
                        //TODO: if resource belongs to System.Web.Extensions.dll, 
                        //it does not provide assembly info that's why hard-coded 
                        //assembly name is written to get it in profiler
                        objScrRef.Assembly = "System.Web.Extensions, Version=3.5.0.0," +
                                             " Culture=neutral, 
					PublicKeyToken=31bf3856ad364e35";
                    }
                    objScrRef.Path = e.Script.Path;
                    objScrRef.IgnoreScriptPath = e.Script.IgnoreScriptPath;
                    objScrRef.NotifyScriptLoaded = e.Script.NotifyScriptLoaded;
                    objScrRef.ResourceUICultures = e.Script.ResourceUICultures;
                    objScrRef.ScriptMode = e.Script.ScriptMode;
                    _profilerScripts.Add(objScrRef);
                    objScrRef = null;
                }
            }
            #endregion
             #region Combining Client Scripts
             bool isAssemblyBased = ((e.Script.Assembly.Length > 0) ? true : false);
            bool isPathBased = ((e.Script.Path.Length > 0) ? true : false);
            bool isNameBased = ((e.Script.Path.Length == 0 && 
				e.Script.Assembly.Length == 0 
                                	&& e.Script.Name.Length > 0) ? true : false);
             if (OptimizerConfig.Enable && (isAssemblyBased || 
				isPathBased || isNameBased))
            {
                ScriptElement element = null;
                try
                {
                    if (isAssemblyBased)
                        element = OptimizerConfig.GetScriptByResource(e.Script.Name, 
                                                                      e.Script.Assembly);
                    else if (isPathBased)
                    {
                        element = OptimizerConfig.GetScriptByPath(e.Script.Path);
                        if (null != element)
                        {
                            if (!OptimizerHelper.IsValidExtension(element, ".js"))
                            {
                                element = null;
                            }
                            else if (!OptimizerHelper.IsAbsolutePathExists(element))
                            {
                                string absolutePath = 
				OptimizerHelper.GetAbsolutePath(element);
                                element = null;
                            }
                        }
                    }
                    else if (isNameBased)
                        element = OptimizerConfig.GetScriptByName(e.Script.Name);
                }
                catch (Exception exc)
                {
                    element = null;
                }
                 if (element != null)
                {
                    if (!_scripts.ContainsKey(element.Key))
                    {
                        try
                        {
                            _scripts.Add(element.Key, e.Script);
                            e.Script.Assembly = string.Empty;
                            e.Script.Name = string.Empty;
                             StringBuilder objStrBuilder = new StringBuilder();
                            objStrBuilder.Append(HANDLER_PATH);
                             foreach (KeyValuePair<string, /> script in _scripts)
                            {
                                objStrBuilder.Append(script.Key + ".");
                            }
                            string strPath = objStrBuilder.ToString();
                            objStrBuilder = null;
                             foreach (KeyValuePair<string, /> script in _scripts)
                            {
                                script.Value.Path = strPath;
                            }
                        }
                        catch { }
                    }
                    else
                    {
                        e.Script.Assembly = string.Empty;
                        e.Script.Name = string.Empty;
                        e.Script.Path = BLOCKED_HANDLER_PATH;
                    }
                }
            }
            #endregion
        }
        catch (Exception ex)
        {
            this.Page.Response.Write(ex.ToString().Replace("\n", "<br>"));
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        try
        {
            #region Writing profiled scripts on the browser
            if (OptimizerConfig.EnableProfiler && _profilerScripts != null)
            {
                StringBuilder builder = new StringBuilder();
                int index = 1;
                foreach (ScriptReference script in _profilerScripts)
                {
                    builder.Append("<add key=\"" + index++ + "\" name=\"" + 
                                   script.Name + "\" assembly=\"" + script.Assembly + 
                                   "\" path=\"" + script.Path + "\" /><br>");
                }
                writer.WriteLine("<pre>");
                writer.WriteLine(builder.ToString());
                writer.WriteLine("</pre>");
                builder = null;
            }
            #endregion
        }
        catch (Exception ex)
        {
            this.Page.Response.Write(ex.ToString().Replace("\n", "<br>"));
        }
        finally
        {
            base.Render(writer);
        }
    }
}

Once all scripts have been resolved, it will build the URL with all keys which are required by the page:

<script src="ClientScriptCombiner.aspx?keys=
	1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24." 
         type="text/javascript"></script>

Here keys parameter in query string represents the script numbers separated by dot (.). These script numbers are specified in web.config, so while resolving the script, it picks the number against the matching string and builds the URL. Once the URL has been built and written to the browser, the handler will get called and pick each number to extract the client script from the assemblies or files. The StringBuilder is used to collect the stream of scripts and write it on the browser. When the handler gets called and finishes combining all scripts, it determines the capability of the browser, either it supports the compression or not. If it does, e.g. gzip, it will create the instance of the GZipStream class and compress the scripts, otherwise it writes as is.

public void ProcessRequest(HttpContext context)
{
    bool shouldProcessRequest = true;
    string[] scriptKeys = null;
    string keys = context.Server.UrlDecode(context.Request.Params["keys"]);
    string scriptResourcePath = String.Empty;
    ScriptManager objScriptManager = new ScriptManager();
    StringBuilder scriptBuilder = new StringBuilder();
    IHttpHandler handler = new ScriptResourceHandler();
    ///StringCollection _gescHdrStatus = new StringCollection();
     if (String.IsNullOrEmpty(keys) || keys.Equals("-1")) shouldProcessRequest = false;
     if (shouldProcessRequest)
    {
        scriptKeys = keys.Split('.');
        ///_gescHdrStatus.Add("incount:" + scripts.Length.ToString()); //script count
        scriptResourcePath = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}", 
                context.Request.Url.Scheme, "://", context.Request.Url.Host, ":", 
                context.Request.Url.Port, "/", context.Request.ApplicationPath, 
                "/ScriptResource.axd");
        foreach (string key in scriptKeys)
        {
            ScriptElement element = OptimizerConfig.GetScriptByKey(key);
            if (element == null) continue;
             #region Generating resource URL dynamically and creating 
			WebRequest object to extract stream of script
            if (element != null)
            {
                ScriptReference reference = null;
                bool isPathBased = false;
                if (element.Path.Length > 0)
                {
                    reference = new ScriptReference(element.Path);
                    isPathBased = true;
                }
                else if (element.Assembly.Length > 0 && element.Name.Length > 0)
                {
                    reference = new ScriptReference(element.Name, element.Assembly);
                }
                try
                {
                    OptimizeScriptReference openReference = 
				new OptimizeScriptReference(reference);
                    string url = string.Empty;
                    if (!isPathBased)
                    {
                        url = context.Request.Url.OriginalString.Replace
				(context.Request.RawUrl, "") + 
                                       openReference.GetUrl(objScriptManager);
                        var queryStringIndex = url.IndexOf('?');
                        var queryString = url.Substring(queryStringIndex + 1);
                        var request = new HttpRequest("scriptresource.axd", 
					scriptResourcePath, queryString);
                         using (StringWriter textWriter = 
					new StringWriter(scriptBuilder))
                        {
                            HttpResponse response = new HttpResponse(textWriter);
                            HttpContext ctx = new HttpContext(request, response);
                              handler.ProcessRequest(ctx);
                        }
                    }
                    else
                    {
                        string absolutePath = OptimizerHelper.GetAbsolutePath(element);
                        if (OptimizerHelper.IsAbsolutePathExists(absolutePath))
                        {
                            using (StreamReader objJsReader = 
					new StreamReader(absolutePath, true))
                            {
                                scriptBuilder.Append(objJsReader.ReadToEnd());
                            }
                        }
                    }
                    scriptBuilder.AppendLine();
                }
                catch (Exception ex)
                {
                }
            }
            #endregion
        }
    }
     objScriptManager = null;
     #region Writing combine output scripts to the Response.OutputStream
      context.Response.Clear();
    context.Response.ContentType = "application/x-javascript";
     try
    {
        SetResponseCache(context.Response);
        scriptBuilder.AppendLine();
        //scriptBuilder.AppendLine("if(typeof(Sys)!=='undefined')
        // 	Sys.Application.notifyScriptLoaded();");
        string combinedScripts = scriptBuilder.ToString();
         if (shouldProcessRequest)
        {
            if (OptimizerConfig.EnableScriptMinification)
            {
                combinedScripts = JsMinifier.GetMinifiedCode(combinedScripts);
            }
             string encodingTypes = string.Empty;
            string compressionType = "none";
            if (OptimizerConfig.EnableScriptCompression)
            {
                encodingTypes = context.Request.Headers["Accept-Encoding"];
                 if (!string.IsNullOrEmpty(encodingTypes))
                {
                    encodingTypes = encodingTypes.ToLower();
                    if (context.Request.Browser.Browser == "IE")
                    {
                        if (context.Request.Browser.MajorVersion < 6)
                            compressionType = "none";
                        else if (context.Request.Browser.MajorVersion == 6 && 
                           !string.IsNullOrEmpty(context.Request.ServerVariables
							["HTTP_USER_AGENT"]) 
                           && context.Request.ServerVariables
					["HTTP_USER_AGENT"].Contains("EV1"))
                            compressionType = "none";
                    }
                    if ((encodingTypes.Contains("gzip") || 
				encodingTypes.Contains("x-gzip") 
                       || encodingTypes.Contains("*")))
                        compressionType = "gzip";
                    else if (encodingTypes.Contains("deflate"))
                        compressionType = "deflate";
                }
            }
            else
            {
                compressionType = "none";
            }
            if (compressionType == "gzip")
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    using (StreamWriter writer = new StreamWriter(new GZipStream(stream, 
                                      	CompressionMode.Compress), Encoding.UTF8))
                    {
                        writer.Write(combinedScripts);
                    }
                    byte[] buffer = stream.ToArray();
                    context.Response.AddHeader("Content-encoding", "gzip");
                    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
                }
            }
            else if (compressionType == "deflate")
            {
                using (MemoryStream stream = new MemoryStream())
                {
                    using (StreamWriter writer = new StreamWriter
						(new DeflateStream(stream, 
                                                      	CompressionMode.Compress), 
						Encoding.UTF8))
                    {
                        writer.Write(combinedScripts);
                    }
                    byte[] buffer = stream.ToArray();
                    context.Response.AddHeader("Content-encoding", "deflate");
                    context.Response.OutputStream.Write(buffer, 0, buffer.Length);
                }
            }
            else
            {
                //no compression plain text...
                context.Response.AddHeader("Content-Length", 
				combinedScripts.Length.ToString());
                context.Response.Write(combinedScripts);
            }
        }
        scriptBuilder = null;
    }
    catch (Exception ex)
    {
        context.Response.Write(ex.ToString().Replace("\n", "<br>"));
    }
    #endregion
}

Script Minifier

We are using jsmin i.e. courtesy of Douglas Crockford.

HTML Compressor, HTML Minifier

In order to compress and minify the HTML markups, we have come up with the new streaming class called HtmlCompressStream inheriting from Stream.

public class HtmlCompressStream : Stream
{
    public enum CompressionType { None = 0, GZip = 1, Deflate = 2 };
    private Stream _stream;
     public HtmlCompressStream
	(Stream stream, CompressionMode mode, CompressionType type)
    {
        switch (type)
        {
            case CompressionType.GZip:
                _stream = new GZipStream(stream, mode);
                break;
            case CompressionType.Deflate:
                _stream = new DeflateStream(stream, mode);
                break;
            default:
                _stream = new StreamWriter(stream).BaseStream;
                break;
        }
    }
     public Stream BaseStream
    {
        get { return _stream; }
    }
    public override bool CanRead
    {
        get { return _stream.CanRead; }
    }
    public override bool CanSeek
    {
        get { return _stream.CanSeek; }
    }
    public override bool CanWrite
    {
        get { return _stream.CanWrite; }
    }
    public override long Length
    {
        get { return _stream.Length; }
    }
    public override long Position
    {
        get { return _stream.Position; }
        set { _stream.Position = value; }
    }
     public override IAsyncResult BeginRead(byte[] array, int offset, int count, 
                                    AsyncCallback asyncCallback, object asyncState)
    {
        return _stream.BeginRead(array, offset, count, asyncCallback, asyncState);
    }
    public override IAsyncResult BeginWrite(byte[] array, int offset, int count, 
                                    AsyncCallback asyncCallback, object asyncState)
    {
        return _stream.BeginWrite(array, offset, count, asyncCallback, asyncCallback);
    }
    protected override void Dispose(bool disposing)
    {
        _stream.Dispose();
    }
    public override int EndRead(IAsyncResult asyncResult)
    {
        return _stream.EndRead(asyncResult);
    }
    public override void EndWrite(IAsyncResult asyncResult)
    {
        _stream.EndWrite(asyncResult);
    }
    public override void Flush()
    {
        _stream.Flush();
    }
    public override int Read(byte[] array, int offset, int count)
    {
        return _stream.Read(array, offset, count);
    }
    public override long Seek(long offset, SeekOrigin origin)
    {
        return _stream.Seek(offset, origin);
    }
    public override void SetLength(long value)
    {
        _stream.SetLength(value);
    }
    public override void Write(byte[] array, int offset, int count)
    {
        if (OptimizerConfig.EnableHtmlMinification)
        {
            //TODO: HTML Minification
            _stream.Write(array, offset, count);
        }
        else
        {
            _stream.Write(array, offset, count);
        }
    }
}

Based on the browser’s capability, we create the compress stream object and let the writer write the stuff. Writing this class has a couple of advantages, one is to enable compression and the second is to minify HTML markup at the time of writing. So, when the writer wants to emit stream on the browser, it invokes the Write method which writes the stream of bytes to the browser. We assign the instance of this class to the Response.Filter property at the time of page’s request, i.e. written in the Application_BeginRequest event of Global.asax.

public class Global : System.Web.HttpApplication
{
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpRequest request = this.Request;
        HttpResponse response = this.Response;
         if (request.RawUrl.IndexOf(".aspx") > -1 && 
			string.IsNullOrEmpty(request.Params["keys"]))
        {
            if (OptimizerConfig.EnableHtmlCompression && 
			!(request.Browser.IsBrowser("IE") &&
                request.Browser.MajorVersion <= 6))
            {
                string acceptEncoding = request.Headers["Accept-Encoding"];
                if (!string.IsNullOrEmpty(acceptEncoding))
                {
                    acceptEncoding = 
			acceptEncoding.ToLower(CultureInfo.InvariantCulture);
                     if (acceptEncoding.Contains("gzip"))
                    {
                        //response.Filter = 
			new GZipStream(response.Filter, CompressionMode.Compress);
                        response.Filter = new HtmlCompressStream(response.Filter,
                            CompressionMode.Compress,
                            HtmlCompressStream.CompressionType.GZip);
                        response.AddHeader("Content-encoding", "gzip");
                    }
                    else if (acceptEncoding.Contains("deflate"))
                    {
                        //response.Filter = new DeflateStream
		      //(response.Filter, CompressionMode.Compress);
                        response.Filter = new HtmlCompressStream(response.Filter,
                            CompressionMode.Compress,
                            HtmlCompressStream.CompressionType.Deflate);
                        response.AddHeader("Content-encoding", "deflate");
                    }
                }
            }
            else
            {
                response.Filter = new HtmlCompressStream(response.Filter,
                    CompressionMode.Compress, HtmlCompressStream.CompressionType.None);
            }
        }
    }
}

Note: The HTML minification is still under construction and would be done very soon. We’re looking forward to having a great HTML minifier.

Using the Sample Project

There are some Ajax client controls and *.js files created to run and test the sample. You will need to rebuild the project and then access default.aspx. If you want to test it with AjaxControlToolkit, download the latest version of control toolkit and reference it in the project. Drag and drop some controls from the Toolbox, enable the profiler, and run the project. It will write the list of scripts on the page; just copy those scripts and put it in the web.config file under optimizerSection. That’s all we need to do to run the project.

Useful Results

BEFORE (1.55 seconds for 238kb)

AFTER (62 milliseconds for 75kb)

Conclusion

We always find some kind of trade-off between processing and network latency, this article is entirely focusing on network latency; not on the processing cost because it has been assumed that this implementation is done on high end processing servers. Finally, if you're using ASP.NET with AJAX client controls, make sure your website is providing best performance at all levels of scalability. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Moiz Dhanji

Software Developer (Senior)
CIGNA Healthcare
United States United States

Member


Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralRe: Timeout Problem PinmemberMoiz Dhanji17:40 10 Jun '09  
QuestionIt is possibe to capture Ajax scripts as well? Pinmemberarbound0821:57 28 May '09  
AnswerRe: It is possibe to capture Ajax scripts as well? PinmemberMoiz Dhanji7:23 4 Jun '09  
GeneralIdeas PinmemberSergio Luix21:22 2 Apr '09  
GeneralRe: Ideas PinmemberMoiz Dhanji7:30 4 Jun '09  
QuestionDoes this project have a public repository? Pinmemberesteewhy21:50 15 Mar '09  
AnswerVery unefficient, but heres a solution!!!! Pinmembermarkentingh12:38 14 Feb '09  
I am a high-end .net developer and your project has helped me in so many ways. I now have an average 96% A in the YSlow plugin for firefox, which is amazing. I used your concept and took it much further. Your project has many flaws that, in the long run, will take up too many resources with CPU and RAM usage.
 
Problems:
 
1. I shouldnt have to add anything to the web.config
2. my browser should only have to download one script one time and have it cached.
3. some pages might require axd code that others do not, so caching becomes complex
4. combining the scripts, minifying it, then gzipping it for every request is very unefficient
 
Solution:
Reprogram everything with a new strategy.
The concept works like this, first check to see if the cached js file is located on the server, if not, generate the cached file from all used assemblies and external js files, then save it to disk. If it is cached, skip all steps.
 
First, remove profiling code that gives you a list of assemblies to add to your web.config. Get rid of the optimizerconfig code, and the optimizescriptreference class. Add an attribute to your scriptmanager tag (on your page) named "scriptname".
 
<ajax:scriptoptimizer id="scriptman" scriptname="master" />
 
That will be the name of the cached js file on the server for that page (or for all pages that use the exact same assemblies). Then add code to the onInit of your script manager base class that checks to see if that js file (scriptname + .js) already exists on the server. If it doesn't, var isCached = false. Then add a protected string var scriptCode to the class. In the OnResolveScriptReference sub, first check if isCached = false, then add each assembly's js source code to var scriptCode. In the Render sub, check if isCached = false, save var scriptCode to your scriptname + .js file, or if isCached = true, do nothing because the file is already cached.
 
You can add another class to your script manager optimizer to add custom js files to your cached js file.
 
<ajax:scriptInclude src="../scripts.global.js" scriptmanager="scriptman">
 
Have the scriptinclude class use the scriptmanager attribute to find the instance of "scriptman" on the page, execute a function within the "scriptman" scriptmanager class that adds the contents of the .js file in your scriptinclude tag to the var scriptCode before the cached file is created.
 
The solution will make one file that can be used site-wide, downloaded once by each user, and generated once for the whole community, and if you have a few pages that use special server-side elements, like for example the ajaxcontroltoolkit Calendar popup, you can use a different scriptname for that page which will generate a different cached js file just for that page.
 
Here's the entire class (in vb.net), and it works beautifully! I didn't add minify code because I couldnt convert the minify c sharp to vb.net. I'll be using this class for a c sharp project soon, so i'll have a c sharp version ready in the coming months.
 
 
Imports Microsoft.VisualBasic
Imports System
Imports System.Web.UI.ScriptManager
Imports System.IO
Imports System.Reflection
Imports System.Collections.Generic
Imports System.Text
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Collections.Specialized
Imports System.Web
 
Namespace Rennder.scriptManagerOptimizer
 
    Public Class scriptOptimizer
        Inherits scriptManager
        Implements IRequiresSessionState
        Protected scriptBuilder As New StringBuilder
        Protected sessionIndex As String = ""
        Protected scriptLibrary As List(Of ScriptReference)
        Protected isCached As Boolean = False
        Protected myScriptName As String = ""
 
        Public Property scriptname() As String
            Get
                Return myScriptName
            End Get
            Set(ByVal value As String)
                myScriptName = value
            End Set
        End Property
 
        Public Sub New()
            
        End Sub
 
        Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
            MyBase.OnInit(e)
 
            If scriptname = "" Then scriptname = "cachedscript"
            'this compiled script should be cached on the server
            'check for the file, if it exists, load that file instead of generating it
            If File.Exists(Me.Page.Server.MapPath("~/scripts/" & scriptname & ".js")) = True Then
                isCached = True
            End If
 

            If isCached = False Then
                scriptLibrary = New List(Of ScriptReference)()
            End If
        End Sub
 
        Protected Overrides Sub OnResolveScriptReference(ByVal e As System.Web.UI.ScriptReferenceEventArgs)
            Try
                MyBase.OnResolveScriptReference(e)
 
                If isCached = False Then
 

                    'create variables for important references that will help us determine
                    'if a script should be used, how it should be loaded, and the contents
                    'of the scripts themselves

                    Dim element As ScriptReference
                    element = e.Script
                    Dim scriptResourcePath As String = String.Format("{0}{1}{2}{3}{4}{5}{6}{7}", Context.Request.Url.Scheme, "://", Context.Request.Url.Host, ":", Context.Request.Url.Port, "/", Context.Request.ApplicationPath, "/ScriptResource.axd")
                    Dim reference As ScriptReference = Nothing
                    Dim isPathBased As Boolean = False
                    Dim handler As IHttpHandler = New System.Web.Handlers.ScriptResourceHandler
 
                    '////////////////////////////////////////////////////////////
                    'First, check to make sure this script should be loaded
                    Dim isFound As Boolean = False
                    For Each refx As ScriptReference In scriptLibrary
                        If refx.Assembly = e.Script.Assembly And refx.Name = e.Script.Name And refx.Path = e.Script.Path Then
                            isFound = True
                            Exit For
                        End If
                    Next
 
                    If isFound = False Then
                        '////////////////////////////////////////////////////////////
                        'If this script is found within the list of scripts that this 
                        'page uses, add the script to the scripthandler.aspx js output

                        'create an index for saving compiled, minified, gzipped
                        'javascript to session before scripthandlder.aspx uses 
                        'the session to output js to client-side browser
                        If sessionIndex = "" Then
                            Randomize()
                            Dim myArr() As String = Split(Me.Page.Request.Url.AbsolutePath, "/")
                            For x As Integer = 0 To myArr.Length - 1
                                If InStr(myArr(x), ".aspx") > 0 Then
                                    sessionIndex = myArr(x - 1) & Replace(Split(myArr(x), "?")(0), ".aspx", "")
                                    Exit For
                                End If
                            Next
                            'sessionIndex = 1 + Int(Rnd() * 999)
                        End If
 
                        If element.Path.Length <= 0 And element.Name.Length > 0 And element.Assembly.Length <= 0 Then
                            'if resource belongs to System.Web.Extensions.dll, it does not 
                            'provide assembly info that's why hard-coded assembly name is 
                            'written to get it in profiler
                            element.Assembly = "System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                        End If
 
                        'check to see what type of script this is
                        If element.Path.Length > 0 Then
                            'this script is a physical file
                            reference = New ScriptReference(element.Path)
                            isPathBased = True
                        ElseIf element.Assembly.Length > 0 And element.Name.Length > 0 Then
                            'this script is generated by an assembly
                            reference = New ScriptReference(element.Name, element.Assembly)
                        Else
                            'when all else fails (waef), this script is not found
                            'HttpContext.Current.Response.Write("reference not found: <br>element name = " & element.Name & "<br>element assembly = " & element.Assembly & "<br>element path = " & element.Path)
                            'HttpContext.Current.Response.End()
                        End If
 
                        Dim url As String = ""
                        If Not isPathBased Then
                            'if this script is from an assembly, load the assembly via
                            'a server-side http request, then add the output to the
                            'compiled js variable
                            If InStr(Context.Request.RawUrl, "localhost") Then
                                url = "http://localhost:80" & GetUrl(Me, reference)
                            Else
                                url = "http://www.mysite.com" & GetUrl(Me, reference)
                            End If
 
                            Dim queryStringIndex = url.IndexOf("?"c)
                            Dim queryString = url.Substring(queryStringIndex + 1)
                            Dim request = New HttpRequest("scriptresource.axd", scriptResourcePath, queryString)
 
                            Using textWriter As New StringWriter(scriptBuilder)
                                Dim response As New HttpResponse(textWriter)
                                Dim ctx As New HttpContext(request, response)
                                handler.ProcessRequest(ctx)
                            End Using
 
                        Else
                            'if this script is from a file, open the file and load the
                            'contents of the file into the compiled js variable
                            Dim absolutePath As String = HttpContext.Current.Server.MapPath(element.Path)
                            If System.IO.File.Exists(absolutePath) Then
                                Using objJsReader As New StreamReader(absolutePath, True)
                                    scriptBuilder.Append(objJsReader.ReadToEnd())
                                End Using
                            End If
                        End If
                        scriptBuilder.AppendLine()
 
                        'add this script to the script reference library
                        Dim newElement As New ScriptReference
                        newElement.Name = e.Script.Name.ToString()
                        newElement.Assembly = e.Script.Assembly.ToString()
                        newElement.Path = e.Script.Path.ToString()
                        scriptLibrary.Add(newElement)
                        newElement = Nothing
                    End If
 
                    'a script filename is provided for caching
                    e.Script.Assembly = String.Empty
                    e.Script.Name = String.Empty
                    e.Script.Path = "/scripts/" & scriptname & ".js"
 

                Else
                    e.Script.Assembly = String.Empty
                    e.Script.Name = String.Empty
                    e.Script.Path = "/scripts/" & scriptname & ".js"
                End If
 
            Catch ex As Exception
                HttpContext.Current.Response.Write(ex)
                HttpContext.Current.Response.End()
            End Try
 
        End Sub
 
        Public Sub AddScript(ByVal file As String)
            If isCached = False Then
                Dim absolutePath As String = HttpContext.Current.Server.MapPath(file)
                If System.IO.File.Exists(absolutePath) Then
                    Using objJsReader As New StreamReader(absolutePath, True)
                        scriptBuilder.Append(objJsReader.ReadToEnd())
                    End Using
                End If
                scriptBuilder.AppendLine()
 
                'add this script to the script reference library
                Dim newElement As New ScriptReference
                newElement.Name = ""
                newElement.Assembly = ""
                newElement.Path = file
                scriptLibrary.Add(newElement)
                newElement = Nothing
            End If
        End Sub
 
        Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
            MyBase.Render(writer)
            If isCached = False Then
                If scriptname <> "" Then
                    'save script to file for caching
                    Dim myFile As New FileStream(Me.Page.Server.MapPath("~/scripts/" & scriptname & ".js"), FileMode.Create, FileAccess.Write, FileShare.Read)
                    Dim myWriter As New StreamWriter(myFile)
                    myWriter.Write(scriptBuilder.ToString)
                    myWriter.Flush()
                    myWriter.Close()
                    myFile.Close()
                End If
                scriptBuilder = Nothing
            End If
        End Sub
 
        Public Function GetUrl(ByVal sManager As ScriptManager, ByVal ref As ScriptReference) As String
            Dim url As String = String.Empty
            If String.IsNullOrEmpty(ref.Path) Then
                Try
                    Dim piScriptManager_IControl As PropertyInfo = sManager.GetType.GetProperty("Control", BindingFlags.NonPublic Or BindingFlags.Instance)
                    Dim miScriptReference_GetUrl As MethodInfo = GetType(ScriptReference).GetMethod("GetUrl", BindingFlags.NonPublic Or BindingFlags.Instance)
                    Dim typeIControl As Type = Type.GetType(piScriptManager_IControl.PropertyType.AssemblyQualifiedName.ToString(), False, True)
                    'Dim value As Object = Convert.ChangeType(piScriptManager_IControl.PropertyType, typeIControl)
                    Dim value As Object = piScriptManager_IControl.GetValue(sManager, Nothing)
                    url = DirectCast(miScriptReference_GetUrl.Invoke(ref, New Object() {value, False}), String)
 
                    'return base.GetUrl(scriptManager, false); //Ajax 3.5

                    'Dim miGetScriptResourceUrl As MethodInfo = GetType(ScriptManager).GetMethod("GetScriptResourceUrl", BindingFlags.NonPublic Or BindingFlags.Instance)
                    'Dim asm As Assembly = System.Reflection.Assembly.GetExecutingAssembly()
                    'url = miGetScriptResourceUrl.Invoke(sManager, New Object() {ref.Name, asm})

                Catch ex As Exception
                End Try
            Else
                url = sManager.ResolveClientUrl(ref.Path)
            End If
            Return url
        End Function
 
    End Class
 

    Public Class scriptInclude
        Inherits Web.UI.LiteralControl
 
        Public myFile As String
        Public myScriptManager As String
 
        Public Property file() As String
            Get
                Return myFile
            End Get
            Set(ByVal value As String)
                myFile = value
            End Set
        End Property
 
        Public Property scriptmanager() As String
            Get
                Return myScriptManager
            End Get
            Set(ByVal value As String)
                myScriptManager = value
            End Set
        End Property
 
        Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
            MyBase.OnLoad(e)
            Try
                Dim myScriptMan As scriptOptimizer
                myScriptMan = Me.Page.Form.FindControl(myScriptManager)
                myScriptMan.AddScript(myFile)
            Catch ex As Exception
                'HttpContext.Current.Response.Write(ex)
            End Try
        End Sub
 
        Protected Overrides Sub Finalize()
            MyBase.Finalize()
        End Sub
 
        Public Sub New()
        End Sub
    End Class
 

End Namespace
 

 

</br></br></br>

GeneralRe: Very unefficient, but heres a solution!!!! PinmemberMichael Moreno10:00 28 Feb '09  
GeneralRe: Very unefficient, but heres a solution!!!! PinmemberMoiz Dhanji8:01 6 Mar '09  
GeneralPlease to Net2 PinmemberCadenza9:37 26 Jan '09  
GeneralRe: Please to Net2 PinmemberMoiz Dhanji13:39 2 Feb '09  
GeneralWebResource.axd Pinmemberkhacken10:44 9 Jan '09  
GeneralRe: WebResource.axd Pinmemberkhacken12:10 9 Jan '09  
QuestionOptimizeScriptManagerproxy? Pinmembererdogan0:46 8 Jan '09  
AnswerRe: OptimizeScriptManagerproxy? PinmemberMoiz Dhanji10:36 13 Jan '09  
GeneralProblem with Script Manager Pinmemberhimanshu25610:21 24 Dec '08  
GeneralRe: Problem with Script Manager PinmemberMoiz Dhanji10:53 24 Dec '08  
GeneralRe: Problem with Script Manager Pinmemberhimanshu256121:42 24 Dec '08  
GeneralRe: Problem with Script Manager PinmemberMoiz Dhanji9:42 26 Dec '08  
GeneralHTML Minifier bis Pinmembernicongri0:08 14 Nov '08  
GeneralRe: HTML Minifier bis PinmemberMoiz Dhanji8:10 17 Nov '08  
GeneralHTML Minifier PinmemberMember 34752144:07 5 Nov '08  
GeneralRe: HTML Minifier PinmemberMoiz Dhanji6:56 5 Nov '08  
GeneralDoes Not Contain a Definition for GetURL Pinmembercjralston8:48 23 Oct '08  
GeneralRe: Does Not Contain a Definition for GetURL Pinmemberjigarbjpatel2:09 24 Oct '08  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web04 | 2.5.120210.1 | Last Updated 2 Feb 2009
Article Copyright 2008 by Moiz Dhanji
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid