O frameworkach i JavaScript słów kilka

Większe aplikacje JavaScript przez samą swoją złożoność wymagają, aby oprzeć ich kod na frameworku – dla wygody, kompleksowości rozwiązań, supportu, ale przede wszystkim szybkości. Czy wszystkie frameworki się do tego nadają?

Nie zadaje tego pytania przypadkowo – ostatnie miesiące to dla mnie rozwijanie Drawtera – dużej i złożonej webaplikacji, która możliwościami i wygodą przerasta niejedną aplikację offline. Postawiłem tutaj na framework jQuery, początkowo oczarowany jego prostotą i wygodą tworzenia kodu. Powstał na tym blogu nawet czteroodcinkowy (na razie) kurs tego narzędzia. Z dzisiejszej perspektywy uważam, że bezgraniczne ufanie jQuery jest zwyczajnym głupstwem.

Możliwości to nie wszystko

Nie ganie tutaj w żadnym razie małych możliwości tego frameworka – są one wystarczające, by zrobić z nimi, co się nam żywnie podoba. Gdyby było inaczej, wyrzuciłbym go z pamięci po kilku minutach. To jasne. W zasadzie jQuery oferuje w większości to, co jest dzisiaj standardem, ugruntowanym przez Prototype. Trzeba przyznać, że to ciekawe zjawisko – framework, który w większości wytyczył ścieżki, jeśli chodzi o frameworki JS, zostaje wyparty w słupkach popularności przez oferujący to samo, a może nawet mniej, jQuery. Nie trzeba znać obu frameworków bardzo dobrze, by stwierdzić, że główną tego przyczyną była prostota jQuery.

Podsumowując wątek, kierując się możliwościami, stawiam jQuery na podium. To bardzo dobry framework, szczególnie dla małych rozwiązań. Sedno sprawy rozbija się jednak nieco o inne kryteria. O szybkość działania.

Need for speed

W ostatnim półroczu modna była dyskusja na temat szybkości najpopularniejszych frameworków JS. Powstał nawet test prędkości (opierający się na pobraniu elementów DOM, kierując się selektorami, XPath itd.), w którym jQuery musiało w wielu przypadkach uznać wyższość konkurentów: MooTools i Prototype. Potem światło dzienne ujrzała poprawiona wersja jQuery (1.21) i znów zaczął się pusty lament – o wiele szybsze, czy tylko szybsze?

Szczerze mówiąc, tego typu testy są fajne, bo dają jakiś tam – mniej lub bardziej zachwiany, obraz danego frameworka. Chyba nie muszę udowadniać, jak sukces w speed teście napędza popularność narzędzia. I nawet nie chce myśleć, co stałoby się, gdyby jutro wyszedł na świat framework XXX i pobił konkurencję w każdej kategorii o 1 milisekundę…

Testy szybkości do lamusa

Z drugiej strony, SlickSpeed jest zbyt banalny, aby brać go na poważnie. Jestem daleki od uznania, że programiści na świecie dostali olśnienia i zaczęli masowo przenosić przyzwyczajenia z kodowania CSS na swoje skrypty JS. W związku z tym chętnie poznam kogoś, kto swoje projekty opiera na używaniu podobnych selektorów: div div div czy #id .klasa #id.klasa. Webdeveloperzy to w większości (tym bardziej, jeśli są świadomi korzyści płynących z używania frameworka) logicznie myślący ludzie, którzy szukają jak najbardziej optymalnych rozwiązań. Śmiem zaryzykować więc, że najczęściej używanymi selektorami, które sprawiają dość „dużo” kłopotów frameworkom będą np. takie: div .dialog czy div > div. A teraz popatrzcie sobie na szybkość frameworków w tych kategoriach. Jest duża!

Było nie było, nie chodzi nawet o złożoność selektorów. Nie zawsze kod JS produkują rasowi programiści, a w zamian robią to ludzie od CSS i XHTML, używający tego, co im wygodne. A niech sobie robią, bo naprawdę trzeba „umieć” zahamować skrypt samymi selektorami… W większości bowiem, to nie dostęp do elementów drzewa DOM odgrywa najważniejszą rolę w szybkości działania naszych skryptów.

