How Does the Location of a Script Tag in a Page Affect a JavaScript Function That Is Defined in It

How does the location of a script tag in a page affect a JavaScript function that is defined in it?

Telling people to add <SCRIPT> only in the head sounds like a reasonable thing to do, but as others have said there are many reasons why this isn't recommended or even practical - mainly speed and the way that HTML pages are generated dynamically.

This is what the HTML 4 spec says :

The SCRIPT element places a script
within a document. This element may
appear any number of times in the HEAD
or BODY of an HTML document.

And some sample HTML. Doesn't it look pretty all formatted here :)

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<HTML>
<HEAD>
<TITLE>A document with SCRIPT</TITLE>
<META http-equiv="Content-Script-Type" content="text/tcl">
<SCRIPT type="text/vbscript" src="http://someplace.com/progs/vbcalc">
</SCRIPT>
</HEAD>
<BODY>
<SCRIPT type="text/javascript">
...some JavaScript...
</SCRIPT>
</BODY>
</HTML>

And something to look forward to in HTML 5 :

New async attribute in <SCRIPT> :

Note: There are ways [sic] a script can be
executed:

The async attribute is "true": The
script will be executed asynchrously
with the rest of the page, so the
script will be executed while the page
continues the parsing.

The async attribute is "false", but
the defer attribute is "true": The
script will be executed when the page
is finished with the parsing.

Does the script tag position in HTML affects performance of the webpage?

Javascript assets, by default, tend to block any other parallel downloads from occurring. So, you can imagine if you have plenty of <script> tags in the head, calling on multiple external scripts will block the HTML from loading, thus greeting the user with a blank white screen, because no other content on your page will load until the JS files have completely loaded.

In order to combat this issue, many developers have opted to placing JS at the bottom of the HTML page (before the </body> tag). This seems logical because, most of the time JS is not required until the user begins interacting with the site. Placing JS files at the bottom also enables progressive rendering.

Alternatively, you can choose to load Javascript files asynchronously. There are plenty of existing methods which this can be accomplished by:

XHR Eval

var xhrObj = getXHRObject();
xhrObj.onreadystatechange =
function() {
if ( xhrObj.readyState != 4 ) return;
eval(xhrObj.responseText);
};
xhrObj.open('GET', 'A.js', true);
xhrObj.send('');

Script DOM Element

var se = document.createElement('script');
se.src = 'http://anydomain.com/A.js';
document.getElementsByTagName('head')
[0].appendChild(se);

Meebo Iframed JS

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var doc = iframe.contentWindow.document;
doc.open().write('<body onload="insertJS()">');
doc.close();

To name a few...

Note: Only a maximum of five scripts can be loaded in parallel in current browsers.


ForIE there is the defer attribute you can use like so:

<script defer src="jsasset.js" type="text/javascript"></script>

When set, this boolean attribute
provides a hint to the user agent that
the script is not going to generate
any document content (e.g., no
"document.write" in javascript) and
thus, the user agent can continue
parsing and rendering.

Update 2022: all browsers now accept the defer attribute.

Where should I put script tags in HTML markup?

Here's what happens when a browser loads a website with a <script> tag on it:

  1. Fetch the HTML page (e.g. index.html)
  2. Begin parsing the HTML
  3. The parser encounters a <script> tag referencing an external script file.
  4. The browser requests the script file. Meanwhile, the parser blocks and stops parsing the other HTML on your page.
  5. After some time the script is downloaded and subsequently executed.
  6. The parser continues parsing the rest of the HTML document.

Step #4 causes a bad user experience. Your website basically stops loading until you've downloaded all scripts. If there's one thing that users hate it's waiting for a website to load.

Why does this even happen?

Any script can insert its own HTML via document.write() or other DOM manipulations. This implies that the parser has to wait until the script has been downloaded and executed before it can safely parse the rest of the document. After all, the script could have inserted its own HTML in the document.

However, most JavaScript developers no longer manipulate the DOM while the document is loading. Instead, they wait until the document has been loaded before modifying it. For example:

<!-- index.html -->
<html>
<head>
<title>My Page</title>
<script src="my-script.js"></script>
</head>
<body>
<div id="user-greeting">Welcome back, user</div>
</body>
</html>

JavaScript:

// my-script.js
document.addEventListener("DOMContentLoaded", function() {
// this function runs when the DOM is ready, i.e. when the document has been parsed
document.getElementById("user-greeting").textContent = "Welcome back, Bart";
});

Because your browser does not know my-script.js isn't going to modify the document until it has been downloaded and executed, the parser stops parsing.

