2

I have been researching this one for awhile. I found several resources on the subject and they all tend to use the same approach - override Page.Render, use the HtmlTextWriter to convert the output into a string, and then use a series of compiled regular expressions to remove the extra whitespace. Here is an example.

Well, I tried it and it works...but....

In Safari 5.0, this seems to cause all sorts of performance issues with loading images and getting "server too busy" errors. IE 7, FireFox 3.6 and Google Chrome 6.0 seem to work okay, but I haven't stress tested the server very much. Occasionally there seems to be lags in the time the page is generated, but once the html is sent to the browser, the page displays quickly.

Anyway, when you think about it, it seems rather silly to have .NET build up all of these tabs and line breaks only to strip them all out again using string parsing - the least efficient way to strip them out. It would make more sense to override the HtmlTextWriter and pass it into the tree on the main page request in order to avoid putting them into the output at all - logically there should be a performance gain instead of a hit in that case.

Even if I can only remove 50% of the whitespace using this method, it will still leave much less work for the regular expressions to do - meaning it should perform better than it does with regular expressions alone.

I tried using a Control Adapter and overriding several of the members

  1. Directing all calls to WriteLine() to the corresponding Write() method
  2. Setting the NewLine property to an empty string
  3. Overriding the OutputTabs() method and simply removing the code
  4. Overriding the Indent property and returning 0

I also tried overriding RenderChildren, Render, BeginRender, and EndRender to pass in my custom HtmlTextWriter, but I can't seem to make even a simple label control remove the tabs before its tag. I also dug through the framework using Reflector, but I simply can't figure out how these characters are being generated - I thought I was using a "catch all" approach, but apparently I am missing something.

Anyway, here is what I have come up with. This code is not functioning the way I would like it to. Of course, I have also tried overriding the various Render methods on the page directly and passing in an instance of my custom HtmlTextWriter, but that didn't work either.

Public Class PageCompressorControlAdapter
    Inherits System.Web.UI.Adapters.ControlAdapter

    Protected Overrides Sub RenderChildren(ByVal writer As System.Web.UI.HtmlTextWriter)
        MyBase.RenderChildren(New CompressedHtmlTextWriter(writer))
    End Sub

    Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
        MyBase.Render(New CompressedHtmlTextWriter(writer))
    End Sub

    Protected Overrides Sub BeginRender(ByVal writer As System.Web.UI.HtmlTextWriter)
        MyBase.BeginRender(New CompressedHtmlTextWriter(writer))
    End Sub

    Protected Overrides Sub EndRender(ByVal writer As System.Web.UI.HtmlTextWriter)
        MyBase.EndRender(New CompressedHtmlTextWriter(writer))
    End Sub

End Class

Public Class CompressedHtmlTextWriter
    Inherits HtmlTextWriter

    Sub New(ByVal writer As HtmlTextWriter)
        MyBase.New(writer, "")
        Me.InnerWriter = writer.InnerWriter

        Me.NewLine = ""
    End Sub

    Sub New(ByVal writer As System.IO.TextWriter)
        MyBase.New(writer, "")
        MyBase.InnerWriter = writer

        Me.NewLine = ""
    End Sub

    Protected Overrides Sub OutputTabs()
        'Skip over the tabs
    End Sub

    Public Overrides Property NewLine() As String
        Get
            Return ""
        End Get
        Set(ByVal value As String)
            MyBase.NewLine = value
        End Set
    End Property


    Public Overrides Sub WriteLine()

    End Sub

    Public Overrides Sub WriteLine(ByVal value As Boolean)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Char)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal buffer() As Char)
        MyBase.Write(buffer)
    End Sub

    Public Overrides Sub WriteLine(ByVal buffer() As Char, ByVal index As Integer, ByVal count As Integer)
        MyBase.Write(buffer, index, count)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Decimal)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Double)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Integer)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Long)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Object)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As Single)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal s As String)
        MyBase.Write(s)
    End Sub

    Public Overrides Sub WriteLine(ByVal format As String, ByVal arg0 As Object)
        MyBase.Write(format, arg0)
    End Sub

    Public Overrides Sub WriteLine(ByVal format As String, ByVal arg0 As Object, ByVal arg1 As Object)
        MyBase.Write(format, arg0, arg1)
    End Sub

    Public Overrides Sub WriteLine(ByVal format As String, ByVal arg0 As Object, ByVal arg1 As Object, ByVal arg2 As Object)
        MyBase.Write(format, arg0, arg1, arg2)
    End Sub

    Public Overrides Sub WriteLine(ByVal format As String, ByVal ParamArray arg() As Object)
        MyBase.Write(format, arg)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As UInteger)
        MyBase.Write(value)
    End Sub

    Public Overrides Sub WriteLine(ByVal value As ULong)
        MyBase.Write(value)
    End Sub

End Class