Szybkość, a najczęstsze zadania

Jak wiemy, nie pobieramy elementów drzewa DOM dla sztuki. Często zmieniamy ich treść, rodziców, style CSS, przypisujemy im zdarzenia. I to jest tak naprawdę kwestia pierwszorzędna. Co z tego, że framework pobierze 300 divów w mgnieniu oka, skoro potem weźmie przykład ze zwycięzcy wyścigu żółwi i doda zdarzenia onmouseover w czasie, kiedy strona powinna być dawno gotowa do użycia. Jest to szczególnie ważne, kiedy tworzymy interfejsy użytkownika, webaplikacje oparte o zdarzenia, czy też efekty wizualne na naszych stronach. Zazwyczaj staramy się optymalizować takie skrypty (działając w ogromnej większości na identyfikatorach, a nie rozbudowanych selektorach).

Wracając do autopsji, początkowo wszelkie operacje na stylach i selektorach #id oparłem na jQuery. Do czasu, kiedy przyszło mi przetestować prędkość działania narzędzia. Niestety, wyniki, jakie ujrzałem spowodowały, że miałem niemały orzech do zgryzienia. Rysowanie choćby jednego diva zajmowało od 70 do 100ms, że nie wspomnę o innych operacjach. W czasie mojej konsternacji na szczęście trafiłem na początkową wersję Drawtera, wykorzystującą tylko czysty JavaScript. Jakież było moje zdziwienie, kiedy zobaczyłem naprawdę szybko działającą aplikację! Kolejne moje kroki były automatyczne – tam, gdzie było to wskazane, zmieniałem kod na czysty JS. Najczęściej używane konstrukcje miały podobną postać:

var element = $("#node1");
element.css("left", e.pageX);
element.css("width", width);
element2 = $("<div></div>");
element.append(element2);

Bez chwili zastanowienia zamieniłem wszystkie:

var element = document.getElementById("node1");
element.style['left'] = e.pageX+"px";
element.style['width'] = width+"px";
element2 = document.createElement("div");
element.appendChild(element2);

Wyobraźcie sobie teraz, że większość konstrukcji była uruchamiana podczas zdarzenia onmousemove, czyli podczas każdego ruchu kursorem myszki. W efekcie, zmiany dały skok wydajnościowy na poziomie 50%.

Wkrótce potem zacząłem zastanawiać się, dlaczego operacje na stylach tak bardzo obciążają skrypt. Oczywiście kluczem do uzyskania odpowiedzi było zajrzenie w kod źródłowy jQuery. Funkcja, która odpowiada tutaj za ustawienie styli nazywa się attr i ma taką postać:

	attr: function(elem, name, value){
		var fix = jQuery.isXMLDoc(elem) ? {} : jQuery.props;

		// Safari mis-reports the default selected property of a hidden option
		// Accessing the parent's selectedIndex property fixes it
		if ( name == "selected" && jQuery.browser.safari )
			elem.parentNode.selectedIndex;

		// Certain attributes only work when accessed via the old DOM 0 way
		if ( fix[name] ) {
			if ( value != undefined ) elem[fix[name]] = value;
			return elem[fix[name]];
		} else if ( jQuery.browser.msie && name == "style" )
			return jQuery.attr( elem.style, "cssText", value );

		else if ( value == undefined && jQuery.browser.msie && jQuery.nodeName(elem, "form") && (name == "action" || name == "method") )
			return elem.getAttributeNode(name).nodeValue;

		// IE elem.getAttribute passes even for style
		else if ( elem.tagName ) {

			if ( value != undefined ) {
				if ( name == "type" && jQuery.nodeName(elem,"input") && elem.parentNode )
					throw "type property can't be changed";
				elem.setAttribute( name, value );
			}

			if ( jQuery.browser.msie && /href|src/.test(name) && !jQuery.isXMLDoc(elem) )
				return elem.getAttribute( name, 2 );

			return elem.getAttribute( name );

		// elem is actually elem.style ... set the style
		} else {

			// IE actually uses filters for opacity
			if ( name == "opacity" && jQuery.browser.msie ) {
				if ( value != undefined ) {
					// IE has trouble with opacity if it does not have layout
					// Force it by setting the zoom level
					elem.zoom = 1; 

					// Set the alpha filter to set the opacity
					elem.filter = (elem.filter || "").replace(/alpha\([^)]*\)/,"") +
						(parseFloat(value).toString() == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
				}

				return elem.filter ?
					(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100).toString() : "";
			}
			name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});
			if ( value != undefined ) elem[name] = value;
			return elem[name];
		}
	},

