This blog contains reflections and thoughts on my work as a software engineer

mandag den 14. december 2009

Optimizing website performance: Using ASP.NET Webcontrols to combine your CSS and JS files

So – this month we’ve been going through a performance optimization based on the suggestions provided by YSlow. There is no absolute truth in performance so the suggestions provided by YSlow should be put in a context but it is great for inspiration and lots of the advices proposed do make a lot of sense.

Untitled

One of the things we’ve pinpointed was the massive amount of files needed to load our front page (50+) so we are working on sprites as replacement for single files and I’ve been working on a solution to combine our CSS and Javascript files into one single style  The number of physical files you need to download to view a webpage is important because requests get queued and the browser consumes only two downloads in parallel – every other request sits waiting in the queue so the more requests you make the slower your website is going to be.

We needed a solution and my collegue proposed an ASP.NET Webcontrol which would act as a placeholder for our styles. After 2 days of work I came up with control made for for CSS and Javascript files which can:

  • Combine any number of CSS /Javascript files into one combined file
  • Output either combined file or single files (debug mode)
  • Removes whitespace if needed
The control ended up like the following snippet:
   <cc1:StaticFileCollection runat="server" ID="cssCollection" StaticFileType="CSS" Outputfile="/css/FrontpageAspxCssCollection.css" TrimWhiteSpace="true">      
    <cc1:StaticFile ID="StaticFile1" runat="server" Url="/script/ext-2.0/resources/css/form.css"  />
    <cc1:StaticFile ID="StaticFile2" runat="server" Url="/script/ext-2.0/resources/css/combo.css"  />
    <cc1:StaticFile ID="StaticFile3" runat="server" Url="/css/global.css"  />
    <cc1:StaticFile ID="StaticFile4" runat="server" Url="/css/article.css"  />
    <cc1:StaticFile ID="StaticFile5" runat="server" Url="/css/boxes.css"  />
    <cc1:StaticFile ID="StaticFile6" runat="server" Url="/css/ext-overrides.css"  />       
</cc1:StaticFileCollection>



What happens on PreRender is that every StaticFile is being opened and placed in a StringBuilder. It is being output in the file “/css/FrontpageAspxCssCollection.css” if the size has changed (that means that one of the sourcefiles have been altered, i.e. during development). A reference to /css/FrontpageAspxCssCollection.css is written in a Literal control. Plain and simple – no magic attached. So if you place the StaticFileCollection above in you <head> section of your webpage what you get back is this:

<link href="/css/FrontpageAspxCssCollection.css" rel="stylesheet" type="text/css">

…where the FrontPageAspxCssCollection.css is all your StaticFile’s combined into one single, physical file.

The code works for both Javascript and CSS styles. Enjoy   :o)

    [ToolboxData("<{0}:StaticFile runat=\"server\"></{0}:STATICFILE>")]
public class StaticFile : WebControl
{
public string Url { get; set; }

public override bool Visible
{
get
{
return false;
}
set
{
base.Visible = value;
}
}
}



[ToolboxData("<{0}:StaticFileCollection runat=\"server\"></{0}:StaticFileCollection>")]
public class StaticFileCollection : PlaceHolder
{
    private string _staticFileType;
    public string StaticFileType { get { return _staticFileType.ToLower();} set { _staticFileType = value;} }
    public bool TrimWhiteSpace { get; set; }
    public bool Debug { get; set; }
    public string Outputfile { get; set; }
    protected override void OnPreRender(EventArgs e)
    {        
        if (!CheckInput())
            return;
        if (Debug)           
            DoNotRenderCombinedFile();                       
        else                   
            RenderCombinedFile();


        base.OnPreRender(e);
    }

    /// <summary>
    /// Render every file in separate
    /// </summary>
    private void DoNotRenderCombinedFile()
    {
        var controls = new List<Control>();
        foreach (var control in Controls)
        {
            var c = control as StaticFile;
            controls.Add(new LiteralControl(GetScriptReference(c.Url)));
        }
        controls.ForEach(x => Controls.Add(x));
    }
    /// <summary>
    /// Render all files combined to OutputFile
    /// </summary>
    private void RenderCombinedFile()
    {
        var combinedFileString = CollectFileContent();
        string outputFile = HttpContext.Current.Server.MapPath("/" + Outputfile);
        //Get current file
        string currentContent = string.Empty;
        var currentFile = new FileInfo(outputFile);
        if (currentFile.Exists)
        {
            using (var sr = currentFile.OpenText())
                currentContent = sr.ReadToEnd();
        }
        if (TrimWhiteSpace)
        {
            var r = new Regex("\\s+", RegexOptions.Multiline);
            combinedFileString = r.Replace(combinedFileString, @" ");
        }
        //Only create new file if content has changed to maintain timestamp (avoid download to client on every hit)
        if (currentContent.Length != combinedFileString.Length)
        {              
            using (var sw = new StreamWriter(outputFile))
            {
                sw.Write(combinedFileString);
                sw.Close();
            }
        }
        Controls.Add(new LiteralControl(GetScriptReference(Outputfile)));       
    }
    private string CollectFileContent()
    {
        var cssBuilder = new StringBuilder();
        DateTime begin = DateTime.Now;
        foreach (var control in Controls)
        {
            FileInfo fi = GetFileInfoObject(control);
            using (var content = fi.OpenText())
            {
                cssBuilder.Append(string.Format("/*** {0} start ***/", fi.Name));
                cssBuilder.Append(content.ReadToEnd());
                cssBuilder.Append(string.Format("/*** {0} end ***/", fi.Name));
                cssBuilder.Append("");
            }
        }
        return cssBuilder.ToString();
    }
    private FileInfo GetFileInfoObject(object control)
    {
        var c = control as StaticFile;
        if (c == null)
            throw new ArgumentException("Only StaticFile controls can be childresn to a StaticFileCollection");
        var fi = new FileInfo(HttpContext.Current.Server.MapPath(c.Url));
        if (!fi.Exists)
            throw new ArgumentException(c.Url + " does not exist!");
        if (fi.Extension.ToLower() != "." + StaticFileType)
            throw new ArgumentException(string.Format("{0} is not of type {1}", c.Url, StaticFileType));
        return fi;
    }
    private string GetScriptReference(string url)
    {
        //Not so nice... But a strategy pattern impl. is way overkill
        string str = string.Format("<link type=\"text/css\" rel=\"stylesheet\" href=\"{0}\" />", url);
        if (StaticFileType.Equals("js"))
            str = string.Format("<script type=\"text/javascript\" src=\"{0}\" />", url);
        return str;           
    }


    private bool CheckInput()
    {
        if (string.IsNullOrEmpty(Outputfile))
            throw new ArgumentNullException("Outputfile must be set on a StaticFileCollection");
        if (StaticFileType != "js" && StaticFileType != "css")
            throw new ArgumentNullException("StaticFileType should be either JS eller CSS");
        return true;
    }
}