In case you are not familliar with control adapters, simply place the xml below in a .browser file in the ASP.NET App_Browsers folder. You can change the controlType to apply the control adapter to a Label or something else for a smaller scope of a test. If I can get this working, it isn't such a big deal to add all of the controls in my project here if it will be necessary to do so.

<browsers>

    <browser refID="Default">
        <controlAdapters>
            <adapter controlType="System.Web.UI.Page"
                     adapterType="PageCompressorControlAdapter"/>
        </controlAdapters>
    </browser>

</browsers>

Anyway, you would think there would just be a simple configuration setting like VerboseHtml="false" or PreserveHtmlFormatting="false" or something along those lines. If you look at the output from MSN.com, they are using some kind of compression similar to this...and it appears to be very performant.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Here is a similar answer http://stackoverflow.com/questions/2743594/how-to-remove-spaces-and-newlines-from-server-response/2743896#2743896 – Aristos Sep 15 '10 at 21:30
  • 1
    Are you sure you want to do this. Using the built in deflate and gzip compressors is much more efficient. – Tomasi Sep 15 '10 at 21:44
  • @diamandiev - Compression built into IIS or built into .NET? I did a little research and discovered this feature is built into IIS 6 and requires a little hacking to get it to work. There are also some HttpModules available for .NET. So which is better, use IIS, .NET or a combination of both? – NightOwl888 Sep 16 '10 at 13:40
  • @Aristos - Thanks for the link. I followed all of the links off the page you sent and pages those linked to. All of them basically use the RegEx approach, but do it in slightly different places in the framework. One of them had found a way to do it at compile time, which I will probably use, even though the kb savings is only about 50% as the other method I tried. But back to the original question - isn't there a way to prevent ASP.NET from putting tabs and spaces in the output in the first place rather than replacing them after the fact (hence doing twice the work)? – NightOwl888 Sep 16 '10 at 13:47
  • @NightOwl888 I do not know to answer that at this time. I think that is not worth it because the page is GZip compress it and there is not reason to do that because you do not gain too much on the end. – Aristos Sep 16 '10 at 15:54
  • Well, I don't have gzip compression enabled (in fact I didn't even realize it was an option). I downloaded and configured the stock DLL from http://blowery.org/httpcompress/ and it improved performance dramatically. The best solution I think will be to use a combination of http://omari-o.blogspot.com/2009/09/aspnet-white-space-cleaning-with-no.html and compression - as long as the replacement is made at compile-time there is no negative performance impact and it saves about 11% of the page size. – NightOwl888 Sep 16 '10 at 17:15
  • app.Response.Filter = New DeflateStream(prevUncompressedStream, CompressionMode.Compress) app.Response.AppendHeader("Content-Encoding", "deflate") This works with iis7 and is out of the box. I dont know for lower versions. – Tomasi Sep 17 '10 at 09:35
  • Hmm...ok, but just that code alone doesn't ensure the browser supports compression. With the onslaught of mobile computing devices, you are bound to run into browsers that don't accept compression. I checked out the code in HttpCompress (an HttpModule) and it appears sound and supports both gzip and deflate, but I have yet to stress-test it. However, I am also considering enabling the support in IIS6 so I can compress .css and .js files as in this article: http://www.lostechies.com/blogs/dahlbyk/archive/2010/01/08/script-to-enable-http-compression-gzip-deflate-in-iis-6.aspx. – NightOwl888 Sep 17 '10 at 11:35

1 Answers1

1

The solution involved using a modified version of a compresson module I found here:

http://omari-o.blogspot.com/2009/09/aspnet-white-space-cleaning-with-no.html

This solution implements a custom PageParserFilter which is then configured in the web.config file. The beauty of this solution is that it filters the white space at compile time, so at runtime there is no performance hit.

And then thanks to Aristos and diamandiev, I looked for a reasonable way to add compression to the response stream for browsers that support it. I found another open source module that can be used for this here: http://blowery.org/httpcompress/

The performance gain was significant with the combination of the two methods - much more than using one or the other. It seems that browsers render much faster when the tabs are removed from the page.

As for static content, I plan to use the method in this article to compress those files as well (except for images of course, they are already compressed): http://www.lostechies.com/blogs/dahlbyk/archive/2010/01/08/script-to-enable-http-compression-gzip-deflate-in-iis-6.aspx

Final Solution

I have added my final solution to this Gist.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Would you mind sharing at least the core part of the solution? There's currently no download link on the blog page you're linking to. – Oliver Aug 12 '14 at 17:39
  • @Oliver - I have updated my post to include a link to [the final solution](https://gist.github.com/NightOwl888/c1f0a558d69f82dedfef). Sorry, I don't recall how it works exactly. – NightOwl888 Aug 13 '14 at 11:24
  • Thanks a lot for your commitment! – Oliver Aug 14 '14 at 22:09