Community Pick: Many members of our community have endorsed this article.

Minify and Concatenate Your Scripts and Stylesheets

Published:
Updated:
Today is the age of broadband.  More and more people are going this route determined to experience the web and it’s multitude of services as quickly and painlessly as possible. Coupled with the move to broadband, people are experiencing the web via their mobile devices – and it is this which most developers/designers forget.

With mobile devices becoming mainstream, it is more important than ever that we as designers and developers convey the importance of optimized web sites to our clients. With this in mind, I would like to introduce some strategies I use when developing a website, or web application.

#1 Make sure no matter how many CSS stylesheets you use, they are placed in between your <head> tags.  This will make sure that your styling gets rendered by the browser in a timely manner relative to the rendering of the page.

#2 Wherever possible, combine your stylesheets into one.  This will decrease page load time by cutting down the parrellel downloads the browser has to make to request your page.  If you absolutely must have mutliple stylesheets, see below for a code snippet that will do this on the fly for you.  (written in VB.NET)

#3 We all love javascript, I personally am a big fan of jQuery, and the jQuery UI.  Make sure that no matter how many javascript includes you have, they are placed at the very bottom of the page right before the </body> tag.  This will make sure the page is rendered before any javascript can interupt the document.

#4 Wherever possible, combine your script files into one.  This will decrease page load time by cutting down on the parrellel downloads the browser has to make to request your page.  If you absolutely must have mutliple script files, see below for a code snippet that will do this on the fly for you.  (written in VB.NET)

#5 Minify both your scripts and stylesheets to achieve optimal download times.  This will help your pages load faster – less size, less time to render the page.

#6 Enable GZip encoding.  This will further compress your files making for a faster loading page.

#7 Wherever possible: Cache, Cache, Cache.  I can't say it enough to emphasize how important it is to leverage both server and client caching.  This will reduce the number of times your browser has to hit the server for a page request.

Now without further ado, is some handy, helpful code that will take your page output, parse it for script and css linked stylesheets, combine them into one "file" each, "minify" them, "cache" them and then render them to the browser.

To be honest, there may be better methods to do this minification, however, in my 'production' environment, what I have listed below is what has worked best for me.

This main processing of this project contains five files, and for testing purposes I added four javascript files, two stylesheets, and a default page to test with.