Nie trzeba wiele, by stwierdzić, że jQuery wykonuje znacznie więcej operacji, niż ustawienie odpowiedniego stylu. Z ciekawości przyjrzałem się MooTools, który wg zwolenników chwali się bardzo dobrą implementacją najprostszych instrukcji (jak np. ustawianie stylów).

	setStyle: function(property, value){
		switch(property){
			case 'opacity': return this.setOpacity(parseFloat(value));
			case 'float': property = (window.ie) ? 'styleFloat' : 'cssFloat';
		}
		property = property.camelCase();
		switch($type(value)){
			case 'number': if (!['zIndex', 'zoom'].contains(property)) value += 'px'; break;
			case 'array': value = 'rgb(' + value.join(',') + ')';
		}
		this.style[property] = value;
		return this;
	},

Powiedzmy, że tutaj sprawa przedstawia się lepiej, kod jest znacznie przejrzystszy, a instrukcja switch jest o wiele lepszym rozwiązaniem od if. Co ważne, funkcja ta wykonuje to, co ma wykonywać, nie ma tutaj zbędnych rzeczy do sprawdzenia!

Pojedyncza funkcja jednak wiosny nie czyni, przeanalizujmy więc, jak w powyższych frameworkach wyglądałaby implementacja JSowego:

document.getElementById('node').style['margin'] = '2px';

jQuery:

$("#node").css("margin", "2px");

Moo Tools:

$("node").setStyle("margin", "2px");

Sprawa wygląda bliźniaczo podobnie. Przyjrzyjmy się teraz krok po kroku, co musi zrobić jQuery by ustawić margin równy 2 piksele. Najpierw uruchamiana jest funkcja $(), która pełni rolę nakładki na funkcję init(), a ta wygląda tak:

	init: function(selector, context) {
		// Make sure that a selection was provided
		selector = selector || document;

		// Handle HTML strings
		if ( typeof selector  == "string" ) {
			var m = quickExpr.exec(selector);
			if ( m && (m[1] || !context) ) {
				// HANDLE: $(html) -> $(array)
				if ( m[1] )
					selector = jQuery.clean( [ m[1] ], context );

				// HANDLE: $("#id")
				else {
					var tmp = document.getElementById( m[3] );
					if ( tmp )
						// Handle the case where IE and Opera return items
						// by name instead of ID
						if ( tmp.id != m[3] )
							return jQuery().find( selector );
						else {
							this[0] = tmp;
							this.length = 1;
							return this;
						}
					else
						selector = [];
				}

			// HANDLE: $(expr)
			} else
				return new jQuery( context ).find( selector );

		// HANDLE: $(function)
		// Shortcut for document ready
		} else if ( jQuery.isFunction(selector) )
			return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( selector );

		return this.setArray(
			// HANDLE: $(array)
			selector.constructor == Array && selector ||

			// HANDLE: $(arraylike)
			// Watch for when an array-like object is passed as the selector
			(selector.jquery || selector.length && selector != window && !selector.nodeType && selector[0] != undefined && selector[0].nodeType) && jQuery.makeArray( selector ) ||

			// HANDLE: $(*)
			[ selector ] );
	},

Skrypt rozpoznaje, jaki argument podaliśmy (var m = quickExpr.exec(selector);) i podstawia go do funkcji document.getElementById. Następnie uruchamiamy funkcję css:

	css: function( key, value ) {
		return this.attr( key, value, "curCSS" );
	},

Ta radośnie przekierowuje nas do funkcji attr, gdzie czeka nas kolejne kilka warunków if i finałowe ustawienie stylów.

MooTools również uruchamia funkcję $(), która wygląda tak:

