André Krämers Blog

Lösungen für Ihre Probleme

Jeder der in einer ASP.NET Webforms Anwendung ein oder mehrere “Ajax-Enabled” Controls eines Komponentenherstellers nutzt kennt das Problem: Die Anzahl der zum Client übertragenen Scripte steigt explosionsartig an.

Dies liegt daran, dass die meisten Komponentenhersteller je Control ein eigenes Script zum Client senden. Generell ist das auch durchaus sinnvoll, denn nur so kann gewährleistet werden, dass der Browser nur die minimal benötigte Anzahl an Scriptcode vom Server herunterladen muss.

Wie der folgende Screenshot zeigt, wird dieser Segen jedoch auch schnell zum Fluch. Die Abbildung zeigt einen Mitschnitt des Netzwerktraffics beim Besuch einer Seite eines aktuellen Projekts. Inhalt der Seite sind ein Scriptmanager, zwei Infragistics Controls, einige ESRI WebADF Controls sowie ein paar eigene Servercontrols, die auch eigene Scripts rendern.

1_Vorher

Die Anzahl zu übertragenden Scripts beträgt laut Screenshot also 37 Stück.

Interessant, aber wo liegt nun das Problem?

Wir wissen zwar nun, dass wir 37 Scripte zum Client übertragen müssen, aber was ist daran so schlimm und warum sollte es uns überhaupt interessieren?

Um das Problem an der Situation zu erkennen, sollten wir uns kurz vor Auge führen, dass die Scripte nicht gemeinsam innerhalb des selben HTTP Requests wie die eigentliche Website übertragen werden. Stattdessen wird je Script (und auch Bild, CSS Stylesheet, etc.) eine eigene Anfrage an den Webserver gestellt.

Jede dieser Anfragen hat eine gewisse “Rüstzeit” oder auch Latenz. Damit ist die Zeitspanne gemeint, die zwischen dem Absetzen des Befehls, eine Anfrage zu starten und dem tatsächlichen Start der Anfrage vergeht. Je nach Entfernung zwischen Client und Server sowie der Qualität der Internetanbindung der beiden variiert der tatsächliche Wert dieser Reaktionszeit.

Und was bedeutet dies nun konkret?

Schauen wir uns zur Verdeutlichung ein kleines Beispiel an. Gehen wir von jemandem mit einer extrem schlechten Anbindung und einer Latenz von 500 ms aus (zugegeben, der Wert ist recht hoch, lässt sich aber schön einfach rechnen ;-))

Um unsere 37 Scriptdateien herunterzuladen, hätten wir nun also

37 Anfragen 500 ms Latenz + 37 Antworten 500 ms Latenz = 37 Sekunden.

Diese 37 Sekunden sind reine Wartezeit und kommen zur eigentlichen Übertragungszeit der Scripte hinzu. Nun ist es natürlich nicht so, dass sämtliche Anfragen sequentiell abgearbeitet werden. Allerdings sieht die Standardkonfiguration vieler Browser eine Beschränkung von zwei gleichzeitigen Verbindungen zu einem Hostnamen vor. Diese Anzahl resultiert aus einer Empfehlung der HTTP 1.1 Spezifikation. Heisst, wir müssen unsere 37 Sekunden noch durch zwei Teilen, da zwei gleichzeitige Verbindungen möglich sind. Bleiben immer noch 18,5 Sekunden übrig.

Was können wir dagegen tun?

Um dieses Problem, was tückischerweise während der Entwicklung nur selten auffällt, da die Latenz zwischen lokalem Webserver und lokalem Webbrowser nicht sonderlich hoch sein dürfte ;-), in den Griff zu bekommen, bietet ASP.NET ab der Version 3.51 das Element CompositeScript als Kindelement des Scriptmanagers an.

Dieses Element erlaubt es, einzelne Scripts anzugeben, welche anschließend automatisch zu einem Script zusammengefasst werden.

Das Ganze sieht ungefähr so aus:

<asp:ScriptManager runat="server" ID="sm1">
    <CompositeScript>
        <Scripts>
            <asp:ScriptReference Name="/Scripts/MeinScript1.js" />
            <asp:ScriptReference Name="/Scripts/MeinScript2.js" />
            <asp:ScriptReference Name="/Scrips/MeinScript3.js" />
        </Scripts>
    </CompositeScript>
</asp:ScriptManager>

