Apr 20th 2008 10:30 pm

YSlow and ASP.NET - Concatenating and Minifying Css and Js Resources

In our code we reference several js libraries (prototype, scriptaculous, custom libraries etc.) and we break our css out into several files (layout, fonts, etc.). We encouraged our developers to use whitespace, comments, descriptive names, and separate files to make code as clear as possible. Clear code, however, results in longer download times for web clients.  We wanted to follow the suggestions from the YUI folks to minify and concatenate our resources so that clear code and fast download times could co-exist.

The answer was our automated build. It would have the job of minifying the js and css. It would concatenate the resources together into two files - one for js and one for css.

To use concatenated resources in our code while preserving the ability of developers we work, we changed the the markup section of our master slightly to:

<head runat="server">
	<!-- css -->
	<!--	PLEASE NOTE: CSS and JS ARE COMBINED ON AUTOMATED BUILD
				Please add entries there as necessary!!
	-->
	<link rel="stylesheet" href="Css/Fonts.css" type="text/css" id="FontsCss" runat="server">
	<link rel="stylesheet" href="Css/Structure.css" type="text/css" id="StructureCss" runat="server">
	<link rel="stylesheet" href="Css/Hyperlinks.css;" type="text/css" id="HyperlinksCss" runat="server">
	<link rel="stylesheet" href="Css/Menus.css" type="text/css" id="MenusCss" runat="server">
	<link rel="stylesheet" href="Css/Concatenated.css;" type="text/css" id="ConcatenatedCss" runat="server" visible="false">
	<!-- javascript -->
	<ScriptReference:ScriptRef src="Js/prototype.js" runat="server" ID="UiPrototypeJs">
	<!-- note there are multiple files downloaded after scriptaculous loads, these files should already be included in the concatenated.js if appropriate-->
	<ScriptReference:ScriptRef src="Js/library/scriptaculous.js" runat="server" ID="ScriptaculousJs">
	<ScriptReference:ScriptRef src="Js/library/application.js" runat="server" ID="ApplicationJs">
	<ScriptReference:ScriptRef src="Js/library/Concatenated.js;" runat="server" ID="ConcatenatedScript" Visible="false">
</head>

Note: asp.net doesn’t have a server control for script elements, so we roll our own server control:

Imports System
Imports System.Web.UI       

	Public Class ScriptRef
		Inherits WebControls.Literal       

		Private Const _scriptRefMarkup As String = "<script src="" mce_src=""{0}"" type=""text/javascript""></script>"       

		Public Property Src() As String
			Get
				Return Me.Text
			End Get
			Set(ByVal value As String)
				If Not value Is Nothing Then
					Me.Text = String.Format(Globalization.CultureInfo.InvariantCulture, _scriptRefMarkup, value.Replace("~", System.Web.HttpContext.Current.Request.ApplicationPath))
				End If
			End Set
		End Property       

	End Class

At runtime we can now choose whether to show the css/js files or our concatenated version:

Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
	MyBase.OnInit(e)       

	Dim useConcatenatedJsAndCss As Boolean = False
	If Not String.IsNullOrEmpty(System.Configuration.ConfigurationManager.AppSettings("useConcatenatedJsAndCss")) AndAlso System.Boolean.TryParse(System.Configuration.ConfigurationManager.AppSettings("useConcatenatedJsAndCss"), useConcatenatedJsAndCss) AndAlso useConcatenatedJsAndCss Then
		If useConcatenatedJsAndCss Then
			'NOTE: Whatever we set to visible false here has to be added to the
			'build to be included in the Concatenated css
			FontsCss.Visible = False
			StructureCss.Visible = False
			HyperlinksCss.Visible = False
			MenuCss.Visible = False       

			PrototypeJs.Visible = False
			ScriptaculousJs.Visible = False
			ApplicationJs.Visible = False       

			UiConcatenatedCss.Visible = True
			UiConcatenatedScript.Visible = True
		End If
	End If
End Sub

using the config setting “useConcatenatedJsAndCss”

	<appSettings>
		<add key="useConcatenatedJsAndCss" value="false"/>
	</appSettings>

For minification, we used the YUICompressor. Download it to a place where the build can find it.

<target name="CleanCss">
	<foreach item="File" in="${PathToYourWebSiteBuildOutput}/Css" property="filename">
		<property name="extension" value="${path::get-extension(filename)}">
		<property name="extension2" value="${string::to-lower(extension)}">
		<property name="isCss" value="${string::ends-with(extension2, 'css')}">
		<if test="${isCss}">
			<exec program="java">
				<arg value="-jar">
				<arg value="${YUICompressorPath}">
				<arg value="-o">
				<arg value="${filename}.min">
				<arg value="${filename}">
				<arg value="--warn">
				<arg value="--charset">
				<arg value="Cp1252">
			</exec>
			<move file="${filename}.min" tofile="${filename}" overwrite="true">
		</if>
	</foreach>
</target>       

<target name="CleanJs">
	<foreach item="File" property="filename">
		<in>
			<items>
				<include name="${PathToYourWebSiteBuildOutput}/Javascripts/**/*.js">
			</items>
		</in>
		<do>
			<property name="extension" value="${path::get-extension(filename)}">
			<property name="extension2" value="${string::to-lower(extension)}">
			<property name="isJs" value="${string::ends-with(extension2,'js')}">
			<if test="${isJs}">
				<exec program="java">
					<arg value="-jar">
					<arg value="${YUICompressorPath}">
					<arg value="-o">
					<arg value="${filename}.min">
					<arg value="${filename}">
					<arg value="--preserve-semi">
					<arg value="--charset">
					<arg value="Cp1252">
				</exec>
				<move file="${filename}.min" tofile="${filename}" overwrite="true">
			</if>
		</do>
	</foreach>
</target>

Note: when calling jsmin from the commandline we need to specify the character set for the file we’re compressing or use the default format (ANSI).

For concatenation we can use the build task “concat”. When concatenating files, order is important so we enumerate the files explicitly:

<target name="CombineCss">
	<!-- ordering of concat filesets is nondeterministic, so we concat separately -->
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Css\Concatenated.css" append="false">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Css/Fonts.css">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Css\Concatenated.css" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Css/structure.css">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Css\Concatenated.css" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Css/Hyperlinks.css">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Css\Concatenated.css" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Css/Menus.css">
		</fileset>
	</concat>
</target>     

<target name="CombineJs">
	<!-- ordering of concat filesets is nondeterministic, so we concat separately -->
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/prototype.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/scriptaculous.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/crir.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/builder.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/effects.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/dragdrop.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/controls.js">
		</fileset>
	</concat>
	<concat destfile="${WebSiteStaticContentVersionDirectoryPath}\Javascripts\library\Concatenated.js" append="true">
		<fileset>
			<include name="${WebSiteStaticContentVersionDirectoryPath}/Javascripts/library/slider.js">
		</fileset>
	</concat>
</target>

1 Comment »

One Response to “YSlow and ASP.NET - Concatenating and Minifying Css and Js Resources”

  1. Rob on 13 Aug 2008 at 4:53 pm #

    We do virtually the same to merge and compress our CSS and JavaScript.

    Although we use a .bat file for doing the merging and calling YUICompressor (called in our nant files during compilation). Which in reflection, I think you’re use of nant commands is much better (using concat and then calling YUICompressor via nant). Batch files can be a bit fiddly. If I get time I’ll change ours to be similar to yours. Cheers.

Trackback URI | Comments RSS

Leave a Reply

« YSlow and ASP.NET - Expires Header (Part 1 of 3) | YSlow and ASP.NET - Expires Header - Expression Builders (Part 2 of 3) »