Degrading Script Tags


One thing has always annoyed me about the script tag. Script tags that reference external resources (via the src attribute) are no longer able to execute script embedded within the tag itself.

It doesn’t make sense to me that we’re forced to write:

  1. <script src="some-lib.js"></script>
  2. <script>
  3.   var foo = use_some_lib();
  4.   foo.do.stuff();
  5. </script>

when this is so much more elegant:

  1. <script src="some-lib.js">
  2.   var foo = use_some_lib();
  3.   foo.do.stuff();
  4. </script>

Only one tag – and the semantics would work like this: The external script would attempt to be loaded (as normal). If the load and execution was error-free then the internal script would be executed as well. If the load or execution failed then the internal script would not be executed (this being the important distinction from the first example, shown above).

We can verify these two facts with some tests:

  1. Test 1: Verify that internal scripts aren’t executed, even if an external src is loaded.
  2. Test 2: Verify that internal scripts aren’t executed, even if an external src is not loaded.

Running the tests in Firefox, Opera, Safari, and IE show them all passing (the internal scripts are unable to execute).

With this knowledge in place now, let’s get to the juicy part: How do we actually implement the above style of script-tag-writing in a way that melds well with our desired style of development?

It’s actually terribly simple. I took a copy of jQuery and tacked the following two lines on to the end of the script:

  1. var scripts = document.getElementsByTagName("script");
  2. eval( scripts[ scripts.length - 1 ].innerHTML );

Taking a look at a demo shows it working, using only one script tag:

  1. <script src="jquery1.js">
  2. jQuery(document).ready(function(){
  3.   jQuery("p").addClass("pretty");
  4. });
  5. </script>

We have succeeded! but how does the above code work? The short snippet takes advantage of the way in which script tags are loaded and executed. Each script tag within a page may be downloaded in any order (for example, Internet Explorer 8 and Firefox 3.1 are starting to download many scripts in parallel). BUT their execution must come in order AND they must be executed in their exact place in the DOM.

This means that when the script finally executes that it’ll be the last script in the DOM – and even the last element in the DOM (the rest of the DOM is built incrementally as it hits more script tags, or until the end of the document).

This particular behavior occurs because scripts are able to document.write, affecting the very nature of the document. The following is perfectly legal (even if you can imagine the trauma that it must cause browser vendors – preventing them from performing some perfectly good optimizations):

<script>document.write('<' + '!--');</script>
<script src='some_external_script.js'></script>
<script>document.write('-->');</script>

With the above taken in mind – let’s consider a further optimization to our code. While the above code snippet is generic to any external source file, what if we were to take advantage of the nature of jQuery, simplifying its behavior?

Virtually all jQuery-using code is encased within a call to the document ready function (which delays execution of attached handlers until the DOM is full loaded).

If we were to make some adjustments to our script addition, like so:

  1. var scripts = document.getElementsByTagName("script");
  2. var script = scripts[ scripts.length - 1 ].innerHTML;
  3. if ( script ) {
  4.   jQuery(document).ready(function(){
  5.     jQuery.globalEval( script );
  6.   });
  7. }

We can now get the full advantage of having our scripts inline and not having to write out a document ready call, like so:

  1. <script src="jquery2.js">
  2. jQuery("p").addClass("pretty");
  3. </script>

Pretty sexy, huh?

The only thing holding me back from blindly adding the above snippet directly in to jQuery, today, is a small possibility: The chance that someone could copy the full library and stick it in an embedded script tag (rather than calling it from an external file).

Right now if you were to stick either of the above snippets in an embedded script tag you would end up with a never-ending loop. Consider this code:

  1. <script>
  2. var scripts = document.getElementsByTagName("script");
  3. eval( scripts[ scripts.length - 1 ].innerHTML );
  4. </script>

This code would look for the last script on the page, find it (it would be the script itself), and execute – causing a piece of recursion to occur. It would become necessary to flag the script to prevent it from executing again.

With a little bit of extra work, we can make it possible (demo):

  1. (function(){
  2. var scripts = document.getElementsByTagName("script");
  3. var curScript = scripts[ scripts.length - 1 ];
  4.  
  5. if ( curScript.executed )
  6.   return;
  7.  
  8. // ... jQuery ...
  9.  
  10. curScript.executed = true;
  11. var script = curScript.innerHTML;
  12. if ( script ) {
  13.   jQuery(document).ready(function(){
  14.     jQuery.globalEval( script );
  15.   });
  16. }
  17. })();

I think this particular technique could serve to simplify a number of web pages, even if it’s only because:

  1. There’s one less script tag on the page.
  2. The dependency link between the script being loaded and the script being executed becomes exceedingly obvious.
  3. The dependent script won’t execute (and subsequently cause errors) if the external script is unable to load.
  4. The need for user-authored window load, document ready, etc. functions are no longer necessary (can be hidden within the library).

It’s definitely something that’ll need to be mulled over as it’s pretty fundamentally changing the existing behavior of the script tag (although, it is being done in a completely safe and backwards-compatible manner). But if there’s sufficient interest it seems as if this is one area in which external scripts could stand to offer an obvious improvement to the developer.

Posted: August 26th, 2008


If you particularly enjoy my work, I appreciate donations given with Gittip.

53 Comments (Show Comments)



Comments are closed.
Comments are automatically turned off two weeks after the original post. If you have a question concerning the content of this post, please feel free to contact me.


Secrets of the JavaScript Ninja

Secrets of the JS Ninja

Secret techniques of top JavaScript programmers. Published by Manning.

Ukiyo-e Database and Search

Ukiyo-e.org

Japanese woodblock print database and search engine.


John Resig Twitter Updates

@jeresig

Infrequent, short, updates and links.