function $(el){
	if (!el) return null;
	if (el.htmlElement) return Garbage.collect(el);
	if ([window, document].contains(el)) return el;
	var type = $type(el);
	if (type == 'string'){
		el = document.getElementById(el);
		type = (el) ? 'element' : false;
	}
	if (type != 'element') return null;
	if (el.htmlElement) return Garbage.collect(el);
	if (['object', 'embed'].contains(el.tagName.toLowerCase())) return el;
	$extend(el, Element.prototype);
	el.htmlElement = function(){};
	return Garbage.collect(el);
};

Następny znany nam setStyle i po obiedzie.

Podane przykłady nasuwają kilka wniosków i przemyśleń.

Stawiaj na czysty JS, gdy wiesz, czego oczekujesz

Frameworki zapewniają nam szybkość tworzenia kodu, a więc mniej pisania i więcej czasu na nasze pomysły, kosztem większej ilości dołączonego kodu frameworka. Czy jednak zawsze należy korzystać z funkcji, oferowanych przez framework? Moja odpowiedź brzmi przecząco, o ile mamy do czynienia z rozbudowanymi skryptami (które robią nieco więcej, niż pokazywanie i ukrywanie boksów). Jak zobaczyliśmy, uruchamianie funkcji css w jQuery, podczas każdego ruchu kursora myszki to dość nierozważne posunięcie, szczególnie, jeśli mamy na myśli szybkość działania. Dawałoby to przecież kilkaset wywołań trzech, dość obfitych w kod funkcji. Czy tego chce ambitny developer? W przypadku MooTools jest trochę lepiej, choć nic nie zastąpi funkcji, którą moglibyśmy sami sobie napisać, a która byłaby wydajna w 100%, bo robiłaby to, co chcemy:

function $(id) {
return document.getElementById(id); }

Używając powyższej, otrzymalibyśmy analogicznie do przykładu z poprzednich akapitów:

$("node").style['margin'] = "2px";

Jeśli nie podoba nam się nazwa funkcji $() (np. ze względu kolidowania z jQuery), możemy przecież znaleźć inną, bądź użyć metody wbudowanej we framework – noConflict(). Jej użycie powoduje, że wolna zostaje funkcja $(), a zastępuje ją defaultowo jQuery(). Przykładowe wywołanie wyglądałoby wtedy tak:

jQuery.noConflict();
jQuery(document).ready(function() {});

Oczywiście, jeśli uznalibyśmy, że jQuery to za długi zwrot, możliwe jest jego zastąpienie, np.:

jQuery.noConflict();
$j = jQuery;
$j(document).ready(function() {});

Mootools, ani Prototype niestety nie posiadają takiej metody…

Czyste funkcje DOM potrafią znacznie przyspieszyć skrypt!

Drawter korzystał kilka razy, w dość krytycznych momentach, z funkcji children().each(). Wyszukiwałem w ten sposób pewne elementy, a następnie sprawdzałem ich właściwości. Funkcja ta była uruchamiana często i w testach wydajnościowych zajmowała pierwsze lokaty od końca. Rozwiązanie było tylko jedno: getElementsByTagName. Dość powiedzieć, że skrypt z 500ms zjechał do 100ms. Podobnie jest z append() i kilkoma innymi, „zapchanymi kodem” funkcjami frameworka.

Framework nie dla każdego

Nie chcę, mimo wszystko, tworzyć mitu nieprzydatnych frameworków. Z moich doświadczeń wynika, że frameworki nadają się idealnie do rozwiązań wymagających natychmiastowego, nieskomplikowanego działania. Używając frameworka mamy pewność, że kod był testowany wcześniej przez grupę doświadczonych programistów, dodatkowo przepuszczony był przez wielu testerów. Framework to dana jakość i ułatwienie, które nie przekraczają pewnego poziomu. Ciekawe są natomiast refleksje, płynące z używania ich, gdzie się da.

