Click here to Skip to main content
Click here to Skip to main content

Reduce The Size Of Your ASP.NET Output

By , 26 Jan 2004
 

Reduce your output size by 7% with one class and one line of code.

Introduction

This article demonstrates how to use HttpResponse.Filter to easily reduce the output size of your website.

Background

Recently, we redesigned the web site for Layton City. Because the redesign made it much easier for citizens to find what they were looking for, our hits per day nearly tripled overnight. Unfortunately, so did our bandwidth. We're currently serving almost 60Mb a day of just HTML. That doesn't include images or Adobe® Reader® documents. So priority #1 became reducing our bandwidth without reducing usability or having to rewrite the majority of our pages.

One downside of using some of the ASP.NET controls is that they insert lots of whitespace characters so that developers can easily see where problems are. While that is desirable during debugging, there is no means of turning that functionality off when you have released your site.

After finding an article on HttpResponse.Filter in the Longhorn SDK (here), we decided to use HttpResponse.Filter to intercept our outgoing HTML and squish it.

Using the code

Add the WhitespaceFilter class to your project, and add the following line of code into the Application_BeginRequest function in your Global.asax file:

Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
    Response.Filter = New WhitespaceFilter(Response.Filter)
End Sub

The above code causes the compressor to be added to every single page in your application. Alternatively, if you only want to compress individual pages, you can add the line to the Page_Load event.

Whitespace.vb

Comments are inline. Some of the weird lines are in to help compress specific portions of the website. (Updated 1/23/2004)

Imports System.IO
Imports System.Text.RegularExpressions 

' This filter gets rid of all unnecessary whitespace in the output.

Public Class WhitespaceFilter
    Inherits Stream

    Private _sink As Stream
    Private _position As Long

    Public Sub New(ByVal sink As Stream)
        _sink = sink
    End Sub 'New

#Region " Code that will most likely never change from filter to filter. "
    ' The following members of Stream must be overridden.
    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 ReadOnly Property Length() As Long
        Get
            Return 0
        End Get
    End Property

    Public Overrides Property Position() As Long
        Get
            Return _position
        End Get
        Set(ByVal Value As Long)
            _position = Value
        End Set
    End Property

    Public Overrides Function Seek(ByVal offset As Long, _ 
           ByVal direction As System.IO.SeekOrigin) As Long
        Return _sink.Seek(offset, direction)
    End Function 'Seek

    Public Overrides Sub SetLength(ByVal length As Long)
        _sink.SetLength(length)
    End Sub 'SetLength

    Public Overrides Sub Close()
        _sink.Close()
    End Sub 'Close

    Public Overrides Sub Flush()
        _sink.Flush()
    End Sub 'Flush

    Public Overrides Function Read(ByVal MyBuffer() As Byte, _ 
      ByVal offset As Integer, ByVal count As Integer) As Integer
        _sink.Read(MyBuffer, offset, count)
    End Function