Antiquated recommendation

The old approach to solving this problem was to put <script> tags at the bottom of your <body>, because this ensures the parser isn't blocked until the very end.

This approach has its own problem: the browser cannot start downloading the scripts until the entire document is parsed. For larger websites with large scripts and stylesheets, being able to download the script as soon as possible is very important for performance. If your website doesn't load within 2 seconds, people will go to another website.

In an optimal solution, the browser would start downloading your scripts as soon as possible, while at the same time parsing the rest of your document.

The modern approach

Today, browsers support the async and defer attributes on scripts. These attributes tell the browser it's safe to continue parsing while the scripts are being downloaded.

async

<script src="path/to/script1.js" async></script>
<script src="path/to/script2.js" async></script>

Scripts with the async attribute are executed asynchronously. This means the script is executed as soon as it's downloaded, without blocking the browser in the meantime.
This implies that it's possible that script 2 is downloaded and executed before script 1.

According to http://caniuse.com/#feat=script-async, 97.78% of all browsers support this.

defer

<script src="path/to/script1.js" defer></script>
<script src="path/to/script2.js" defer></script>

Scripts with the defer attribute are executed in order (i.e. first script 1, then script 2). This also does not block the browser.

Unlike async scripts, defer scripts are only executed after the entire document has been loaded.

(To learn more and see some really helpful visual representations of the differences between async, defer and normal scripts check the first two links at the references section of this answer)

Conclusion

The current state-of-the-art is to put scripts in the <head> tag and use the async or defer attributes. This allows your scripts to be downloaded ASAP without blocking your browser.

The good thing is that your website should still load correctly on the 2% of browsers that do not support these attributes while speeding up the other 98%.

References

  • async vs defer attributes
  • Efficiently load JavaScript with defer and async
  • Remove Render-Blocking JavaScript
  • Async, Defer, Modules: A Visual Cheatsheet

Javascript. Does the position of the code affect performance?

If there is a lot of JS, put it at the end of the document. Although this makes no difference on load time, the user will see the page sooner and can begin to read it while your js loads, as opposed to seeing nothing until your JS is downloaded (which is what happens when you put it in the head). It merely makes the download appear faster. This would also solve the problem mentioned above about the script executing on an unfinished document, although for that, an even better solution is to use window.onload().

javascript defines scope by script tags?

This isn't a scope issue. If you define a function (in the global scope) in one script element, then you can use it in another.

However, script elements are parsed and executed as they are encountered.

Hoisting won't work across script elements. A function defined in a later script element won't be available during the initial run of an earlier script element.

You either need to swap the order of your script elements, or delay the function call until after the script that defines it has run (e.g. by attaching it to an onload event handler).

<script>
function go() {
alert('');
}
</script>
<script>
go();
</script>

or

<script>
window.addEventListener("load", function () {
go();
}, false);
</script>
<script>
function go() {
alert('');
}
</script>

Access variables and functions defined in page context using a content script

Underlying cause:

Content scripts are executed in an "isolated world" environment.

Solution:

Inject the code into the page using DOM - that code will be able to access functions/variables of the page context ("main world") or expose functions/variables to the page context (in your case it's the state() method).

  • Note in case communication with the page script is needed:

    Use DOM CustomEvent handler. Examples: one, two, and three.

  • Note in case chrome API is needed in the page script:

    Since chrome.* APIs can't be used in the page script, you have to use them in the content script and send the results to the page script via DOM messaging (see the note above).

Safety warning:

A page may redefine or augment/hook a built-in prototype so your exposed code may fail if the page did it in an incompatible fashion. If you want to make sure your exposed code runs in a safe environment then you should either a) declare your content script with "run_at": "document_start" and use Methods 2-3 not 1, or b) extract the original native built-ins via an empty iframe, example. Note that with document_start you may need to use DOMContentLoaded event inside the exposed code to wait for DOM.

Table of contents

  • Method 1: Inject another file - ManifestV3 compatible
  • Method 2: Inject embedded code - MV2
  • Method 2b: Using a function - MV2
  • Method 3: Using an inline event - ManifestV3 compatible
  • Method 4: Using executeScript's world - ManifestV3 only
  • Dynamic values in the injected code

Method 1: Inject another file (ManifestV3/MV2)

Particularly good when you have lots of code. Put the code in a file within your extension, say script.js. Then load it in your content script like this:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);

