0

Instead of having an external .js file, we can inline Javascript directly in HTML, i.e.

Externalized version

<html>
<body>
  <script type="text/javascript" src="/app.js"></script>
</body>
</html>

Inlined version

<html>
<body>
  <script type="text/javascript">
    // app.js inlined
  </script>
</body>
</html>

However, it's not recommended:

The main reason is caching and pre-compiling - in the externalized version, the browser can download, pre-compile and store the file once for multiple pages, while it cannot do the same for inlined version.

However, is it possible to do something along these lines:

Inlined keyed version

<html>
<body>
  <script type="text/javascript" hash="abc">
    // app.js inlined
  </script>
</body>
</html>

That is, do this:

  • In the first invocation, send the whole script and somehow tell the browser that the script hash is abc
  • Later, when the browser loads that or other pages containing the same script, it will send this key as a cookie. The server will only render the contents of the script if the key has been received.

That is, if the browser already knows about the script, the server will render just this:

Inlined keyed version, subsequent fetches (of the same or other pages)

<html>
<body>
  <script type="text/javascript" hash="abc">
  </script>
</body>
</html>

where notably the script contents are empty.

This would allow for shorter script fetching with a natural fallback.

Is the above possible? If not, is some other alternative to the above possible?

Community
  • 1
  • 1
levant pied
  • 3,886
  • 5
  • 37
  • 56
  • Maybe possible, but seems much more difficult than just linking external JS. – Oriol Dec 26 '14 at 23:06
  • You can read more about ETags: https://en.wikipedia.org/wiki/HTTP_ETag – Joseph Dec 26 '14 at 23:09
  • 1
    Which problems with external scripts are you trying to avoid? The performance penalty from having two requests instead of one? – Yogu Dec 26 '14 at 23:24
  • How do you plan to execute the script on page2 what was sent to page1? – Salman A Dec 26 '14 at 23:45
  • @Oriol Agreed, but there are two requests instead of one. – levant pied Dec 29 '14 at 22:00
  • @Yogu Not just performance penalty, but the page load "feeling". For low page visit sites, most of what you link to will not be cached, so you load slower than you need to and you know nobody waits more than a split second these days... – levant pied Dec 29 '14 at 22:02
  • @JosephtheDreamer Thanks, but that still doesn't solve the one-request thing - it still requires waiting for the second request to come back. For 100 line scripts, that's mostly waiting for a roundtrip, not waiting for the transfer itself. Am I missing something? – levant pied Dec 29 '14 at 22:03
  • @SalmanA Each script will have a constant hash. Which page references it doesn't really matter. If page1 requests script with hash=abc, it will either get the full contents or (similar to ETag thing Joseph mentioned) will get nothing, as the client already has the latest version and will just insert it there. If page2 requests the same script then, the same thing will happen - it already has the script cached locally and will reuse it, server will send the page minus that script. – levant pied Dec 29 '14 at 22:06
  • *" in the externalized version, the browser can download, **pre-compile**"* - the browser does not pre-compile scripts. When the browser sees a script tag with `src`, it will issue that request or retrieve from cache and run the script as is. Nothing is stored and pre-compiled. – Joseph Dec 30 '14 at 02:44
  • What you're trying to do is simply just bloating the initial load, and saving yourself in the additional loads. It's pretty much the same for normal browser caching, minus the additional requests. Also, 100-line scripts? Shouldn't you be minifying a bunch of these to one script instead? – Joseph Dec 30 '14 at 03:14
  • Hmm, may be just store the script in local storage along with the key? – Salman A Dec 30 '14 at 07:31

3 Answers3

4

I don't know of a way to do what you asked, so I'll provide an alternative that might still suit your needs.

If you're really after a low latency first page load, you could inline the script, and then after the page loads, load the script via url so that it's in the browser cache for future requests. Set a cookie once you've loaded the script by direct url, so that your server can determine whether to inline the script or provide the external script url.

first page load

<script>
// inlined my-script.js goes here.
</script>
<script>
$(function(){
    // load it again, so it's in the browser cache.
    // notice I'm not executing the script, just loading it.
    $.ajax("my-script.js").then(function(){
        // set a cookie marking this script as cached
    });
});
</script>

second page load

<script src="my-script.js"></script>

Obviously, this has the drawback that it loads the script twice. It also adds additional complexity for you to take care of when you update your script with new code - you need to make sure you address the cookie being for a old version.

I wouldn't bother with all this unless you really feel the need to optimize the first page. It might be worth it in your case.

goat
  • 31,486
  • 7
  • 73
  • 96
  • Thanks - that is an interesting approach! I think this would work nicely. Your comment about optimizing the first page is what I'm after - for sites with low visitor count, I assume it will always mean the page would load slower than usual, as it's likely not in the cache. – levant pied Dec 29 '14 at 22:14
  • 1
    My vote for this. Except for a few tweaks, e.g. instead of fetching script on document.ready you should download after page load. And use script tag injection. And alter the script to check if it has already executed. – Salman A Dec 30 '14 at 07:35
