Friday, January 15, 2010

Combine JavaScript and CSS files

Todays websites implement sophisticated design and functionality. That results in a lot of CSS and JavaScript included in the page content. More files means more requests and slower load time.

Here is simple solution - combine the resource files and compress them with GZip.

The first part of the article is about how to combine AJAX Control Toolkit resource files. The second part demonstrates how to combine JavaScript or CSS files, strip the white space, compress the result with GZip and cache it.

1. How to combine WebResource.axd and ScriptResource.axd files:

#region Using

using System;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Text.RegularExpressions;
using System.Collections.Generic;

#endregion

///
/// Find scripts and change the src to the ScriptCompressorHandler.
///

public class ScriptCompressorModule : IHttpModule
{

#region IHttpModule Members

void IHttpModule.Dispose()
{
// Nothing to dispose;
}

void IHttpModule.Init(HttpApplication context)
{
context.PostRequestHandlerExecute += new EventHandler(context_BeginRequest);
}

#endregion

void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
if (app.Context.CurrentHandler is Page && !app.Request.RawUrl.Contains("serviceframe"))
{
if (!app.Context.Request.Url.Scheme.Contains("https"))
{
app.Response.Filter = new WebResourceFilter(app.Response.Filter);
}
}
}

#region Stream filter

private class WebResourceFilter : Stream
{

public WebResourceFilter(Stream sink)
{
_sink = sink;
}

private Stream _sink;

#region Properites

public override bool CanRead
{
get { return true; }
}

public override bool CanSeek
{
get { return true; }
}

public override bool CanWrite
{
get { return true; }
}

public override void Flush()
{
_sink.Flush();
}

public override long Length
{
get { return 0; }
}

private long _position;
public override long Position
{
get { return _position; }
set { _position = value; }
}

#endregion

#region Methods

public override int Read(byte[] buffer, int offset, int count)
{
return _sink.Read(buffer, offset, count);
}

public override long Seek(long offset, SeekOrigin origin)
{
return _sink.Seek(offset, origin);
}

public override void SetLength(long value)
{
_sink.SetLength(value);
}

public override void Close()
{
_sink.Close();
}

public override void Write(byte[] buffer, int offset, int count)
{
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
string html = System.Text.Encoding.Default.GetString(buffer);
int index = 0;
List list = new List();

Regex regex = new Regex("]*>[^<]*(?:)?", RegexOptions.IgnoreCase);
foreach (Match match in regex.Matches(html))
{
if (index == 0)
index = html.IndexOf(match.Value);

string relative = match.Groups[1].Value;
list.Add(relative);
html = html.Replace(match.Value, string.Empty);
}

if (index > 0)
{
string script = "";
string path = string.Empty;
foreach (string s in list)
{
if (path.Length + HttpUtility.UrlEncode(s).Length > 1800)
{
html = html.Insert(index, string.Format(script, path));

index += script.Length + path.Length - 3;
path = String.Empty;
}

path += HttpUtility.UrlEncode(s) + ",";
}

html = html.Insert(index, string.Format(script, path));
}

byte[] outdata = System.Text.Encoding.Default.GetBytes(html);
_sink.Write(outdata, 0, outdata.GetLength(0));
}

#endregion
}

#endregion
}

Web.Config settings:






2. How to combine script and stylesheet files:

<%@ WebHandler Language="C#" Class="ResourceHandler" %>

using System;
using System.Configuration;
using System.IO;
using System.Net;
using System.IO.Compression;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;

public class ResourceHandler : IHttpHandler {

#region Public Properties

public static bool UseFileSet
{
get { return Convert.ToBoolean(ConfigurationManager.AppSettings["UseFileSet"]); }
}

public static string VersionNo
{
get { return ConfigurationManager.AppSettings["VersionNo"]; }
}

public bool IsReusable
{
get { return true; }
}

#endregion

#region Private Methods

private static void Cache(TimeSpan duration)
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;

FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, duration);

cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetMaxAge(duration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}

private static byte[] GZip(string source)
{
byte[] result;

using (MemoryStream ms = new MemoryStream())
{
byte[] buffer = System.Text.Encoding.ASCII.GetBytes(source);

GZipStream GZip = new GZipStream(ms, CompressionMode.Compress);

GZip.Write(buffer, 0, buffer.Length);
GZip.Close();

result = ms.ToArray();
ms.Close();
}

return result;
}

private static string Minimize(string source)
{
string result = Regex.Replace(source.Trim(), @"(\s+)|((\r?\n)+)|(\t+)", " ");

return result;
}

private static string RetrieveScript(string file)
{
string script = null;

try
{
Uri url = new Uri(file, UriKind.Absolute);

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
request.AutomaticDecompression = DecompressionMethods.GZip;

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
script = reader.ReadToEnd();
}
}
catch (System.Net.Sockets.SocketException)
{
// The remote site is currently down. Try again next time.
}
catch (UriFormatException)
{
// Only valid absolute URLs are accepted
}

return script;
}

#endregion

public void ProcessRequest(HttpContext context)
{
HttpRequest request = context.Request;
HttpResponse response = context.Response;

string root = request.Url.GetLeftPart(UriPartial.Authority);

string contetType = request.QueryString["type"] ?? "application/x-javascript";

string[] fileNames = new string[0];

if (!string.IsNullOrEmpty(request.QueryString["fileSet"]))
{
string fileSet = request.QueryString["fileSet"];
//Basic Validation
if (string.IsNullOrEmpty(fileSet))
return;

if (string.IsNullOrEmpty(contetType))
return;

string files = ConfigurationManager.AppSettings["FileSet_" + fileSet];

if (string.IsNullOrEmpty(files))
return;

//Get the list of files specified in the FileSet
fileNames = files.Split(',');
}
else if (!string.IsNullOrEmpty(request.QueryString["path"]))
{
string files = HttpUtility.UrlDecode(request.QueryString["path"]);

//Get the list of files specified in the FileSet
fileNames = files.Split(',');
}

if ((fileNames == null) || (fileNames.Length == 0)) return;

bool compress = Convert.ToBoolean(request.QueryString["compress"] ?? "true");

// Check whether GZip is supported by the client
string acceptEncoding = request.Headers["Accept-Encoding"];

bool gZip = (!string.IsNullOrEmpty(acceptEncoding) && acceptEncoding.ToLower().IndexOf("gzip") > -1);

//Set the content type
response.ContentType = contetType;

string content = String.Empty;

foreach (string fileName in fileNames)
if (!string.IsNullOrEmpty(fileName))
if (fileName.ToUpperInvariant().Contains("RESOURCE.AXD"))
content += RetrieveScript(root + fileName) + Environment.NewLine;
else if (File.Exists(context.Server.MapPath(fileName)))
content += File.ReadAllText(context.Server.MapPath(fileName)) + Environment.NewLine;

if (compress)
if (gZip)
{
response.AppendHeader("Content-Encoding", "gzip");
response.BinaryWrite(GZip(content));
}
else if (contetType != "application/x-javascript")
response.Write(Minimize(content));
else
response.Write(content);
else
response.Write(content);

response.End();

// Cache the resource for 10 minutes
Cache(TimeSpan.FromMinutes(10));
}
}

Web.Config Settings:






No comments:

Post a Comment