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.
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
<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;
}
}