Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Debug oraz Release a wydajność kodu korzystającego z Microsoft AJAX Framework

Istnieje kilka powodów, dla których nie powinno się umieszczać aplikacji ASP.NET w środowisku produkcyjnym z ustawieniem <compilation debug=”true”/> (czytaj…). Wbrew pozorom prędkość wykonywania instrukcji kodu po stronie serwera nie stanowi bardzo istotnego problemu. Inaczej jednak wygląda sytuacja po stronie klienta - gdy nasza aplikacja korzysta z javascriptowej biblioteki Microsoft AJAX Framework. Wówczas ustawienie Release/Debug* ma ogromny wpływ na szybkość wykonywania kodu aplikacji!

Doskonale widać to na przykładzie poniższej pętli, korzystającej z funkcji startsWith dostarczanej wraz z częścią kliencką pakietu ASP.NET AJAX.

for (var i = 0; i < 10000; i++) {
    'xxx'.startsWith('abc');
}

Pętla ta wykonuje się ok. 5 razy wolniej gdy używana jest wersja debug biblioteki JavaScript!  Czasy gdy JS był używany do wyświetlania alertów dawno minęły. Teraz gdy aplikacje webowe mogą posiadać tysiące linii kodu wykonywanego po stronie klienta, tak wielka różnica wydajnościowa może być sporym problemem (zwłaszcza gdy przeglądarka zgłosi komunikat o wolnym działaniu skryptu na stronie).

Skąd bierze się różnica w szybkości działania? Wynika ona z tego, że kod JavaScript opracowany przez Microsoft dla potrzeb tworzenia aplikacji ajaxowych, dostarczany jest w dwóch wersjach. Wersja, która jest wykorzystywana w trybie Debug, zawiera bloki kodu znacznie ułatwiające rozwijanie aplikacji – np. sprawdzają one ilość i typy parametrów podawanych w wywołaniu funkcji. Wersja dostarczana do przeglądarki w trybie Release nie zawiera tego rodzaju udogodnień – przez co skrypt wykonuje znacznie mniej pracy i działa szybciej.

Oto kod wykonywany przy wywołaniu metody startsWith w trybie Release (w celu ograniczenia wielkości plików pobieranych przez przeglądarkę kod nie zawiera komentarzy i zbędnych białych znaków):

String.prototype.startsWith=function(a){return this.substr(0,a.length)===a};

A to kod z trybu Debug:

String.prototype.startsWith = function String$startsWith(prefix) {
    /// <summary locid="M:J#String.startsWith" />
    /// <param name="prefix"></param>
    /// <returns type="Boolean"></returns>
    var e = Function._validateParams(arguments, [{
        name: "prefix",
        type: String
    }]);

    if (e) throw e;

    return (this.substr(0, prefix.length) === prefix);
}

Widzisz wywołanie funkcji _validateParams? Korzysta ona z kilku innych funkcji – łącznie kod walidujący parametry ma 155 linii (w wersji 3.5.30729.196 pliku MicrosoftAjax.debug.js).

