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!";
    }
}

Dostęp do sesji z Page Methods

ASP.NET AJAX oferuje łatwy w użyciu i efektywny mechanizm wywołań metod na serwerze z poziomu JavaScript. Mowa o Page Methods. Ponieważ są one statyczne* nie możesz w kodzie takiej metody użyć dostępu do właściwości Page.Session. Nie oznacza to jednak, że musisz zrezygnować z wykorzystania sesji. Wystarczy, że zamiast Page.Session skorzystasz z HttpContext.Current.Session.

Przykładowo by zapisać i odczytać wartość ze stanu sesji wystarczy użyć kodu:

[WebMethod]
public static void ZapiszDoSession(string s)
{
    HttpContext.Current.Session["abc"] = s;
}

[WebMethod]
public static string OdczytajZSession()
{
    return (string)HttpContext.Current.Session["abc"];
}

A co gdybyś chciał np. pobrać nazwę zalogowanego użytkownika? To równie proste, skorzystaj z HttpContext.Current.User.Identity.Name.

Dostęp do danych sesji jest możliwy, ponieważ przy wysyłaniu zapytań za pośrednictwem obiektu XmlHttpRequest, cookie z identyfikatorem sesji również jest dołączone. Takie ciastko wygląda np. tak:

Cookie: ASP.NET_SessionId=kxxrwp45bhkyyhbzpijo0zj4

* Page Methods są statyczne - i dobrze! Dzięki temu, unika się tworzenia obiektu Page oraz przesyłania ViewState. Serwer ma mniej pracy (nie zachodzą zdarzenia takie jak Page_Load czy Page_PreRender) a ilość danych przesyłanych w sieci jest znacząco mniejsza.

Ajax w starych wersjach IE wymaga ActiveX

Zaletą pisania aplikacji intranetowych jest często to, że wszyscy użytkownicy programu będą korzystać z identycznej przeglądarki. Niestety w wielu instytucjach tą przeglądarką jest IE6. Niechęć do zmiany przeglądarki wynika często z obaw o to, że stare aplikacje (krytyczne dla działalności) nie będą pracować w nowszym IE...

Jeśli w budowanym rozwiązaniu zamierzasz użyć Ajaxa nie zapomnij w wymaganiach warstwy klienckiej określonych w umowie, dodać obsługę ActiveX. Dlaczego nie wystarczy włączyć JavaScript? W IE5.x i IE6 za obsługę asynchronicznej komunikacji z serwerem odpowiada obiekt ActiveX. Utworzyć go można w taki sposób:

var xhr = new ActiveXObject('Microsoft.XMLHTTP');

W innych przeglądarkach obiekt XmlHttpRequest tworzony jest za pomocą kodu:

var xhr = new XMLHttpRequest();

więc użycie ActiveX nie jest konieczne (dotyczy to także IE7).

Jeśli myślisz, że możesz nie przejmować się IE6 to niestety jesteś w błędzie. Ta przestarzała (wypuszczona w 2001) przeglądarka ma nadal 30% udziału w rynku internetowym. W intranetach firm i urzędów na pewno nie jest lepiej...