My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, August 29, 2007

A better JavaScript code evaluation?

Yesterday Ajaxian published a benchmark using Flex and comparing them with Ajax JSON, Dojo and other way to transfer data between client and server.

Interesting point is not that kind of bench (imho) but one comment wrote by Albert Kühner:


A question about AJAX-JSON: Is there a performance difference if you dynamically create a script tag which loads a generated js file in which all objects are serialized to JavaScript objects (like JSON but without having to eval() the XHR response)?


It seems to be a simple and basic question but He was right because the answer is: yes, a script tag is generally interpreted faster than code evaluation using eval, execScript or Function constructor.

I suppose the reason is quite obvious: eval, as execScript or Function, brings temporary local scope inside evaluated code and this cause a little overhead while a script tag is always interpreted only inside global scope (window or super object).

This should be a limit for evaluation common usage (often wrong) but this was the primitive Ajax implementation to interact with server.

Example:

// specific callback to show product informations
function productInfoCallback(product){
alert(product);
}

// function to send something using GET way (query string)
function sendRequest(queryString, callback){
var s = document.createElement("script");
s.type = "text/javascript";
s.src = "interactor.php?".concat(queryString, "&callback=", callback);
(document.getElementsByTagName("head")[0] || document.getElementsByTagName("*")[0])
.appendChild(s).parentNode.removeChild(s);
};

// generic product id
var productId = 123;

// interaction
sendRequest("productId=".concat(productId), "productInfoCallback");


Now try to image server side code, in this case a really simple PHP script:

if(isset($_GET['productId'], $_GET['callback'])){
$productInfo = 'Web Reflection Blog';
//$productInfo = mysql_query ...
header('Content-Type:text/javascript');
exit($_GET['callback'].'("'.addslashes($productInfo).'")');
}
?>


Well done, this is Ajax without Ajax ... a sort of Asyncronous JavaScript and JavaScript (could we call them Ajaj ? :D )


At this point You should agree that there's no way to inject temporary scope inside response and when You load manually or dynamically a script tag the behaviour is the same.

Is it ok? Is it clear?

Well, next step is to use script tag instead of code evaluation to perform quickly (I'm talking about milliseconds) expecially when We use Ajax to recieve a JSON string.

With JSON We usually don't need temporary scope so why We should "wait" more time to evaluate that string?

This is an elementary proposal, the function evalScript:

(evalScript = function(e){
var h = evalScript.node,
s = document.createElement("script");
s.type = "text/javascript";
s.text = e;
h.appendChild(s);
h.removeChild(s);
}).node = document.getElementsByTagName("head")[0] || document.getElementsByTagName("*")[0];


How to use them? It's really simple:

// basic example
evalScript('alert("Hello World")');


The only thing You need to remember is that scope is not injected inside evaluation:

function badEvalScript(){
var string = "something";
evalScript('var string2 = string + " else"');
};

badEvalScript();
// string is not defined
// ... and string2 will be global (window.string) if string is defined



So how to use evalScript ?
You can use them after one or more Ajax interaction if You're loading a script, instead of XML or (x)HTML or text, to run for example dinamically external resources.

You can use evalScript with JSON interactions too, but You should use a little trick if You don't want to be obtrusive.

The simplest solution is, in fact, this one:

evalScript('JSONResponse = '.concat(xhr.responseText));

... but in this web world full of different libraries this is not a good idea, so why don't use evalScript itself to store temporary results?


// generic Ajax interaction ...
evalScript('evalScript.result = '.concat(xhr.responseText));
var response = evalScript.result;


Since JavaScript is, at least inside browsers, single thread, there's no risk about concurrency and in this way You'll not overwrite anything different from single evalScript function.

If You don't believe this function is usually faster than eval, just try to generate a big and complex JSON notation, using every compatible kind of compatible value, so try 10 or more times to eval them or to use my evalScript proposal.

Of course, this is just for performances maniacs :P


Update
You can use evalScript to solve correctly global scope code evaluation too, sorry John (and jQuery team) but I suppose that eval.call(window) and setTimeout(f,0) aren't a "realistic" solution (first one requires this inside evaluation while second one, I suppose, is not syncronous).


function globalScopeEvaluation(){

var testMe = "private scope";
evalScript('alert(testMe)');

};

var testMe = "global scope";

globalScopeEvaluation(); // global scope


Finally, if You call another function to evaluate something, its scope is just global, You don't need to use eval.call or other strategies, am I wrong?


function eval2(data){eval(data)};
var a = 1;
(function(a){eval2('alert(a)')})(2);
// alert 1 ...

So I probably don't understand eval.call usage and setTimeout with data ... anyone could explain me better what does jQuery do with globalEval method? Thank You :-)

5 comments:

kentaromiura said...

I'm wandering if this method on the long have side effects, I think it will slow down the DOM tree if not removed, and if the node is removed(like you do) its side effect is that DOM have to be loaded 2 times, on the append and on the remove...on a big page it will slow down much faster that on a lighter one.

I think this is not a good way to work,dom shouldn't be touched for any good reason (eval work just fine and using Function you can protect a bit more your global vars),
although you just raise a good point, and probably it needs more investigation..

Peter Michaux said...

For quite some time now, Randy Webb (a comp.lang.javascript regular) has been investigating the difficulties of a non-eval script execution. There is a recent thread

http://groups.google.com/group/comp.lang.javascript/browse_frm/thread/7120355459a853f1/a3fa38adb6767dd9#a3fa38adb6767dd9

and several more in the group's archives.

It seems this is a tricky problem.

Andrea Giammarchi said...

@kentaromiura
good points, so please help me to investigate about big dom problems :D

@peter
interesting post, I agree with Randy Webb when He says this sentences:
I have always said that an "AJAX type" site should
have the backend designed to be handled by AJAX on the client side


That's was (half implemented) goal of my old PHP / JavaScript ACE project.

However, that post talks about "strange or wrong code evaluation" while this one just show a proposal ( and the question mark at the end of the topic is there for a reason ;) ) to load dynamically entire scripts (something like dojo.require should be easily implemented using this function) or parse a bit faster JSON strings loaded using Ajax.

This function is not a new way to evaluate code, is just an alternative to run external scripts or parse code quickly.

Finally this function works with every browser I tested (IE5+, Safari2+, Opera9+, FireFox 1.5+ ... probably others) but I'm really interesting about one Randy comment inside this post :-)

Thank You for this link

Andrea Giammarchi said...

P.S. .... Peter, I probably didn't understand your link ... it was Your problem and Randy is right, you're doing something strange, choosing to use common evaluation functions such eval, execScript or Function and call, or Object.eval that's deprecated, is not a good way while my evalScript proposal is for your specific problem (but I didn't think about that when I wrote them).

Well, I hope You'll resolve your problem ;-)

Shamil Mehdiyev said...

Thank you for nice explanation.
There is a problem using sendRequest function. It doesn't load script(or doesn't have sufficient time to load). Problem gets solved by removing ".parentNode.removeChild(s)" part from the function. I think we should use setTimeOut to remove child.

I tested this behavior with GoogleChrome and IE6(may sound weird but i don't like ie just it came default with Win)

I think by immediate removing of child element we deprive it of having free time to evaluate script.
I wander why didn't you notice this or why i have this problem and this makes me think maybe i am wrong because you may have noticed it.