BlogEngine.NET

… und warum sollte ich das tun?
Weil die Reihenfolge der Tags im Header Einfluss auf das Ladeverhalten der Browser nimmt. Das Zauberwort hierbei ist: Progressive Page Rendering. Damit ist jenes Verhalten des Browsers gemeint, in dem er jeglichen Inhalt der vom Server übertragen wird unmittelbar rendert und darstellt.
Sollten sich im Header an erster Stelle Verknüpfungen zu JavaScript-Dateien befinden, wird die Verarbeitung solange gestoppt bis alle verknüpften JavaScript-Dateien heruntergeladen wurden. Erst dann werden weitere Ressourcen vom Server geladen. Wenn zu diesem Zeitpunkt noch keine Styling-Informationen bzw. kein CSS vorhanden ist, sitzt der Benutzer solange vor einem weißen Fenster, bis der Download der JavaScript-Dateien abgeschlossen ist. Demzufolge sollten die Verknüpfungen zu den CSS-Dateien an erster Stelle im Header platziert werden. Hierzu auch ein guter Artikel zu Performance and Load Testing in der MSDN Darin sind zwar die Bing Maps das vorherrschende Thema, aber durchaus auch sehr viele allgemeingültige Infos und Tipps enthalten.

Eine weitere Unart von BlogEngine.NET, seit der Version 2.0, ist das Laden der jQuery-Dateien in jedem Artikel und jeder Seite. Egal ob sie benötigt werden oder nicht. Ich für meinen Teil benötige jQuery nicht im normalen Betrieb. Lediglich im Admin-Bereich, zur Verwaltung des Blog, wird es benötigt. Also warum immer diese Last mitschleppen?
Soweit zur Theorie. Werfen wir nun einen Blick auf einen möglichen Lösungsansatz.

Der gesamte Header soll vor dem Senden zum Client untersucht, evtl. überflüssige Einträge entfernt und neu sortiert werden.
Dem Header werden in BlogEngine.NET an verschiedenen Stellen Elemente hinzugefügt. Unter anderem in der MasterPage des verwendeten Theme und im OnLoad-Ereignishandler der BlogBasePage. Somit wäre der am einfachsten zu erreichende, aber doch späteste mögliche Zeitpunkt um den Header mit allen Elementen zu verarbeiten, der PageLoad-Ereignishandler der Masterpage des verwendeten Theme. Der Aufruf sollte in besagtem Ereignishandler so spät als möglich, aber doch so früh erfolgen, dass er immer ausgeführt wird. Also noch vor evtl. bedingten Ausstiegen aus dem Ereignishandler.

Die Elemente im Header werden in Form verschiedener Controls in einer ControlCollection vorgehalten. Am häufigsten sind Controls der Typen HtmlGenericControl, HtmlLink und HtmlMeta anzutreffen. Für das title-Tag wird ein Steuerelement vom Typ HtmlTitle verwendet. Um die verschiedenen Einträge im Header zu Kategorisieren verwende ich fünf verschiedene Auflistungen vom Typ List<T>, wobei T vom Typ System.Web.UI.Control ist.

  1. cssTags für das Speichern der im Header enthaltenen Verknüpfungen zu CSS-Dateien.
  2. scriptTags für die Verknüpfungen zu JavaScript-Dateien.
  3. linkTags für andere benötigte Verknüpfungen
  4. otherTags für nicht zugeordnete Einträge
  5. metaTags für alle Einträge vom Typ HtmlMeta.

In folgender Methode wird der angegebene Header durchlaufen, die enthaltenen Elemente Kategorisiert, der vorhandene Header geleert und sortiert neu erstellt.

public static void OptimizeHeader(HtmlHead header, bool removeJQuery)
{
    var firstTag = header.Controls[0];
    if (firstTag is HtmlLink
        && ((HtmlLink)firstTag).Attributes["rel"].Equals(
            "stylesheet",
            StringComparison.OrdinalIgnoreCase))
    {
        return;
    }
    var title = new HtmlTitle();
    var cssTags = new List<Control>();
    var scriptTags = new List<Control>();
    var linkTags = new List<Control>();
    var metaTags = new List<Control>();
    var otherTags = new List<Control>();
    var allControls = new List<Control>[]
    {
        cssTags,
        scriptTags,
        linkTags,
        otherTags,
        metaTags
    };
    foreach (Control control in header.Controls)
    {
        switch (control.GetType().Name)
        {
            case "HtmlLink":
                ProcessHtmlLink(
					(HtmlLink)control,
					allControls);
                break;
            case "HtmlMeta":
                metaTags.Add(control);
                break;
            case "HtmlGenericControl":
                ProcessGenericControl(
					(HtmlGenericControl)control,
					allControls,
					removeJQuery);
                break;
            case "HtmlTitle":
                title = (HtmlTitle)control;
                break;
            default:
                otherTags.Add(control);
                break;
        }
    }
    header.Controls.Clear();
    foreach (var entry in allControls.AsQueryable()
			.SelectMany(list => list.AsQueryable()))
    {
        header.Controls.Add(entry);
    }
    header.Controls.Add(title);
}

Zunächst wird geprüft ob der angegebene Header bereits sortiert ist in dem überprüft wird, ob das erste Element im Header eine Verknüpfung zu einer CSS-Datei darstellt. Ist diese Bedingung erfüllt, wird die Methode beendet.
Die in den Zeilen 168 und 178 verwendeten Hilfsmethoden ProcessHtmlLink und ProcessGenericControl werden zur Verarbeitung der entsprechenden Steuerelemente benötigt.

private static void ProcessGenericControl(
					HtmlGenericControl htmlControl,
					List<Control>[] allControls,
					bool removeJQuery)
{
    if (htmlControl.TagName.Equals("script", StringComparison.OrdinalIgnoreCase))
    {
        if (removeJQuery
            && IsJQueryLink(htmlControl))
        {
            return;
        }
        allControls[1].Add(htmlControl);
        return;
    }
    allControls[3].Add(htmlControl);
}
private static void ProcessHtmlLink(HtmlLink link, List<Control>[] allControls)
{
    switch (link.Attributes["rel"])
    {
        case "stylesheet":
            allControls[0].Add(link);
            break;
        case "profile":
            allControls[1].Add(link);
            break;
        default:
            allControls[2].Add(link);
            break;
    }
}

Eine weitere Hilfsmethode IsJQueryLink überprüft lediglich, ob es sich beim angegebenen Steuerelement um eine Verknüpfung zu einer der jQuery-Dateien handelt.

private static bool IsJQueryLink(HtmlGenericControl control)
{
    var src = control.Attributes["src"];
    return src.EndsWith("jquery.js", StringComparison.OrdinalIgnoreCase)
        || src.EndsWith("jquery.cookie.js", StringComparison.OrdinalIgnoreCase)
        || src.EndsWith("jquery.validate.min.js", StringComparison.OrdinalIgnoreCase)
        || src.EndsWith("jquery-jtemplates.js", StringComparison.OrdinalIgnoreCase)
        || src.EndsWith("json2.js", StringComparison.OrdinalIgnoreCase);
}

Fazit:

Welchen Vorteil bringt mir der erbrachte Aufwand?
Die geschilderte Methode allein bringt mit Sicherheit keinen enormen Vorteil. In Verbindung mit weiteren kleineren und größeren Optimierungen und Anpassungen, sollte es jedoch ein Schritt in die richtige Richtung sein.

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