#End Region

    ' Write is the method that actually does the filtering.

    Public Overrides Sub Write(ByVal MyBuffer() As Byte, _ 
             ByVal offset As Integer, ByVal count As Integer)
        Dim data(count) As Byte
        Buffer.BlockCopy(MyBuffer, offset, data, 0, count)

        ' Don't use ASCII encoding here.  The .NET IDE replaces
        ' some characters, such as ®
        ' with a UTF-8 entity.  If you use ASCII encoding,
        ' you'll get B. instead of the registered
        ' trademark symbol.
        Dim s As String = System.Text.Encoding.UTF8.GetString(data)

        ' Replace control characters with either spaces or nothing

        ' The funky semi-colon handling is there because
        ' of a JavaScript comment in a component.
        ' This way, we keep the carriage returns that actually matter.
        s = s.Replace(ControlChars.Cr, _ 
              Chr(255)).Replace(ControlChars.Lf, _ 
              "").Replace(ControlChars.Tab, "")
        s = s.Replace(";" & Chr(255), ";" & ControlChars.Cr)
        s = s.Replace(Chr(255), " ")

        ' Eliminate excess whitespace.
        Do
            s = s.Replace("  ", " ")
        Loop Until s.IndexOf("  ") = -1

        ' Eliminate known comments.

        ' We use three comments in our template. These comments
        ' go on every single page on the site.
        ' Obviously, we can kill them when they are going out.
        ' This way, the comments stay in for
        ' maintenance, but are trimmed before release.
        s = s.Replace("<!-- Page Content Goes Above Here -->", "")
        s = s.Replace("<!-- Page Content Goes Below Here -->", "")
        s = s.Replace("<!-- Do not get rid of this   on data pages -->", "")

        ' Eliminate some additional whitespace we can kill

        ' For some reason, a single space gets emitted
        ' before each of our DOCTYPE directives.
        s = s.Replace(" <!DOCTYPE", "<!DOCTYPE")

        ' These are the most common excess whitespace items we can remove.
        s = s.Replace("<li> ", _ 
              "<li>").Replace("</td> ", _ 
              "</td>").Replace("</tr> ", _ 
              "</tr>").Replace("</ul> ", _ 
              "</ul>").Replace("</table> ", _ 
              "</table>").Replace("</li> ", "</li>")
        s = s.Replace("<LI> ", _ 
              "<LI>").Replace("</TD> ", _
              "</TD>").Replace("</TR> ", _ 
              "</TR>").Replace("</UL> ", _ 
              "</UL>").Replace("</TABLE> ", _
              "</TABLE>").Replace("</LI> ", "</LI>")
        s = s.Replace("<td> ", _
              "<td>").Replace("<tr> ", _
              "<tr>")
        s = s.Replace("<TD> ", _ 
              "<TD>").Replace("<TR> ",_ 
              "<TR>")
        s = s.Replace("<P> ", "<P>").Replace("<p> ", "<p>")
        s = s.Replace("</P> ", "</P>").Replace("</p> ", "</p>")
        s = s.Replace("style=""display:inline""> ", _ 
              "style=""display:inline"">")
        s = s.Replace(" <H", "<H").Replace(" <h", _ 
              "<h").Replace(" </H", _
              "</H").Replace(" </h", "</h")
        s = s.Replace("<UL> ", "<UL>").Replace("<ul> ", "<ul>")
        s = s.Replace(" <TABLE", _ 
              " ID="Table1"<TABLE").Replace(" ID="Table2"<table", _
              " ID="Table3"<table")
        s = s.Replace(" ID="Table4"<li>", _
              "<li>").Replace(" <LI>", "<LI>")
        s = s.Replace(" <br>", _ 
              "<br>").Replace(" <BR>",_ 
              "<BR>").Replace("<br> ", _
              "<br>").Replace("<BR> ", "<BR>")
        s = s.Replace(" <ul>", "<ul>").Replace(" <UL>", "<UL>")

        ' Replace long tags with short ones
        s = s.Replace("<STRONG>", "<B>").Replace("<strong>", "<b>")
        s = s.Replace("</STRONG>", "</B>").Replace("</strong>", "</b>")

        ' Replace some HTML entities with true character codes
        s = s.Replace("&brkbar;", "|")
        s = s.Replace("¦", "|")
        s = s.Replace("&shy;", "-")
        s = s.Replace(" ", Chr(160))
        s = s.Replace("&lsquor;", "'")
        s = s.Replace("&ldquor;", """")
        s = s.Replace("‘", "'")
        s = s.Replace("&rsquor;", "'")
        s = s.Replace("’", "'")
        s = s.Replace("“", """")
        s = s.Replace("&rdquor;", """")
        s = s.Replace("”", """")
        s = s.Replace("–", "-")
        s = s.Replace("&endash;", "-")

        ' If we don't do this, JavaScript horks on the site
        s = s.Replace("<!--", "<!--" & ControlChars.Cr)
        s = s.Replace("}", "}" & ControlChars.Cr)

        ' Last chance to eliminate excess whitespace
        Do
            s = s.Replace("  ", " ")
        Loop Until s.IndexOf("  ") = -1

        ' Finally, we spit out what we have done.
        Dim outdata() As Byte = System.Text.Encoding.UTF8.GetBytes(s)
        _sink.Write(outdata, 0, outdata.GetLength(0))

    End Sub 'Write 

End Class

Points of Interest

Occasionally, you will find that you have one or more pages that you do not want to compress. For example, the pages may use pre-formatted text or the pages may emit binary data instead of HTML.

In that case, you would want to filter the filter, so to speak. On our site, we have one page that we don't compress, so our Application_BeginRequest looks a little bit like this...

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)

        ' ...non-related code trimmed...

        ' Whitespace Reduction
        If Request.Url.PathAndQuery.ToLower.IndexOf("makethumbnail") = -1 Then
            Response.Filter = New WhitespaceFilter(Response.Filter)
        End If
    End Sub

Using this class will increase the amount of processing time used for each page. In our case, the reduction in bandwidth (7% on our main page, as much as 30% on some of our more complex pages) was worth the increased workload on the server. All of the string operations are very inefficient, admittedly. A rewrite to use StringBuilder is in the works. The only downside to StringBuilder is that you can't run regular expressions against it. However, because of the use of Strings in the current version, I do not recommend using it if the HTML on your page is greater than 80,000 bytes on average, due to the behavior of the .NET Framework's garbage collector. Essentially, any object greater than 80,000 bytes will be immediately pushed into the Large Object Heap, which is only GC'ed as a measure of last resort by the framework.

If you are using a server operating system, you can also enable HTTP compression on the server to reduce your bandwidth usage even further. If an HTTP/1.1 client connects to your server, Windows will compress the binary stream (similar to ZIP) before sending it out to the client.

To enable HTTP compression on Windows 2000, open the Internet Service Manager, right-click on your server, and pick "Properties". Select the "Service" tab, then check "Compress Application Files" and "Compress Static Files".

As far as I can tell, HTTP compression is automatically enabled on IIS 5.1 in Windows XP.

History

  • v1.1, 1/23/2004 - Bug-fix release.
  • v1.0, 1/21/2004 - Initial submission.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Michael Russell
Web Developer
United States United States
Member
I am currently a contract programmer in the Dallas, Texas area.
 
Previously, I worked as Quality Assurance Manager for Ritual Entertainment in Dallas, Texas; Software Test Lead for Microsoft Game Studios in Salt Lake City, and as a Programmer for Layton City, Utah.
 
I can be easily read at http://www.romsteady.net/blog/.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionTry Saving ASP.Net View State on servermemberRakhitha29 Nov '11 - 1:59 
GeneralThis sucks - performance wise!!membersofter14 Jan '09 - 14:07 
GeneralRe: This sucks - performance wise!!memberMichael Russell14 Jan '09 - 14:21 
GeneralSuggestionsmemberThiago Rafael1 Jun '05 - 17:45 
GeneralRe: SuggestionsmemberThiago Rafael2 Jun '05 - 8:17 
GeneralThe code does not work with webservicesmemberEvilPanda24 Feb '05 - 4:55 
GeneralRe: The code does not work with webservicesmemberMartialWeb.com16 Jan '07 - 13:01 
GeneralStraight port to c# from 1 pass version (not tested)susssuperk30 Jun '04 - 3:20 
GeneralIssuesmemberarthur dzhelali3 Feb '04 - 3:22 
GeneralString.EmptymemberKeith Farmer28 Jan '04 - 11:18 
GeneralRe: String.EmptymemberMichael Russell (Layton)28 Jan '04 - 11:34 
General1-Pass Take 1 AvailablememberMichael Russell (Layton)27 Jan '04 - 9:08 
GeneralRe: 1-Pass Take 1 Availablememberdog_spawn28 Jan '04 - 0:34 
GeneralRe: 1-Pass Take 1 AvailablememberSimonGreen28 Jan '04 - 1:27 
GeneralRe: 1-Pass Take 1 AvailablememberRocky Moore2 Feb '04 - 16:02 
QuestionOne path Or Loop what is more efficient?memberarthur dzhelali27 Jan '04 - 6:09 
Answermmmmemberdog_spawn27 Jan '04 - 12:51 
QuestionConsidered Using Response.Filter & GZIP?memberstevensk26 Jan '04 - 17:09 
AnswerRe: Considered Using Response.Filter & GZIP?memberSimonGreen27 Jan '04 - 1:34 
Generalblowery.org = coolmemberdog_spawn27 Jan '04 - 12:47 
GeneralRe: blowery.org = coolmemberSimonGreen27 Jan '04 - 13:12 
GeneralRe: blowery.org = coolmemberdog_spawn28 Jan '04 - 0:39 
GeneralRe: blowery.org = coolmemberSimonGreen28 Jan '04 - 1:22 
GeneralThanksmemberdog_spawn28 Jan '04 - 1:24 
GeneralRe: ThanksmemberSimonGreen28 Jan '04 - 2:15 
GeneralRe: blowery.org = coolsussBen Lowery3 Feb '04 - 8:35 
GeneralRe: blowery.org != coolmemberwebber12345617 Sep '04 - 17:39 
GeneralOops!memberMichael Russell (Layton)23 Jan '04 - 4:38 
GeneralA minor correctionmemberskaygin7 Oct '04 - 5:06 
GeneralGreat idea, but ...sussAnonymous22 Jan '04 - 22:50 
GeneralRe: Great idea, but ...memberMichael Russell (Layton)23 Jan '04 - 5:46 
GeneralMajor improvement suggestionmemberdog_spawn25 Jan '04 - 11:12 
GeneralRe: Major improvement suggestionmemberMichael Russell (Layton)26 Jan '04 - 5:29 
GeneralMissed the pointmemberdog_spawn26 Jan '04 - 5:59 
GeneralRe: Missed the pointmemberMichael Russell (Layton)26 Jan '04 - 6:12 
GeneralOkmemberdog_spawn26 Jan '04 - 6:18 
GeneralRe: Major improvement suggestionsussAnonymous6 May '04 - 17:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 27 Jan 2004
Article Copyright 2004 by Michael Russell
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid