clean html Ein Verhalten von ASP.NET ist die Auslieferung von wirklich übel formatiertem HTML an den Client. Da werden alle eingerückten Elemente so belassen wie sie in der Quelltextanzeige von Visual Studio™ dargestellt werden. Auch alle unnötigen Zeilenumbrüche werden so ausgeliefert wie sie sind. Selbst Fließtexte, wenn sie bereits im Quelltext vorhanden sind, werden sinnlos umgebrochen und eingerückt. Bei der Bearbeitung des Quellcode ist dieses Verhalten ja ganz angenehm, bei der Auslieferung an den Client wohl eher unnötig.
Im Großen und Ganzen wird so einiges an unnötigem Content erzeugt, der jedes mal zum Client übertragen werden muss. Außer der Verschwendung von Bandbreite, die hier sicherlich nicht das Hauptproblem darstellt, müssen vom Browser immer diese überflüssigen Umbrüche, Leerzeichen und Einrückungen geparst werden. Das dürfte sich, je nach verwendetem Browser, negativ auf die Geschwindigkeit beim Rendern der Seite auswirken.
Dabei ist es relativ einfach, den überflüssigen Whitespace zu entfernen, damit der Quellcode so aussieht wie in der oben gezeigten Grafik.

Bei der Recherche zur Lösung dieses Problems, bin ich auf verschiedene Ansätze gestoßen. Die beiden meist genutzten sind das überschreiben der Page.Render Methode einer Seite, und die Verwendung eines HttpResponse.Filter.

Im Blog von Mads Kristenson habe ich zwei Artikel zu dem Thema gefunden:
In Remove whitespace from your pages zeigt er einen Ansatz mit regulären Ausdrücken um Leerzeichen zwischen Tags und Zeilenumbrüche zu entfernen.
In Remove whitespace from your ASP.NET page zeigt er einen etwas komplexeren Weg um Leerzeichen und Zeilenumbrüche zu entfernen.
Beide Ansätze basieren auf dem Überschreiben der Render-Methode einer Seite.

Auf codeproject bin ich auf zwei Artikel über die Verwendung eines HttpResponse.Filter zum Entfernen von Leerzeichen und Umbrüchen gestoßen.
Reduce The Size Of Your ASP.NET Output zeigt einen Ansatz auf Basis sehr vieler und Aufwendiger Manipulationen einer Zeichenkette.
Removing White Chars from ASP.NET Output using Response.Filter property dagegen, zeigt  zwei verschiedene Wege: einen Ansatz auf Basis eines StreamReader und einen durch direkte Manipulation auf byte Ebene des Stream.

Mir persönlich haben alle bisher gefundenen Ansätze nicht sonderlich zugesagt. Sie alle bieten kaum, oder nur sehr schwer umsetzbar, eine Möglichkeit der Feinabstimmung, wie denn mit den Leerzeichen, Tabs und Umbrüchen umgegangen werden soll.
Gerade hier auf meinem Blog werden Tabulatorzeichen von einem Plug-In zur Formatierung des Quellcode verwendet. An andere Stelle, in der selben Seite, sind diese Tabulatorzeichen absolut überflüssig. Genau so verhält es sich auch mit Zeilenumbrüchen; In der Darstellung von Quellcode werden sie benötigt, zwischen HTML-Tags sind sie unerwünscht.

Wo also ansetzen?
Zunächst einmal galt es den richtigen Zeitpunkt zum Eingriff in den HTML-Output zu finden. Mir erschien das Ereignis PreRequestHandlerExecute als der richtige Moment, der unmittelbar vor der Ausführung einer Seite liegt. Normalerweise wird hier ein Http-Modul benötigt. Da ich jedoch den Filter hier in meinem Blog unter BlogEngine.NET testen wollte, konnte ich mich einfach in ein bestehendes Modul einklinken. Ich habe einfach meinen Filter im bestehenden Kompressionsmodul verwendet, wie man in Zeile 64 des folgenden Listing sehen kann.

void context_PostReleaseRequestState(object sender, EventArgs e)
{
    HttpContext context = ((HttpApplication)sender).Context;
    if (context.CurrentHandler is System.Web.UI.Page && context.Request["HTTP_X_MICROSOFTAJAX"] == null && context.Request.HttpMethod == "GET")
    {
        CompressResponse(context);
        context.Response.Filter = new WhiteSpaceFilter(context.Response.Filter);

        if (BlogSettings.Instance.CompressWebResource)
            context.Response.Filter = new WebResourceFilter(context.Response.Filter);
    }
    else if (!BlogSettings.Instance.CompressWebResource && context.Request.Path.Contains("WebResource.axd"))
    {
        context.Response.Cache.SetExpires(DateTime.Now.AddDays(30));
    }
}

Der Filter selbst, ist lediglich eine Klasse, die von der Klasse Stream ableitet. In der überschriebenen Methode Stream.Write, kann nun die Manipulation der Daten stattfinden.
Als Container der Daten für die verschiedenen Manipulationen habe ich mich für eine StringBuilder Instanz entschieden. In ihr können alle Zeichenfolgenbearbeitungen genau wie in einem string erfolgen, ohne die Nachteile der fortdauernden Neuerzeugung einer String-Instanz.
Bei der Bearbeitung der Daten in der StringBuilder-Instanz, bin ich auf eine Eigenart gestoßen, die mir so nicht geläufig war. Wenn Daten folgendermaßen

var sb = new StringBuilder(Encoding.Default.GetString(buffer));

in einen StringBuilder geschrieben werden, wird das Tabulatorzeichen in seiner String Darstellung \t nicht erkannt. Zunächst dachte ich an ein Problem mit der Kodierung des byte Array. Nach dem mir aber dieses Verhalten, hier die Diskussion Tabulatorzeichen wird nicht gefunden, so bestätigt wurde, bin ich einen kleinen Umweg gegangen.
Da die Tabulatorzeichen ja als byte-Code im Array vorliegen, musste ich nur den byte-Code das Tabulatorzeichen durch den byte-Code eines anderes Zeichen ersetzen welches als String Darstellung im StringBuilder gefunden wird und gleichzeitig in Texten so gut wie nicht vorkommt. Hier habe ich mich für die Tilde˜ entschieden. Sie kommt im HTML Quelltext nicht vor, da sie unter ASP.NET bereits als Pfad aufgelöst wurde. Wenn sie im Fließtext eines Beitrag auftaucht, ist sie normalerweise als HTML-Code ˜ enthalten.
Also braucht jetzt nur noch der byte-Code des Tabulatorzeichen mit dem Code der Tilde ersetzt zu werden. Dies geschieht in einer einfachen for-Schleife.

private static byte[] MarkTabs(byte[] buffer)
{
    for (int i = 0; i < buffer.GetLength(0); i++)
    {
        if (buffer[i].Equals(9))
        {
            buffer[i] = 126;
        }
    }

    return buffer;
}

So markiert, kann nun in der StringBuilder Instanz die Tilde durch eine String Entsprechung \t des Tabulatorzeichens ersetzt und verarbeitet werden.

// Tabs als ~ markieren
var markedBuffer = WhiteSpaceFilter.MarkTabs(buffer);
var sb = new StringBuilder(Encoding.Default.GetString(markedBuffer));

// ~ wieder als Tab markieren
sb.Replace("~", "\t");

Da ich in meinen Blogartikeln öfter mal Quelltextbeispiele enthalten sind, möchte ich in diesen die Tabulatoren und Zeilenumbrüche wegen der Formatierung erhalten. Da das HTML der Ausgabeseite in einer StringBuilder Instanz vorliegt, können die Codeblöcke recht einfach mit einem regulären Ausdruck gefunden werden.

private static Regex code = new Regex("<div style=\".+\" id=\".+\" class=\"wlWriterEditableSmartContent\"><pre title=\"code\" class=\".+\">(.*)</pre></div>",
    RegexOptions.Singleline | RegexOptions.Compiled);

Da oftmals mehrere Codeblöcke in einem Artikel vorhanden sein können, werden sie kurzerhand mit einer Hilfsmethode in einer List<T> vom Typ string gespeichert

// Status der Code-Bereiche sichern
List<string> codeStore = (List<string>)WhiteSpaceFilter.SaveCodeStatus(buffer);
private static IList<string> SaveCodeStatus(byte[] buffer)
{
    var sb = new StringBuilder(Encoding.Default.GetString(buffer));
    var codeStatus = new List<string>();
    var codeStoreMatches = code.Matches(sb.ToString());

    foreach (Match match in codeStoreMatches)
    {
        codeStatus.Add(match.Value);
    }

    return codeStatus;
}

und können so, nach dem Entfernen des Whitespace einfach wieder in den ursprünglichen Zustand versetzt werden.

// Status der Code-Bereiche wieder herstellen.
var codeRestorematches = code.Matches(sb.ToString());

for (int i = 0; i < codeRestorematches.Count; i++)
{
    sb.Replace(codeRestorematches[i].Value, codeStore[i]);
}

Wie das eigentliche Entfernen des Whitespace geschieht, ist wahrscheinlich eher situationsabhängig. Ob ein regulärer Ausdruck oder eine genau definierte Zeichenfolge verwendet wird, sollte jeder für sich entscheiden.
Das HTML der jeweiligen Seite kann im StringBuilder praktisch wie eine Zeichenfolge behandelt werden. Es können also einzelne Teile der Seite, wie etwa der Head-Bereich, unabhängig vom anderen Inhalt bearbeitet werden.
Performanceprobleme sind auch kaum zu befürchten, da sowohl reguläre Ausdrücke als auch die Ersetzungen im StringBuilder sehr schnell verarbeitet werden. Ich konnte keine Verzögerungen beim Entfernen des Whitespace feststellen, die über 50 Millisekunden hinausgingen. Die Reduzierung der unkomprimierten Größe der Startseite dieses Blog von 82.3 auf 62.5 kb rechtfertigt aus meiner Sicht den Aufwand allemal.

Fazit:

Das Entfernen des Whitespace lässt sich technisch relativ einfach umsetzen. Eine allgemeingültige Regel konnte ich nicht erkenne, da der Inhalt und die Struktur der einzelnen Webseiten so unterschiedlich sind, wie ihre Betreiber.
Ob sich der Aufwand rechnet, muss jeder für sich selbst entscheiden.
Im Moment spielen die Ladezeiten einer Webseite nur eine untergeordnete Rolle. Allerdings wird gemunkelt, dass Google die Ladezeit einer Webseite in zukünftige Bewertungen mit einließen lassen will.
Nicht zuletzt freut sich jeder Besucher über eine Seite die flott geladen und dargestellt wird.

Technorati-Tags:  |  |  | 
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
kick it on dotnet-kicks.de