The js file must be exposed in web_accessible_resources:

  • manifest.json example for ManifestV2

    "web_accessible_resources": ["script.js"],
  • manifest.json example for ManifestV3

    "web_accessible_resources": [{
    "resources": ["script.js"],
    "matches": ["<all_urls>"]
    }]

If not, the following error will appear in the console:

Denying load of chrome-extension://[EXTENSIONID]/script.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

Method 2: Inject embedded code (MV2)

This method is useful when you want to quickly run a small piece of code. (See also: How to disable facebook hotkeys with Chrome extension?).

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Note: template literals are only supported in Chrome 41 and above. If you want the extension to work in Chrome 40-, use:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
'// Beware! This array have to be joined',
'// using a newline. Otherwise, missing semicolons',
'// or single-line comments (//) will mess up your',
'// code ----->'].join('\n');

Method 2b: Using a function (MV2)

For a big chunk of code, quoting the string is not feasible. Instead of using an array, a function can be used, and stringified:

var actualCode = '(' + function() {
// All code is executed in a local scope.
// For example, the following does NOT overwrite the global `alert` method
var alert = null;
// To overwrite a global variable, prefix `window`:
window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

This method works, because the + operator on strings and a function converts all objects to a string. If you intend on using the code more than once, it's wise to create a function to avoid code repetition. An implementation might look like:

function injectScript(func) {
var actualCode = '(' + func + ')();'
...
}
injectScript(function() {
alert("Injected script");
});

Note: Since the function is serialized, the original scope, and all bound properties are lost!

var scriptToInject = function() {
console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output: "undefined"

Method 3: Using an inline event (ManifestV3/MV2)

Sometimes, you want to run some code immediately, e.g. to run some code before the <head> element is created. This can be done by inserting a <script> tag with textContent (see method 2/2b).

An alternative, but not recommended is to use inline events. It is not recommended because if the page defines a Content Security policy that forbids inline scripts, then inline event listeners are blocked. Inline scripts injected by the extension, on the other hand, still run.
If you still want to use inline events, this is how:

var actualCode = '// Some code example \n' + 
'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Note: This method assumes that there are no other global event listeners that handle the reset event. If there is, you can also pick one of the other global events. Just open the JavaScript console (F12), type document.documentElement.on, and pick on of the available events.

Method 4: Using chrome.scripting API world (ManifestV3 only)

  • Chrome 95 or newer, chrome.scripting.executeScript with world: 'MAIN'
  • Chrome 102 or newer, chrome.scripting.registerContentScripts with world: 'MAIN', also allows runAt: 'document_start' to guarantee early execution of the page script.

Unlike the other methods, this one is for the background script or the popup script, not for the content script. See the documentation and examples.

Dynamic values in the injected code (MV2)

Occasionally, you need to pass an arbitrary variable to the injected function. For example:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
alert(GREETING + NAME);
};

To inject this code, you need to pass the variables as arguments to the anonymous function. Be sure to implement it correctly! The following will not work:

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME + ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
// ^^^^^^^^ ^^^ No string literals!

The solution is to use JSON.stringify before passing the argument. Example:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

If you have many variables, it's worthwhile to use JSON.stringify once, to improve readability, as follows:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]).slice(1, -1) + ')';

Dynamic values in the injected code (ManifestV3)

  • Method 1 can set the URL of the script element in the content script:

    s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({foo: 1});

    Then script.js can read it:

    const params = new URLSearchParams(document.currentScript.src.split('?')[1]);
    console.log(params.get('foo'));
  • Method 4 executeScript has args parameter, registerContentScripts currently doesn't (hopefully it'll be added in the future).

Is it wrong to place the script tag after the /body tag?

It won't validate outside of the <body> or <head> tags. It also won't make much difference — unless you're doing DOM manipulations that could break IE before the body element is fully loaded — to putting it just before the closing </body>.

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

Function Undefined: Must a javascript function be defined within the same file?

Got it working. The issue was definitely multi-faceted, but I figured it out.

First off the use of RequireJS had an impact on updateNotification(), in that it couldn't be called cross-file, and was therefore considered undefined. I assume this because of how RequireJS loads files, and I'll look into the documentation later (and post an edit if I find anything relevant).

Secondly, updateNotification() would again be considered undefined when encased within jQuery's DOM ready loader $(document).ready(function(){}). However updateNotification() contains executions which require jQuery, so I had to encase the contents of the function in $(document).ready(function(){}).

This is an issue very unique to RequireJS/jQuery, hence why in most use cases this wouldn't occur.

Side note: The tags are edited to reflect this.



Related Topics



Leave a reply



Submit