Bum, jaki towarzyszy jQuery jest dość charakterystyczny. Słyszy się tu i ówdzie coraz częściej, że w jQuery napiszemy wszystko i wszyscy. Framework ten jest porównywany do zbawcy świata JS – napiszemy w nim szybko i łatwo, zrobimy wiele więcej, niż zakładaliśmy. Diabeł tkwi jednak w szczegółach. Fakt jest taki, że do skomplikowanych rozwiązań nie powstał jeszcze framework, który pozwoliłby ominąć czysty JS, oferując gamę w pełni wystarczających funkcji. I długo, bądź wcale nie będzie nim Ext. Bo?

Ponieważ nie frameworki, a właśnie JavaScript jest bardzo wolny. Aplikacje oparte na frameworkach, chyba w żadnym innym nie odbiegają tak wydajnością od czystego kodu, jak w JavaScript. W obecnym kształcie trudno jest podążać w stronę choćby JAVY, która jest wręcz wzorowym przykładem korelacji framework – język. Widoczny jest w światku JavaScript trend, by myśleć i pisać frameworkowo, jednak dopóki zdani jesteśmy na powolną ECMAScript 1.7, platforma (framework) i wydajny sposób jej użycia to nadal główny problem w budowaniu aplikacji JavaScript.

Duża też w tym wszystkim rola developerów przeglądarek. Ta jedyna w swoim rodzaju zależność języka od czynników zewnętrznych (programiści przeglądarki) skazuje JavaScript na dość powolny i niejednostajny rozwój. Pozornie specyfikacja jest taka sama dla wszystkich, jednak szybkość działania diametralnie różna. Jak na razie kierunek wyznacza Safari, z niewiarygodnie szybką implementacją DOM i ECMA. Takiej, a nawet większej prędkości brakuje innym, by mówić JS, myśleć Framework.

Warto odpowiedzieć więc na pytania – co robić z JavaScript? Po co nam frameworki, skoro sam język nie potrafi sprostać temu zagadnieniu? Czy budowanie złożonych aplikacji w JS ma sens? Wreszcie, czy JS musi przegrać walkę z mało lubianymi przez Internautów AIR i Flashem? Byłoby to pyrussowe zwycięstwo oponenta, bo cały czas wydaje mi się, że JS – ze względu na historię i taką, a nie inną postać języka, jest skazany na web, a web jest skazany na JS.

Komentarze

1

No, nareszcie rozsądna wypowiedź na temat frameworków do JS’a, od kogoś kto wie o co w tym chodzi. Do tego dochodzi wg. mnie otępianie ludzi, którzy JavaScriptu nie znają, że (przykładowo) jQuery jest lekiem na wszelkie bolączki i że od tej pory będą mogli pisać świetne aplikacje. No i kładzie potem jeden z drugim łapska na przyjemnym kodzie i powstają kwiatki w stylu przewalania całego drzewa DOM przy przy pomocy $(‘tu dość skomplikowana ścieżka jak: #menu > ul li.nieparzyste a’), zamiast trzymać wyniki w zmiennej. Albo inny przykład prosto z kodu mootools:
if (!['zIndex', 'zoom'].contains(property))
ktoś mi powie jaki jest sens wywoływać funkcję (-> stos), której celem będzie przeleceniem się pętlą po aż dwóch elementach tablicy, która w dodatku musiała na potrzeby tego polecenia zostać zainicjowana?
Frameworki nie są złem samym w sobie, ale czasami mam wrażenie, że ich twórcy zachłysnęli się ‘fajnością’ kodu, jaki mogą zaoferować i uczą beznadziejnych praktyk, w podobie powyższej, gdzie po prostu należy napisać:
if(property!='zIndex' && property!='zoom')

Jeżeli chodzi o samą szybkość, to z przykrością muszę powiedzieć, że kulą u nogi jest firefox, który uniemożliwia zastosowanie wielu ciekawych efektów. Szczególnie myślę tutaj o animacjach. Już IE jest znacznie szybsze na tym poletku.
Na szczęście w nadchodzącym Fx3 developerzy poprawili znacząco wydajność skryptów i nie mogę się doczekać, aż zostanie wydany i upowszechniony.

blue
2

Artykuł bardzo ładny, składny i przyjemny. Gratuluję ;).
A co myślisz o mintajax, jeśli można spytać?

3

