Performing the minification of your custom JavaScript and Css files is usually a good practice to follow when deploying your website in a production environment, but this usually makes doing ‘on the fly’ and ‘live’ modification to those files nearly impossible due to the very compact form they get.

A common technique you can use when it comes to asp.net web sites is to develop an HttpHandler that handle the minification task on the fly.

A very simple approach is to minify each file on a file per file basis, that is: every request of a .js or a .css file will be mapped to be processed by our handlers, the file read and compressed (using an appropriate library) and then stored in the cache (for any subsequent request) and streamed back to the client.

To perform the minification I decided to use the Yahoo! UI Library: YUI Compressor for .Net, but you can change it to whatever compressor you like.

Here’s the code for the Css minification handler:

   public class CssYuiCompressorHandler : IHttpHandler
   {
      private const int DefaultCacheDuration = 30;

      public bool IsReusable { get { return true; } }

      public void ProcessRequest(HttpContext context)
      {
			context.Response.ContentType = "text/css";
			string filePath = context.Request.Url.AbsolutePath;
			filePath = context.Server.MapPath(filePath);
         // if the file is already minified (we use the .min.css naming convention)
			// read it as it is and deliver it to the client
      	if (File.Exists(filePath))
      	{
      		if (filePath.EndsWith(".min.css"))
      			context.Response.WriteFile(filePath);
      		else
      			CompressCssAndWriteToResponseStream(context, filePath);
      	}
      	else
      		context.Response.StatusCode = 404;

      	context.Response.Flush();
         context.Response.End();
      }

   	static readonly object FileLock = new object();

		private static void CompressCssAndWriteToResponseStream(HttpContext context, string filePath)
      {
         string requestHash = context.Request.Url.AbsolutePath;
         if (context.Cache[requestHash] != null)
         {
            context.Response.Write((string)context.Cache[requestHash]);
            return;
         }
         lock (FileLock)
         {
            using (StreamReader sr = new StreamReader(filePath, true))
            {
               string compressed = CssCompressor.Compress(sr.ReadToEnd());
               context.Response.Write(compressed);

               context.Cache.Add(requestHash,
                                  compressed,
                                  null,
                                  System.Web.Caching.Cache.NoAbsoluteExpiration,
                                  new TimeSpan(DefaultCacheDuration, 0, 0),
                                  System.Web.Caching.CacheItemPriority.Normal,
                                  null);
               sr.Close();
            }
         }
      }
   }

The JavaScript minification handler is quite similar:

   public class JsYuiCompressorHandler : IHttpHandler
   {
      private const int DefaultCacheDuration = 30;

      public bool IsReusable { get { return true; } }

      public void ProcessRequest(HttpContext context)
      {
			context.Response.ContentType = "application/x-javascript";
			string filePath = context.Request.Url.AbsolutePath;
			filePath = context.Server.MapPath(filePath);
         // if the file is already minified (we use the .min.js naming convention)
			// read it as it is and deliver it to the client
      	if (File.Exists(filePath))
      	{
      		if (filePath.EndsWith(".min.js"))
      			context.Response.WriteFile(filePath);
      		else
					CompressJsAndWriteToResponseStream(context, filePath);
      	}
      	else
      		context.Response.StatusCode = 404;

      	context.Response.Flush();
         context.Response.End();
      }

   	static readonly object FileLock = new object();

		private static void CompressJsAndWriteToResponseStream(HttpContext context, string filePath)
		{
			string requestHash = context.Request.Url.AbsolutePath; //.GetHashCode().ToString();
			if (context.Cache[requestHash] != null)
			{
				context.Response.Write((string)context.Cache[requestHash]);
				return;
			}
			lock (FileLock)
			{
				using (StreamReader sr = new StreamReader(filePath, true))
				{
					string compressed = JavaScriptCompressor.Compress(sr.ReadToEnd());
					context.Response.Write(compressed);

					context.Cache.Add(requestHash,
											 compressed,
											 null,
											 System.Web.Caching.Cache.NoAbsoluteExpiration,
											 new TimeSpan(DefaultCacheDuration, 0, 0),
											 System.Web.Caching.CacheItemPriority.Normal,
											 null);
					sr.Close();
				}
			}
		}
   }

To activate these two handlers you have to modify the web.config file:

		<httpHandlers>
			...
			<add verb="*" path="*.css" type="Dexter.Web.HttpHandlers.CssYuiCompressorHandler, Dexter.Web, Version=1.0.0.0, Culture=neutral"/>
			<add verb="*" path="*.js" type="Dexter.Web.HttpHandlers.JsYuiCompressorHandler, Dexter.Web, Version=1.0.0.0, Culture=neutral"/>
		</httpHandlers>

This approach allows you to deploy the files as they are and the minification is performed on the server during the first request.

I tested this technique in Dexter (it’s actually working in this blog) and I noticed a good reduction in the size of the custom .css and .js files I had:

MinificationBefore MinificationAfter

for example:

Site.css passed from 36.80 KB to 27.53 KB

shCore.js passed from 19.22 KB to 18.17 KB

customFunction.js passed from 5.44 KB to 3.77 KB

The total reduction in size was something like 32 KB.

In a more advanced solution you can look for a way to not only minify the single files, but also to merge them in a single file in order to minimize the number of requests made to the server.

Update: fixed a bug in the cache usage.

Related Content