Click here to Skip to main content
15,867,488 members
Articles / Web Development / ASP.NET
Article

Reduce The Size Of Your ASP.NET Output

Rate me:
Please Sign up or sign in to vote.
3.25/5 (16 votes)
26 Jan 20043 min read 174.2K   855   51   37
Describes the use of HttpResponse.Filter to reduce the size of your outgoing .ASPX files.

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:

VB
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)

VB
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...

VB
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


Written By
Web Developer
United States United States
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/.

Comments and Discussions

 
QuestionTry Saving ASP.Net View State on server Pin
Rakhitha29-Nov-11 1:59
Rakhitha29-Nov-11 1:59 
GeneralThis sucks - performance wise!! Pin
DotNetWise14-Jan-09 14:07
DotNetWise14-Jan-09 14:07 
Michael,
Your point of view is very good - but your method is extremly bad - performance wise!
Basically you just creating tons of strings in ASP.NET's memory!
Multiply that for each page, for each request, each time!
Bummer!

Here is a simpler method - WAY FASTER, no performance penalty at all!

The idea relies on three steps:
1. Replace the standard HtmlTextWriter with a non-indenting one!
2. Copy the app before deployment and remove the white spaces from all the copied files! [Don't worry it is very simple to do this - see below)
3. Precompile your ASP.NET application!

So here is the code:

#1
Put this in your BasePage class

public class BasePage
    : System.Web.UI.Page
{
	public class H : HtmlTextWriter
	{
		public H(TextWriter writer)
			: base(writer, "")
		{
			this.NewLine = "\n";
		}
		public override void WriteLine()
		{
			//base.WriteLine();
		}
		protected override void OutputTabs()
		{
			//base.OutputTabs();
		}
	}
	protected override HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw)
	{
		if (((this.Context != null) && (this.Context.Request != null)) && (this.Context.Request.Browser != null))
		{
			return new H(this.Context.Request.Browser.CreateHtmlTextWriter(tw));
		}
		HtmlTextWriter writer = new H(tw);
		if (writer == null)
		{
			writer = new HtmlTextWriter(tw);
		}
		return writer;
	}
}


2. Create a small console application and put this code on the Program.cs file:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace CleanupWhiteSpace
{
	class Program
	{
		static string[] Extensions = new string[] { "*.aspx", "*.master", "*.ascx", "*.asmx", "*.htm", "*.html" };
		static Regex EndTag = new Regex(@"^(\<\/?\w+\s*\:?\w*\s*\>\s*)+$");

		static void Main(string[] args)
		{
			if (args.Length > 0)
			{
				string path = args[0];
				CleanUp(path);
			}
		}

		private static void CleanUp(string path)
		{
			foreach (var ext in Extensions)
				foreach (var file in Directory.GetFiles(path, ext))
				{
					CleanUpFile(file);
				}
			foreach (var dir in Directory.GetDirectories(path))
			{
				CleanUp(dir);
			}
		}

		private static void CleanUpFile(string file)
		{
			StringBuilder sb = new StringBuilder();
			using (var s = new StreamReader(file))
			{
				bool appendLine = false;
				while (!s.EndOfStream)
				{
					var line = s.ReadLine();
					line = line.TrimStart(' ', '\t');
					if (line.Length == 0)
						continue;
					if (EndTag.Match(line).Success)
					{
						line = line.TrimEnd(' ', '\t');
						appendLine = false;
					}
					if (appendLine)
						sb.Append("\n");
					sb.Append(line);
					appendLine = true;
				}
			}
			using (var s = new StreamWriter(file))
				s.Write(sb.ToString());
		}
	}
}


Make sure you first copy your web site/application to a different folder and run the cleanup app on it before precompling.

3.Precomplie the application and deploy it!

Enjoy the white-space free outputs Smile | :)

Happy Programming,
Laurentiu
GeneralRe: This sucks - performance wise!! Pin
Michael Russell14-Jan-09 14:21
Michael Russell14-Jan-09 14:21 
GeneralSuggestions Pin
Thiago Rafael1-Jun-05 17:45
Thiago Rafael1-Jun-05 17:45 
GeneralRe: Suggestions Pin
Thiago Rafael2-Jun-05 8:17
Thiago Rafael2-Jun-05 8:17 
GeneralThe code does not work with webservices Pin
EvilPanda24-Feb-05 4:55
EvilPanda24-Feb-05 4:55 
GeneralRe: The code does not work with webservices Pin
MartialWeb.com16-Jan-07 13:01
MartialWeb.com16-Jan-07 13:01 
GeneralStraight port to c# from 1 pass version (not tested) Pin
superk30-Jun-04 3:20
superk30-Jun-04 3:20 
GeneralIssues Pin
arthur dzhelali3-Feb-04 3:22
arthur dzhelali3-Feb-04 3:22 
GeneralString.Empty Pin
Keith Farmer28-Jan-04 11:18
Keith Farmer28-Jan-04 11:18 
GeneralRe: String.Empty Pin
Michael Russell28-Jan-04 11:34
Michael Russell28-Jan-04 11:34 
General1-Pass Take 1 Available Pin
Michael Russell27-Jan-04 9:08
Michael Russell27-Jan-04 9:08 
GeneralRe: 1-Pass Take 1 Available Pin
dog_spawn28-Jan-04 0:34
dog_spawn28-Jan-04 0:34 
GeneralRe: 1-Pass Take 1 Available Pin
Member 24214828-Jan-04 1:27
Member 24214828-Jan-04 1:27 
GeneralRe: 1-Pass Take 1 Available Pin
Rocky Moore2-Feb-04 16:02
Rocky Moore2-Feb-04 16:02 
QuestionOne path Or Loop what is more efficient? Pin
arthur dzhelali27-Jan-04 6:09
arthur dzhelali27-Jan-04 6:09 
Answermmm Pin
dog_spawn27-Jan-04 12:51
dog_spawn27-Jan-04 12:51 
QuestionConsidered Using Response.Filter & GZIP? Pin
Kim Stevens26-Jan-04 17:09
Kim Stevens26-Jan-04 17:09 
AnswerRe: Considered Using Response.Filter & GZIP? Pin
27-Jan-04 1:34
suss27-Jan-04 1:34 
Generalblowery.org = cool Pin
dog_spawn27-Jan-04 12:47
dog_spawn27-Jan-04 12:47 
GeneralRe: blowery.org = cool Pin
Member 24214827-Jan-04 13:12
Member 24214827-Jan-04 13:12 
GeneralRe: blowery.org = cool Pin
dog_spawn28-Jan-04 0:39
dog_spawn28-Jan-04 0:39 
GeneralRe: blowery.org = cool Pin
Member 24214828-Jan-04 1:22
Member 24214828-Jan-04 1:22 
GeneralThanks Pin
dog_spawn28-Jan-04 1:24
dog_spawn28-Jan-04 1:24 
GeneralRe: Thanks Pin
Member 24214828-Jan-04 2:15
Member 24214828-Jan-04 2:15 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.