Function._validateParams = function Function$_validateParams(params, expectedParams) {
    var e;
    e = Function._validateParameterCount(params, expectedParams);

    if (e) {
        e.popStackFrame();
        return e;
    }

    for (var i = 0; i < params.length; i++) {
        var expectedParam = expectedParams[Math.min(i, expectedParams.length - 1)];
        var paramName = expectedParam.name;

        if (expectedParam.parameterArray) {
            paramName += "[" + (i - expectedParams.length + 1) + "]";
        }

        e = Function._validateParameter(params[i], expectedParam, paramName);

        if (e) {
            e.popStackFrame();

            return e;
        }
    }

    return null;
* Domyślnie, to jaka wersja biblioteki JavaScript ASP.NET AJAX zostanie użyta, zależy od ustawienia atrybutu debug w elemencie configuration/system.web/compilation w pliku Web.config. Tryb emisji skryptów można jednak zmienić ustawiając atrybut ScriptMode kontrolki ScriptManager na Release lub Debug. Ustawienie to ma wyższy priorytet niż ustawienie w konfiguracji aplikacji.

Kod skryptu aktualizowany w UpdatePanelu

Załóżmy, że z jakiegoś powodu musisz umieścić w obszarze kontrolki UpdatePanel funkcję JavaScript, której kod będzie się zmieniał przy każdej aktualizacji panelu. Funkcja ta ma być wywoływana w odpowiedzi na zdarzenie onclick jakiegoś elementu, który znajduję się poza UpdatePanelem...

Pierwsza rzecz, która przychodzi na myśl to umieszczenie skryptu w kontrolce Literal znajdującej się w aktualizowanym fragmencie strony:

protected void Button1_Click(object  sender, EventArgs e)
{
   string s = "<script type='text/javascript'> function " +
      "test() { alert('" + DateTime.Now + "') };</script>";

   Literal1.Text = s;
}

Niestety, przy próbie wywołania funkcji test() pojawi się błąd (przeglądarka jej nie znajdzie). Dzieje się tak dlatego, że aktualizacja UpdatePanelu to tak naprawdę podmiana właściwości innerHTML warstwy (div), która reprezentuje ten panel po stronie klienta. Wnętrze tej warstwy będzie zawierać fragment skryptu jednak przeglądarka nie będzie sobie zdawać sprawy z jego obecności.

Oto fragment przechwyconej za pomocą narzędzia Fiddler, odpowiedzi serwera na postback pochodzący z UpdatePanelu:

HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Mon, 29 Sep 2008 13:28:26 GMT
X-AspNet-Version: 2.0.50727
Transfer-Encoding: chunked
Cache-Control: private
Content-Type: text/plain; charset=utf-8
Connection: Close

10c
238|updatePanel|UpdatePanel1|
<input type="submit" name="Button1" value="upa1" id="Button1" />

<script type='text/javascript'> function test() { alert('2008-09-29 15:28:26') };</script> | 22a 216|hiddenField|__VIEWSTATE|/wEP...

Widać wyraźnie, że treść skryptu została wysłana do klienta...

Co zrobić by wywołanie funkcji test(), która pojawiła się po aktualizacji UpdatePanelu, zadziałało prawidłowo? Zamiast dosłownie wpisywać treść skryptu w aktualizowany obszar skorzystaj z metody ScriptManager.RegisterStartupScript:

protected void Button1_Click(object  sender, EventArgs e)
{
   string s = "<script type='text/javascript'> function " +
      "test() { alert('" + DateTime.Now + "') };</script>";

   ScriptManager.RegisterStartupScript(this, typeof(Page),
      "abc", s, false);
}

Tym razem wywołanie metody test() zakończy się powodzeniem (użytkownik zobaczy alert z datą ustawianą w chwili aktualizacji panelu).

O znaczeniu poszczególnych parametrów metody ScriptManager.RegisterStartupScript możesz poczytać tutaj.

Treść komunikatu o błędzie podczas asynchronicznego postbacka

Gdy kontrolka znajdująca się w UpdatePanelu wykonuje asynchroniczny postback skutkujący błędem po stronie serwera, użytkownik zostaje poinformowany o problemie przez komunikat JavaScript. Treść tego komunikatu jest taka sama jak właściwość Message wyjątku. Alert ukaże się jedynie wtedy, gdy witryna nie korzysta z przekierowania w przypadku błędu lub, gdy właściwość AllowCustomErrorsRedirect kontrolki ScriptManager jest ustawiona na false.

Co jeśli chciałbyś zmienić treść wiadomości wyświetlanej użytkownikowi?
Można to zrobić na dwa sposoby. Pierwszy to ustawienie odpowiedniego tekstu w propercie AsyncPostBackErrorMessage kontrolki ScriptManager. Drugi to obsługa zdarzenia AsyncPostBackError tejże kontrolki.

Druga technika daje większe możliwości, ponieważ pozwala na dostosowanie treści komunikatu do typu wyjątku. Dobrym pomysłem jest umieszczenie ogólnej informacji o błędzie we właściwości AsyncPostBackErrorMessage i modyfikowanie jej w razie potrzeby w zdarzeniu AsyncPostBackError.

Tak wygląda deklaracja ScriptManagera wyświetlającego domyślmy komunikat błędu o treści "Mamy problem!":

<asp:ScriptManager  ID="ScriptManager1" runat="server"
 AsyncPostBackErrorMessage="Mamy problem!"
 OnAsyncPostBackError="ScriptManager1_AsyncPostBackError" />

A oto przykładowy kod modyfikujący komunikat wyświetlany w alercie, jeśli błąd powstał na skutek operacji dzielenia przez zero:

protected void ScriptManager1_AsyncPostBackError(object sender,
    AsyncPostBackErrorEventArgs e)
{
    if (e.Exception is DivideByZeroException)
    {
        ScriptManager1.AsyncPostBackErrorMessage = "Nie można dzielić przez zero!";
    }
}