<

Go Premium for a chance to win a PS4. Enter to Win

x

Minify and Concatenate Your Scripts and Stylesheets

Published on
10,357 Points
3,957 Views
4 Endorsements
Last Modified:
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
Comment
Author:kevp75
  • 2
2 Comments
 
LVL 25

Author Comment

by:kevp75
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.
0
 
LVL 25

Author Comment

by:kevp75
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.
0

Featured Post

Free Tool: Subnet Calculator

The subnet calculator helps you design networks by taking an IP address and network mask and returning information such as network, broadcast address, and host range.

One of a set of tools we're offering as a way of saying thank you for being a part of the community.

Join & Write a Comment

This Micro Tutorial will teach you how to add a cinematic look to any film or video out there. There are very few simple steps that you will follow to do so. This will be demonstrated using Adobe Premiere Pro CS6.
Are you ready to place your question in front of subject-matter experts for more timely responses? With the release of Priority Question, Premium Members, Team Accounts and Qualified Experts can now identify the emergent level of their issue, signal…
Suggested Courses

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month