2

The Concept

Here's an interesting approach (after being bugged by notifications :P)

You could have the server render your script this way. Notice the weird type attribute. That's to prevent the script from executing. We'll get to that in a second.

<script type="text/cacheable" data-hash="9182n30912830192c83012983xm019283x">
  //inline script
</script>

Then create a library that looks for these scripts with weird types, get the innerHTML of these scripts, and execute them in the global context as if they were normally executing (via eval or new Function). This makes them execute like normal scripts. Here's a demo:

<script type="text/cacheable" data-hash="9182n30912830192c83012983xm019283x">
  alert(a);
</script>

<script type="text/cacheable" data-hash="9182n30912830192c83012983xm019283x">
  alert(b);
</script>

<script>

    // Let's say we have a global
    var a = "foo";
    var b = "bar"

    // Getting the source
    var scripts = Array.prototype.slice.call(
        document.querySelectorAll('script[type="text/cacheable"]')
    );

    scripts.forEach(function(script){
       // Grabbing
       var source = script.innerHTML;
       // Create a function (mind security on this one)
       var fn = new Function(source);
       // Execute in the global scope
       fn.call(window);
    });

</script>

However...

Since you have the script source (the innerHTML), you can cache them somewhere locally (like in localStorage) and use the hash as its identifier. Then you can store the same hash in the cookie, where future page-requests can tell the server "Hey, I have cached script with [hash]. Don't print the script on the page anymore". Then you'll get this in future requests:

<script type="text/cacheable" data-hash="9182n30912830192c83012983xm019283x"></script>

That covers up the first half. The second phase is when your library sees an empty script. The other thing your library should do is when it sees an empty script, it should look up for that script with that hash in your local storage, get the script's source and execute it like you just did in the first place.

The Catch

Now there's always a trade-off in everything, and I'll highlight what I can think of here:

Pros

  • You only need one request for everything. Initial pageload contains scripts, subsequent pages become lighter because of the missing code, which is already cached by then.

  • Instant cache busting. Assuming the hash and code are 1:1, then changing the content should change the hash.

Cons

  • This assumes that pages are dynamic and are never cached. That's because if you happen to create a new script, with new hash, but had the client cache the page, then it will still be using the old hashes thus old scripts.

  • Initial page load will be heavy due to inlined scripts. But this can be overcome by compressing the source using a minifier on the server. Overhead of minification can also be overcome by caching minified results on the server.

  • Security. You'll be using eval or new Function. This poses a big threat when unauthorized code manages to sneak in. In addition, the threat is persistent because of the caching.

  • Out of sync pages. What happens if you get an empty script, whose hash is not in the cache? Perhaps the user deleted local storage? You'll have to issue a request to the server for it. Since you want the source, you'll have to have AJAX.

  • Scripts are not "normal". Your script is best put at the end of the page so that all inline scripts will be parsed by then. This means your scripts execute late and never in the time they get parsed by the browser.

  • Storage limits. localStorage has a size limit of 5-10MB, depending on which browser we're talking about. Cookies are limited to 4KB generally.

  • Request size. Note that cookies are shipped up to the server on request and down to the browser on response. That additional load might be more of a hassle than it is for good.

  • Added server-side logic. Because you need to know what needs to be added, you need to program your server to do it. This makes the client-side implementation dependent on the server. Switching servers (say from PHP to Python) wouldn't be as easy, as you need to port over the implementation.

Joseph
  • 117,725
  • 30
  • 181
  • 234
1

If your <script> is not introduced as type=text/javascript, it will simply not be executed. So you could have many tags like theses:

<script type="text/hashedjavascript" hash="abc">...</script>
<script type="text/hashedjavascript" hash="efg">...</script>

Then when the DOM is loaded, pick one and evaluate it. I made an example here: http://codepen.io/anon/pen/RNGQEM

But it smells, real bad. It's definitely better to fetch two different files. Actually what you should do, is have a single file my-scripts.js that contains the code for each of your script, wrapped in a function

// file: my-scripts.js

function script_abc(){
    // what script abc is supposed to do
}
function script_efg(){
    // what script efg is supposed to do
}

Then execute whatever your cookie tells you to. This is how AMD builders concatenate multiples files in one.

Also look for an AMD library such as requirejs

Edit: I misunderstood your question, removed the irrelevant part.

TKrugg
  • 2,255
  • 16
  • 18
  • Nice, good thinking! However, I don't see how I would be able to not send the script and get it executed by virtue of "I know this script already". – levant pied Dec 29 '14 at 22:17
  • You're right, I didn't understand correctly what you wanted to do. Then you could follow my other advice: wrap each script in a function, put all thoses functions in one script file, and every time your load a page, execute the function you want. – TKrugg Dec 29 '14 at 22:45