mintajax jest calkiem przyjemny, ale nie oparlbym na nim zadnego projektu – to troche taki groch z kapusta, poczawszy od nazewnictwa funkcji, a konczac na rozleglosci i malym zorganizowaniu kodu. Moze to zabrzmi banalnie, ale ten framework nie budzi we mnie zaufania.

Oczywiscie nalezy sie dla tworcy uznanie za kawal nierzadko dobrego kodu, ale majac mootools i jquery, a do czystego Ajaxu polski AdvAjax, po mintajax siegnalbym na koncu.

Dzieki za komentarze
Pozdrowienia :)

4

ja uzywam takiej konstrukcji
div div div cos tam
dzieki temu patrzac w css od razu widze co jest do czego i latwo ogarniam wlasne projekty :)

5

Dokładnie jak w poprzednim poście. Może nie składam stosów div div div, ale bardzo często opieram się o składnię css.

Jako, że programiści jsp nie chętnie wg. nich „cofają” się do poziomu js role prekursorów aplikacji ajax w dwóch dużych firmach informatycznych, w których pracowałem przejmuje podchodzący niejako od dołu webmasterzy (użytkownicy html, css i graficy z PS’a).

W takim przypadku nic dziwnego, że przenoszą ze sobą praktyki z CSS, a następnie są rzucani na coraz większe projekty. A jak w każdej firmie IT czasu jest zawsze za mało, więc używane są znane narzędzia. Co oznacza, że mimo wszystko frameworki są wykorzystywane przy dużych projektach. W takich projektach tak naprawdę „producentem czasu” są mechanizmy komunikacji z bazami danych, więc wystarcza ich optymalizacja i nie ma nacisku na tempo js – taka prawda.

Artykuł bardzo udany.

elon
6

Dlatego właśnie zawsze będę powtarzał – tworzenie zaawansowanych aplikacji w przeglądarce samo w sobie jest nierozsądne. JS, HTML, CSS, PHP, MySQL – ich kombinacja owocuje kilometrami nadmiarowego kodu, narzutem czasu tworzenia i problemami z użytkowaniem. Nie lepiej napisać np. CMS’a w Javie? Też zadziała wszędzie, a nie zniszczy nerwów programistów i użytkowników.

A tak poza tym to świetny artykuł.

7

[...] Pisałem to już jakiś czas temu, napiszę jeszcze raz: Ta jedyna w swoim rodzaju zależność języka od czynników zewnętrznych (przeglądarki) skazuje JavaScript na dość powolny i niejednostajny rozwój. Pozornie specyfikacja jest taka sama dla wszystkich, jednak szybkość działania diametralnie różna [...]

8

Jako programista Javy chcialabym zburzyc mit „przenaszalnosci” aplikacji popelnionych w Javie, albo pisanych „lepiej” niz w innych jezykach.

Kod zalezy od programisty a nie od srodowiska w jakim programuje. A wsrod programistow Javy jest tyle samo lamerow co wsrod programistow PHP, javaScript czy Delphi.

Kania
9

Proponuję zapoznać się z google closure + google closure compiler oraz z GWT.

BX
10

js będzie zawsze, co do frameworków to niestety tak to bywa, prostota kosztem wydajności, dlatego jeśli używamy jQuery to zawsze testujemy nasz kod pod względem wydajności, eliminujemy najwolniejszy kod i zastępujemy go czystym JavaScriptem. Co do stwierdzenia że jQuery ‘że w jQuery napiszemy wszystko i wszyscy’ nie zgadzam się, napisać można, ja też mogę napisać coś w Javie, Rubym czy Pythonie. Najważniejsze jak to będzie napisane i jak będzie działać. Więc żeby napisać dobry i szybki kod w jQuery czy innym frameworku musimy mieć doświadczenie.

11

@Tomek Wójcik and @Kania: Tomku nadmiarowy kod, ale po stronie użytkownika. Kania masz zupełną rację.

Sami widzimy z czasem jak html5 jest pchany na siłę do przodu także przerzucanie coraz większej funkcjonalności w stronę przeglądarek jest chyba jedyną drogą. Na razie przyglądam się temu bez zbędnego optymizmo/sceptycyzmu.

BTW. To nie pomoże w obsłudze html5, ale logo html5 ładne zrobili :)

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>