Problematisch ist nur, dass man die URL der notwendigen Scripts nur für Dateibasierte Scripts kennt. Sobald ein Script aber als eingebettete Ressource über die Webresource.axd nach aussen gerendert wird, ist die URL unbekannt.

Um dieses Problem zu lösen, kann das auf Codeplex erhältliche Control ScriptReferenceProfiler genutzt werden. Wird es auf einer Seite eingesetzt, rendert es eine Liste aller genutzten Scripte heraus. Diese Liste kann 1:1 kopiert und in das Scripts Element des CompositeScript Elements eingefügt werden.

4_ScriptProfilerOutput

Die eingefügten Scripts werden anschließend serverseitig zu einem großen Script hinzugefügt und innerhalb einer einzigen Anfrage herunter geladen.

Nicht so schnell!

Der Versuch, alle 37 Scripte der zuvor genannten Seite in einem CompositeScript Element zusammen zu führen resultiert in folgendem Fehler:

5_ASP_Error

Die Ursache für diesen Fehler ist, dass der Scriptmanager jedes benötigte Script mit einem kryptischen Bezeichner an die URL anhängt. Dies sprengt schnell die maximale Begrenzung einer URL auf 1024 Zeichen.

Die Lösung

Um dieses Problem zu umgehen platziert man einfach mehrere CompositeScript Elemente auf der Seite und verteilt die Scripte auf diese. Da jedes CompositeScript Element einen eigenen Scriptmanager benötigt, nutzt man ab dem zweiten CompositeScript Element statt zusätzlichen Scriptmanagern, von denen es immer nur einen pro Seite geben darf einfach ScriptmanagerProxy Controls.

Die optimale Verteilung der Scripte auf die einzelnen CompositeScript Elemente ist schnell im Trial-and-Error-Verfahren gefunden. In meinem Beispiel sah sie übrigens wie folgt aus:

<asp:ScriptManagerProxy ID="ScriptManagerProxyAjax" runat="server">
    <CompositeScript>
        <Scripts>
            <asp:ScriptReference Name="MicrosoftAjax.js" />
            <asp:ScriptReference Name="MicrosoftAjaxWebForms.js" />
            <asp:ScriptReference Name="AjaxControlToolkit.Common.Common.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Compat.Timer.Timer.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Compat.DragDrop.DragDropScripts.js"                 Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
            <asp:ScriptReference Name="AjaxControlToolkit.Animation.Animations.js" Assembly="AjaxControlToolkit, Version=3.0.30512.20315, Culture=neutral, PublicKeyToken=28f01b0e84b6d53e" />
        </Scripts>
    </CompositeScript>
</asp:ScriptManagerProxy>

<asp:ScriptManagerProxy ID="ScriptManagerProxyInfragisticsCommon" runat="server">
    <CompositeScript>
        <Scripts>
...
        </Scripts>
    </CompositeScript>
</asp:ScriptManagerProxy>

...

Gerade wenn man sich mit einem Element nahe an der 1024 Zeichen Grenze befindet, sollte man jedoch im Hinterkopf halten, dass die tatsächliche Url der Seite später meist nicht mehr http://localhost ist, sondern eher http://meinesubdomain.meinedomain.de. Durch die Änderung des Hostnamens während der Produktivsetzung ändert sich also eventuell noch einmal die Länge der URL. Daher im Zweifelsfall lieber ein Script weniger als in der Entwicklungsumgebung möglich in die CompositeScript Tags einfügen.

Es müssen übrigens nicht alle Scripts in CompositeScript Tags gepackt werden. Solche, die nicht verpackt wurden, werden wie gehabt ganz normal weiter als einzelner Script Tag zum Browser herausgerendert.

Und was bringts nun?

Tja, was bringt der ganze Aufwand nun? Da Bilder bekanntlich mehr als Worte sagen, hier ein paar Vorher/Nachher Screenshots der zuvor vorgestellten Seite:

Initial hat die Seite 37 Scripts heruntergeladen.

 1_Vorher

Yahoo’s YSlow gab der Seite eine Gesamtnote von E.

 2_Vorher_YSlowGrade

Die Ladezeit betrug knapp 15 s, die Gesamtgröße 734 kb.

 3_Vorher_YSlowSize

Nach den Änderungen wurden nur noch 9 Scripte referenziert.

 6_Firebug_Nachher

Die YSlow Benotung stieg immerhin von E auf D.

 7_Nachher_YSlowGrade

Die Ladezeit reduzierte sich auf knapp 5 s, die Gesamtgröße auf 655 kb.

8_Nachher_YSlowSize