First things first, let's get the common stuff out of the way. I have aptly named this Common.vb and this should be placed in your 'App_Code' directory:
Imports Microsoft.VisualBasic
                      
                      ''' <summary>
                      ''' Our common class file
                      ''' </summary>
                      ''' <remarks></remarks>
                      Public Class Common
                      
                          ''' <summary>
                          ''' Let's cache this stuff
                          ''' </summary>
                          ''' <remarks></remarks>
                          Public Shared Sub CacheIt()
                              ' Allow the browser to store resposes in the 'History' folder
                              HttpContext.Current.Response.Cache.SetAllowResponseInBrowserHistory(True)
                              ' Set our cacheability to Public
                              HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.Public)
                              ' Resource is valid until the expiration has passed
                              HttpContext.Current.Response.Cache.SetValidUntilExpires(True)
                              ' Set out last modified date to last year
                              HttpContext.Current.Response.Cache.SetLastModified(Date.Now.AddDays(-366))
                              ' We want to store and cache the resource
                              HttpContext.Current.Response.AddHeader("Cache-Control", "store, cache")
                              ' Set the Pragma to cache
                              HttpContext.Current.Response.AddHeader("pragma", "cache")
                              ' Not sure if this one really works, but it doesn't throw an error, and Google likes resources served from a cookie-less domain... eh... worth a shot
                              HttpContext.Current.Response.AddHeader("Set-Cookie", "false")
                              ' Make sure our cache control is Public
                              HttpContext.Current.Response.CacheControl = "public" '
                              'Set the expiration date of the resource until next year
                              HttpContext.Current.Response.Expires = 24 * 60 * 366
                              HttpContext.Current.Response.ExpiresAbsolute = DateAdd(DateInterval.Hour, 24 * 366, Date.Now)
                          End Sub
                      
                          ''' <summary>
                          ''' Let's check to see if the browser accepts GZip encoding
                          ''' </summary>
                          ''' <returns></returns>
                          ''' <remarks></remarks>
                          Public Shared Function IsGZipEnabled() As Boolean
                              Dim accEncoding As String = HttpContext.Current.Request.Headers("Accept-Encoding")
                              'Does the browser accept content encoding?
                              If (Not accEncoding Is Nothing) Then
                                  If (accEncoding.Contains("gzip") Or accEncoding.Contains("deflate")) Then
                                      Return True
                                  Else
                                      Return False
                                  End If
                              Else
                                  Return False
                              End If
                          End Function
                      
                      End Class
                      

Open in new window


Now let's get down to business.  This is the code that will do the concatenation and minification.  Place it in your 'App_Code' folder. Yes folks, it is an HttpModule:
 
Imports System.IO.Compression
                      Imports System.Web
                      Imports System.Text.RegularExpressions
                      Imports System.Text
                      Imports System.IO
                      Imports System.Web.Compilation
                      
                      ''' <summary>
                      ''' Dynamic compression mechanism for some of the sites content
                      ''' </summary>
                      ''' <remarks></remarks>
                      Public Class DynamicCompression
                          Implements IHttpModule
                      
                          ''' <summary>
                          ''' Initialize our module
                          ''' </summary>
                          ''' <param name="application"></param>
                          ''' <remarks></remarks>
                          Public Sub Init(ByVal application As HttpApplication) _
                              Implements IHttpModule.Init
                              'Add the BeginRequest Handler
                              AddHandler application.BeginRequest, _
                                  AddressOf Me.Application_BeginRequest
                              Dim context As HttpContext = application.Context
                              'Let's see if the browser accepts GZip encoding, if it does add the header to the response
                              If Common.IsGZipEnabled() Then
                                  Dim accEncoding As String = context.Request.Headers("Accept-Encoding")
                                  If accEncoding.Contains("gzip") Then
                                      context.Response.Filter = New GZipStream(context.Response.Filter, CompressionMode.Compress)
                                      context.Response.AppendHeader("Content-Encoding", "gzip")
                                  Else
                                      context.Response.Filter = New DeflateStream(context.Response.Filter, CompressionMode.Compress)
                                      context.Response.AppendHeader("Content-Encoding", "deflate")
                                  End If
                              End If
                          End Sub
                      
                          ''' <summary>
                          ''' Begin the request and process our filter
                          ''' </summary>
                          ''' <param name="source"></param>
                          ''' <param name="e"></param>
                          ''' <remarks></remarks>
                          Private Sub Application_BeginRequest(ByVal source As Object, ByVal e As EventArgs)
                              Dim application As HttpApplication = DirectCast(source, HttpApplication)
                              Dim context As HttpContext = application.Context
                              Dim filePath As String = context.Request.FilePath
                              Dim fileExtension As String = VirtualPathUtility.GetExtension(filePath)
                              If fileExtension.Equals(".aspx") Then
                                  application.Response.Filter = New Worker(application.Response.Filter, source)
                              End If
                          End Sub
                      
                          ''' <summary>
                          ''' Unneeded for our purposes
                          ''' </summary>
                          ''' <remarks></remarks>
                          Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
                          End Sub
                      
                          ''' <summary>
                          ''' Let's do the actual work
                          ''' </summary>
                          ''' <remarks></remarks>
                          Private Class Worker
                              Inherits Stream
                      
                              ''' <summary>
                              ''' Get our application context and file stream
                              ''' </summary>
                              ''' <param name="sink"></param>
                              ''' <param name="source"></param>
                              ''' <remarks></remarks>
                              Public Sub New(ByVal sink As Stream, ByVal source As Object)
                                  _sink = sink
                                  Dim application As HttpApplication = DirectCast(source, HttpApplication)
                                  context = application.Context
                              End Sub
                      
                      #Region "Properites"
                      
                              ''' <summary>
                              ''' These properties are a necessary implementation when we inherit IO.Stream
                              ''' </summary>
                              ''' <value></value>
                              ''' <returns></returns>
                              ''' <remarks></remarks>
                      
                              Public Overrides ReadOnly Property CanRead() As Boolean
                                  Get
                                      Return True
                                  End Get
                              End Property
                      
                              Public Overrides ReadOnly Property CanSeek() As Boolean
                                  Get
                                      Return True
                                  End Get
                              End Property
                      
                              Public Overrides ReadOnly Property CanWrite() As Boolean
                                  Get
                                      Return True
                                  End Get
                              End Property
                      
                              Public Overrides Sub Flush()
                                  _sink.Flush()
                              End Sub
                      
                              Public Overrides ReadOnly Property Length() As Long
                                  Get
                                      Return 0
                                  End Get
                              End Property
                      
                              Private _position As Long
                              Public Overrides Property Position() As Long
                                  Get
                                      Return _position
                                  End Get
                                  Set(ByVal value As Long)
                                      _position = value
                                  End Set
                              End Property
                      
                              Private _sink As Stream
                              Private context As HttpContext
                              Private html As String, FileCSS As String, FileJS As String
                      
                      #End Region
                      
                      #Region "Methods"
                      
                              ''' <summary>
                              ''' Most of these methods are here because they are necessary for inheriting IO.Stream
                              ''' They will make sure we are handling the stream correctly
                              ''' </summary>
                              ''' <param name="buffer"></param>
                              ''' <param name="offset"></param>
                              ''' <param name="count"></param>
                              ''' <returns></returns>
                              ''' <remarks></remarks>
                      
                              Public Overrides Function Read(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer) As Integer
                                  Return _sink.Read(buffer, offset, count)
                              End Function
                      
                              Public Overrides Function Seek(ByVal offset As Long, ByVal origin As SeekOrigin) As Long
                                  Return _sink.Seek(offset, origin)
                              End Function
                      
                              Public Overrides Sub SetLength(ByVal value As Long)
                                  _sink.SetLength(value)
                              End Sub
                      
                              Public Overrides Sub Close()
                                  _sink.Close()
                              End Sub
                      
                              ''' <summary>
                              ''' Overwride the default response write, so we can pass the stream back to the browser
                              ''' </summary>
                              ''' <param name="buffer__1"></param>
                              ''' <param name="offset"></param>
                              ''' <param name="count"></param>
                              ''' <remarks></remarks>
                              Public Overrides Sub Write(ByVal buffer__1 As Byte(), ByVal offset As Integer, ByVal count As Integer)
                                  Dim data As Byte() = New Byte(count - 1) {}
                                  Buffer.BlockCopy(buffer__1, offset, data, 0, count)
                                  html = System.Text.Encoding.[Default].GetString(buffer__1)
                                  'CSS
                                  CompressStyles(html)
                                  'JS
                                  CompressJavascript(html)
                                  Dim outdata As Byte() = System.Text.Encoding.[Default].GetBytes(html)
                                  _sink.Write(outdata, 0, outdata.GetLength(0))
                              End Sub
                      
                              ''' <summary>
                              ''' Grab our stylesheets from the file stream string
                              ''' concatenate it, minify it, and pass back the originating string while replacing all the stylesheet references
                              ''' with a single refernce to our new stylesheet file
                              ''' </summary>
                              ''' <param name="StrFile"></param>
                              ''' <remarks></remarks>
                              Private Sub CompressStyles(ByVal StrFile As String)
                                  Dim FilesM As MatchCollection, FileName As String
                                  FilesM = Regex.Matches(StrFile, "<link.*?href=""(.*?)"".*? />")
                                  Dim M(FilesM.Count - 1) As String
                                  For i As Long = 0 To M.Length - 1
                                      'This is the file
                                      M(i) = FilesM(i).Groups(1).Value
                                      FileName = HttpContext.Current.Server.MapPath(M(i).Replace("/", "\"))
                                      FileName = FileName.Replace("\\\", "\")
                                      If File.Exists(FileName) Then
                                          Using objFile As New StreamReader(FileName)
                                              FileCSS += objFile.ReadToEnd
                                              objFile.Close()
                                          End Using
                                          FileCSS = Regex.Replace(FileCSS, "/\*.+?\*/", "", RegexOptions.Singleline)
                                          FileCSS = FileCSS.Replace("  ", "")
                                          FileCSS = FileCSS.Replace(vbCr, "")
                                          FileCSS = FileCSS.Replace(vbLf, "")
                                          FileCSS = FileCSS.Replace(vbCrLf, "")
                                          FileCSS = FileCSS.Replace(vbTab, " ")
                                          FileCSS = FileCSS.Replace("\t", "")
                                          FileCSS = FileCSS.Replace(" {", "{")
                                          FileCSS = FileCSS.Replace("{ ", "{")
                                          FileCSS = FileCSS.Replace(" }", "}")
                                          FileCSS = FileCSS.Replace("} ", "}")
                                          FileCSS = FileCSS.Replace(" :", ":")
                                          FileCSS = FileCSS.Replace(": ", ":")
                                          FileCSS = FileCSS.Replace(", ", ",")
                                          FileCSS = FileCSS.Replace("; ", ";")
                                          FileCSS = FileCSS.Replace(";}", "}")
                                          FileCSS = Regex.Replace(FileCSS, "/\*[^\*]*\*+([^/\*]*\*+)*/", "$1")
                                      End If
                                  Next
                                  FilesM = Nothing
                                  If context.Cache("CSS") Is Nothing Then
                                      context.Cache.Add("CSS", FileCSS, Nothing, DateTime.Now.AddDays(31), Cache.NoSlidingExpiration, CacheItemPriority.High, Nothing)
                                  End If
                                  Dim tmp As MatchCollection
                                  tmp = Regex.Matches(StrFile, "<link.*?href=""(.*?)"".*? />")
                                  Dim tmpCt As Long = 0
                                  For Each tmpS As Match In tmp
                                      tmpCt += 1
                                      If tmpCt = tmp.Count Then
                                          StrFile = StrFile.Replace(tmpS.Groups(0).ToString(), "<link type=""text/css"" rel=""stylesheet"" href=""/Style.aspx"" />")
                                      Else
                                          StrFile = StrFile.Replace(tmpS.Groups(0).ToString(), String.Empty)
                                      End If
                                  Next
                                  tmp = Nothing
                                  html = StrFile
                              End Sub
                      
                              ''' <summary>
                              ''' Grab our scripts from the file stream string
                              ''' concatenate it, minify it, and pass back the originating string while replacing all the scripts references
                              ''' with a single refernce to our new scripts file
                              ''' </summary>
                              ''' <param name="StrFile"></param>
                              ''' <remarks></remarks>
                              Private Sub CompressJavascript(ByVal StrFile As String)
                                  Dim FilesM1 As MatchCollection, FileName As String
                                  FilesM1 = Regex.Matches(StrFile, "<script.*?src=""(.*?)"".*?></script>")
                                  Dim M1(FilesM1.Count - 1) As String
                                  For j As Long = 0 To M1.Length - 1
                                      'This is the file
                                      M1(j) = FilesM1(j).Groups(1).Value
                                      FileName = HttpContext.Current.Server.MapPath(M1(j).Replace("/", "\"))
                                      FileName = FileName.Replace("\\\", "\")
                                      If File.Exists(FileName) Then
                                          Using objFile1 As New StreamReader(FileName)
                                              FileJS += objFile1.ReadToEnd
                                              objFile1.Close()
                                          End Using
                                          FileJS = Regex.Replace(FileJS, "(// .*?$)", "", RegexOptions.Multiline)
                                          FileJS = Regex.Replace(FileJS, "(/\*.*?\*/)", "", RegexOptions.Multiline)
                                          FileJS = FileJS.Replace("  ", " ")
                                          FileJS = FileJS.Replace(vbCr, vbLf)
                                          FileJS = FileJS.Replace(vbCrLf, vbLf)
                                          FileJS = FileJS.Replace(vbTab, " ")
                                      End If
                                  Next
                                  If context.Cache("JS") Is Nothing Then
                                      context.Cache.Add("JS", FileJS, Nothing, DateTime.Now.AddDays(31), Cache.NoSlidingExpiration, CacheItemPriority.High, Nothing)
                                  End If
                                  FilesM1 = Nothing
                                  Dim tmp1 As MatchCollection
                                  tmp1 = Regex.Matches(StrFile, "<script.*?src=""(.*?)"".*?></script>")
                                  Dim tmpCt1 As Long = 0
                                  For Each tmpS As Match In tmp1
                                      tmpCt1 += 1
                                      If tmpCt1 = tmp1.Count Then
                                          StrFile = StrFile.Replace(tmpS.Groups(0).ToString(), "<script type=""text/javascript"" src=""/Script.aspx""></script>")
                                      Else
                                          StrFile = StrFile.Replace(tmpS.Groups(0).ToString(), String.Empty)
                                      End If
                                  Next
                                  tmp1 = Nothing
                                  html = StrFile
                              End Sub
                      
                      #End Region
                      
                          End Class
                      
                      End Class
                      

Open in new window


Once you have added in the module, you will need to make a modification to register the module for the application.

This section goes in your web.config file for the site:
<add name="DynamicCompression" type="DynamicCompression"/>
                      

Open in new window


and needs to be in the <system.web><httpModules> section.


Now that we have the grunts out of the way, let's add in our 'extra' files that will be needed to process these resources:

First we have Script.aspx which needs to be placed in the root of the website.
 
<%@ Page Language="VB" %>
                      <% 
                          'Cache our response in the browser
                          Common.CacheIt()
                          'Set the content type to output true CSS
                          Response.ContentType = "text/css"
                          If Not (Cache("CSS") Is Nothing) Then
                              'Write it out from the cache
                              Response.Write(Cache("CSS").ToString())
                          End If
                          %>
                      

Open in new window


Next we'll add in Script.aspx and that needs to be placed in the root of the website as well.
 
<%@ Page Language="VB" %>
                      <% 
                          'Cache our response in the browser
                          Common.CacheIt()
                          'Set the content type to output true javascript
                          Response.ContentType = "text/javascript"
                          If Not (Cache("JS") Is Nothing) Then
                              'write out the javascript string from our cache
                              Response.Write(Cache("JS").ToString())
                          End If
                          %>
                      

Open in new window


All that is left is to include your stylesheets and javascript includes and you are good to go.
Here is our Default.aspx that is the default page for the site...real simple:
 
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" Debug="true" %>
                      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
                      <html xmlns="http://www.w3.org/1999/xhtml">
                          <head>
                              <title>Untitled Page</title>
                              <meta http-equiv="Content-Script-Type" content="text/javascript" />
                              <meta http-equiv="Content-Style-Type" content="text/css" />
                              <link href="/styles/layout1.css" rel="stylesheet" type="text/css" />
                              <link href="/styles/layout2.css" rel="stylesheet" type="text/css" />
                          </head>
                          <body>
                              Just a test to make sure the CSS and JS works
                              <a href="javascript:;" onclick="alert($('body').html());">Click Here</a>
                              <script type="text/javascript" src="/scripts/jquery.js"></script>
                              <script type="text/javascript" src="/scripts/jquery.pngFix.pack.js"></script>
                              <script type="text/javascript" src="/scripts/jquery.preloadcssimages.js"></script>
                              <script type="text/javascript" src="/scripts/custom.js"></script>
                          </body>
                      </html>
                      

Open in new window




And that would be that.  Compile the site and run it, and you will see the benefits of the work at hand.

Please make sure to leave me any suggestions, improvements, and/or comments you may have, as I am always open for improvements.

Happy Coding!

p.s.  Here is the complete project:   Test.zip
4
5,375 Views

Comments (2)

Author

Commented:
Great q from 'TheLearnedOne'

"I have one question about "App_Code".  That folder is usually only seen with "Web Site" model web sites, which don't have a project file.  I don't see any mention of this, which might confuse someone who has a "Web Application" model, which won't normally have that folder, unless it is created manually."

This project was initially built as a 'Web Site', and not a 'Web Application'.  Most of the projects I build, are 'Web Applications', and thus (as TheLearnedOne states) does not automagically pop in the 'App_Code' folder.

So, there are 2 solutions to this quandry.  One is to manually create the 'App_Code' folder, if you do not have one, and pop in the code as I state above...  or, if most are like me.  Put your class files in your Class Assembly.

Author

Commented:
I need to mention this as well.  This method will not work if you use 'Master Pages' in your site.  (I have not tried 'Themes' or 'Skins').

It appears Master Pages are rendered prior to the page content being rendered.  I do have an idea of how a work around may be accomplished, but I will save that for a later post (in this article), I just need to build and test it before I can say one way or another.

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.