1. Interaction
  2. JavaScript
  3. Getting script into a page

Interaction · JavaScript 02

Getting script into a page

Before you can study the language you need to run it, and running it means putting script in an HTML document. That turns out to be harder than it looks. Where the script sits and how it is loaded decide what it can see, what it holds up, and whether the page works while it waits. This page starts with one line of hello world and improves a linked script until it stops breaking.

JavaScript is the main client-side scripting language of web development. Each word of that description says something. It runs on the client, in the visitor's browser. Its focus is the web, although it has no such limit: the previous page shows it running in Node and elsewhere. And it is scripting, delivered as source text and run by the page that loads it. It makes up one side of the client-side triangle with HTML and CSS, and it reaches the other two through the document object model, or DOM.

The advice for this section is to get the language right first and then attack the details, so that you know where to place blame. This page breaks that advice on purpose. You cannot try the language without a page to run it in, so the language waits until the next page.

1. Hello world

Here is the smallest useful page with script in it. The browser parses the HTML from the top. When it reaches the script element it stops, runs what is inside, and continues.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>JavaScript Hello World</title> </head> <body> <h1>First JavaScript</h1> <hr> <script> document.write('Hello World from JavaScript!'); </script> </body> </html>

document.write puts text into the stream the parser is reading, at the point where the script sits, and the parser then reads that text as HTML. It makes a good first example because nothing else is needed to see a result. It should stay a first example. MDN marks it as deprecated and strongly discourages it, and the rest of these pages use console.log and textContent instead. It has three behaviors you may meet in old code. Called after the page has loaded, it clears the whole document. Called from a deferred or async script, it is ignored and the browser logs a message. And since it parses its input as HTML, passing it anything a visitor typed opens the door to cross-site scripting. A script might call it, which is why the parser has to stop at a classic script. The rest of this page works through that problem.

HTML and JavaScript do not mix freely

The interplay between HTML and JavaScript can be tricky at first. Inside a script element the text is JavaScript, so an HTML tag there is a syntax error, and the whole script fails.

<script> <strong> document.write('Hello World from JavaScript!'); </strong> </script>

There are two ways to get the bold text you wanted. You can put the tags inside the string, so that they reach the parser as HTML, or you can put the script inside the tags.

<script> document.write('<strong>Hello World from JavaScript!</strong>'); </script> <strong> <script> document.write('Hello World from JavaScript!'); </script> </strong>

The mixing can go wrong in the other direction as well. The HTML parser does not understand JavaScript, so it ends the script at the first </script> it sees, even one inside a string. A script that needs that text in a string writes it as <\/script>.

This playground has only an HTML panel, and its script runs as the preview is parsed, as it would in your own file. Change the message, then try the bad version.

2. JavaScript's silent failings

If you tried that last change, the message disappeared and the rest of the page looked fine. Nothing on the page said anything was wrong. Browsers give visitors minimal feedback that a script has failed. The only way to see the message is the JavaScript console in your dev tools. (In that playground, look in your browser's console rather than the playground's panel, which connects only after the HTML has been parsed.)

Silent errors were not always the rule. Older browsers put a warning icon in the corner of the status bar, and Internet Explorer could show a dialog for each error, giving the line, the character and the message, and asking whether to continue running scripts on the page. That went away because it confused the people using the page, who could do nothing about it. So here is a small experiment: browse the web for a while with your console open. You will find errors on sites you use every day, and many of those sites still appear to work.

A failure stops the script it happens in, at that line. The rest of that script does not run, while the page and any other scripts carry on.

So keep the console open whenever you work on script. If a page looks right and does nothing, read the console before you read your code. Errors and async covers reading a stack trace and catching errors.

3. Four ways to add script

There are four standard ways to include script in an HTML document:

  1. Within the <script> element.
  2. As a linked file, through the src attribute of the <script> element.
  3. Within an HTML event handler attribute such as onclick.
  4. Through the pseudo-URL javascript: syntax, referenced by a link.

Hello world used the first. The next two sections stay with it, because they apply to every script element. Section 6 covers the third and fourth, and section 7 returns to the second, which is the way most script should reach a page.

4. Generalized top-to-bottom execution

You can use as many script elements as you like, in both the head and the body, and they run one after another unless you direct otherwise. All classic scripts in a page also share one global scope, so a function defined in the head can be called from the body.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>JavaScript in the Head</title> <script> function alertTest() { alert('Danger! Danger! JavaScript ahead!'); } </script> </head> <body> <h2>Script in the Head</h2> <hr> <script> alertTest(); </script> </body> </html>

A sequence of alerts makes the order easy to feel, because alert stops everything until it is dismissed. Each script runs when the parser reaches it, and the parser goes no further until the script has finished.

<h1>Ready start</h1> <script> alert('First script ran'); </script> <h2>Running...</h2> <script> alert('Second script ran'); </script> <h2>Keep running</h2> <script> alert('Third script ran'); </script> <h2>Stop!</h2>

The playground makes the same point without the dialogs. Each script counts the headings that exist at the moment it runs. The first finds one, and the count grows as the parser moves down the page, even though four headings are in the source from the start. A script can only see what the parser has already reached.

Now add network calls for scripts, which bring timing into it, and the order in which things happen can get more dangerous. Script's blocking nature has produced simple heuristics such as "move the script to the bottom". They do help, as you can observe, but they encourage guessing what will happen, and guessing is not recommended. The rest of this page replaces that guess with knowing what the parser is doing.

5. When script doesn't run

Script masking

You will sometimes see old pages that wrap script in comments. This is script masking, or script hiding.

<script type="text/javascript"> <!-- put your JavaScript here //--> </script>

A browser that did not recognize the script element would print its contents on the screen, so authors hid the code from it in an HTML comment. Browsers of that kind are long gone, and your goal is to separate code from markup anyway, so do not write it. It continues to run in a classic script because the language keeps <!-- as a kind of comment for the sake of old pages. A module rejects it with a SyntaxError. The type="text/javascript" on that sample is another leftover: JavaScript is the default, so you can leave it off.

The noscript element

The noscript element provides an alternative rendering for browsers that have script turned off.

<noscript> <strong> Either your browser does not support JavaScript or it is currently disabled. </strong> </noscript>

A longer form tells the visitor what to do next. Here the script writes one message and the noscript holds the other, so each visitor gets one of them.

<body> <script> document.write('If you see this you have JavaScript'); </script> <noscript> <h1>JavaScript required</h1> <p>Read how to <a href="/errors/noscript.html">rectify this problem</a></p> </noscript> </body>

Note what noscript does not do. It shows only when scripting is off. If scripting is on and your script fails to download or throws an error, the visitor gets neither the script's work nor the fallback. The head lesson makes the same point: scripts fail for more people than those who turn them off.

The meta refresh trick

The noscript element can also be used to keep visitors without JavaScript out of a site or app altogether. Put a meta refresh inside a noscript in the head, and those visitors are redirected to an error page right away.

<noscript> <meta http-equiv="Refresh" content="0;URL=/errors/noscript.html"> </noscript>

This has possible downsides. Redirects may be turned off along with JavaScript at certain security settings. It also isn't friendly to bots, since a crawler that runs no script follows the redirect and never sees the page. Whether that is a bad thing depends on whether you are applying it to a site or an app. Most apps should not be traversed by bots anyway. How much JavaScript? returns to whether a page should require script at all.

6. Attributes and pseudo-URLs

Event handler attributes

HTML defines a set of event handler attributes related to JavaScript events, such as onclick and onmouseover, and you can bind JavaScript statements to them. The statements run when the event happens to that element. Returning false from the attribute cancels what the browser would have done, so the link below does not go to the search page.

<body onload="alert('page loaded')"> <form action="#" method="GET"> <input type="button" value="press me" onclick="alert('You pressed my button!')"> </form> <p onmouseover="alert('on')" onmouseout="alert('off')">Roll me!</p> <p> <a href="https://www.google.com" onclick="alert('No Search for you!'); return false"> Search Google </a> </p> </body>

Event handler attributes are closely intertwined with the HTML, which has good news and bad news. The good news is that you can clearly see what is bound to which element. The bad news is that you have mixed your code deep into your markup. That goes against the idea of separation of concerns and can make maintenance difficult later on. A site with a strict Content Security Policy also has to forbid them, as they are inline script.

An attribute also isn't advanced event binding. It gives you one handler per event and no easy way to add other bindings or control the event itself. Script that uses addEventListener can add as many as it likes, and the event is generally more accessible there. This does raise a question. Do you want things to be able to bind in easily, without you as the developer being aware of them and accounting for them? Section 8 comes back to it.

<!-- No easy way to add other bindings to the element or control the event --> <input type="button" id="myBtn" value="Press Me" onclick="someFunc()"> <script> // The event is generally more accessible here document.getElementById('myBtn').addEventListener('click', function (e) { // Some function }); </script>

Note the parentheses in onclick="someFunc()". The attribute's value is statements to run, so onclick="someFunc" names the function and never calls it, with no error. Events, step 1 compares the attribute, the element property and addEventListener in full.

JavaScript pseudo-URLs

The javascript: pseudo-URL also triggers script statements, when a link using it is followed.

<a href="javascript: alert('hi');">Click me</a>

You could once type such a URL directly in the browser's location box, for example javascript:alert(5+9). Browsers have restricted that, because attackers talked people into pasting code there. Firefox's bug report records the reasoning for its own change. What happens now varies by browser, so use the console for this kind of experiment.

Pseudo-URLs do not degrade well when script is unavailable, and they may confuse a user when some links load pages and others cause actions. They carry a trap too. If the last expression's value is a string, the browser replaces the page with that string as a new document. This is why old code is full of javascript:void(0), which guarantees a value of undefined. They may be blocked by a Content Security Policy as well. For an action, use a button with a listener. For a place to go, use a link with a URL. You will read pseudo-URLs in older "app" code, and they have one use left in bookmarklets, which are small scripts saved as bookmarks.

7. Linked scripts

Like linked style sheets, you can store JavaScript in a separate file and reference it. Use a .js file, and put only JavaScript in it, with no script tags. Store these files as you would images, in a common directory of your site such as /scripts. Linked scripts can be cached, so a visitor downloads them once across many pages, and they clean up your HTML documents.

<!DOCTYPE html> <html lang="en"> <head> <!-- meta content here --> <title>Linked Script</title> <script src="danger.js"></script> </head> <body> <!-- body content here --> <button id="button1" onclick="alertTest()"> Press Me </button> </body> </html> // danger.js function alertTest() { alert('Danger!'); }

Two details of the element catch people. The end tag is required, because HTML does not honor <script src="danger.js" />: the parser treats everything after it as script content, and the rest of your page disappears. And if an element has a src, any code written inside it is ignored.

Potential hazards

Linked scripts have problems under the obvious conditions of network failure or extreme slowness. Each one can also add a round trip. Unless you use them excessively, external files are still the preferred way to separate code from markup.

Linking to a script on someone else's server raises a larger question. That site now has a performance and security impact on you.

<script src="https://somesite.example/somescript.js"></script>

If that server is slow, a classic script in your head holds up your page. If it is compromised, or sold, its code runs in your page with full access to the page and its visitors' input. This has happened at scale. In 2024 the polyfill.io domain changed hands, and researchers reported that it was injecting malicious code into more than 100,000 sites that linked to it. You can guard against a changed file with Subresource Integrity: an integrity attribute holds a hash of the file you expect, along with crossorigin, and the browser refuses a file that does not match. Hosting the file yourself avoids the question.

8. Improving the linked script, one take at a time

Having JavaScript mixed in with our markup, as in that onclick on the button, can make things a bit messy and is generally bad practice. We should keep things separated. So take the attribute out and bind the handler from danger.js. This small job takes several tries, and each fixes something and exposes something else.

Take 1: bind from the file

<head> <!-- meta content here --> <title>Linked Script</title> <script src="danger.js"></script> </head> <body> <!-- body content here --> <button id="button1"> Press Me </button> </body> // danger.js function alertTest() { alert('Danger!'); } let button = document.getElementById('button1'); button.onclick = function () { alertTest(); };

Uh oh. The console reports a TypeError along the lines of "can't access property "onclick", button is null". Since the parser stops parsing the HTML to run our JavaScript at this point, we are trying to locate a button element that doesn't exist yet. This is section 4's lesson again, and the error is the kind that section 2 warned about, because the page looks fine and the button does nothing.

Why the button is null in Take 1On the left, the source of the page, with the parser stopped at the script element in the head. The body and the button below it have not been read. On the right, the DOM so far holds html, head, title and script. The button is not there, so getElementById returns null.The sourceThe DOM so far<head><title>...</title><script src="danger.js"></script></head><body><button id="button1">...htmlheadtitlescriptNo button yet, sogetElementById returns null.Parser stopped at the highlighted line. The dimmed lines are unread.
A classic script in the head runs before the parser has reached the body, so nothing in the body exists for it to find.

You can reproduce this in one panel. The script looks for the button and reports what it found, and the button comes after it.

Take 2: move it to the bottom

Someone on Stack Overflow says that to avoid this issue you should always put your scripts right before the end of your body element, which seems to fix it.

<body> <!-- body content here --> <button id="button1"> Press Me </button> <!-- lots and lots of code & resources being pulled in here --> <script src="danger.js"></script> </body>

That seems right, but it's not quite right. If the page is rather large and takes a while to load, the button could appear and be clickable before danger.js has even started to load. Backloading all your scripts can add up as your web app grows, so be mindful of the "uncanny valley", where your page has visually loaded but isn't functional. The name comes from robotics, for a face that is close to human and unsettling because of what is missing.

The uncanny valley of JavaScriptA chart over time from the initial request. What the page shows rises from zero when the HTML arrives and reaches full when the view is painted. What the page can do stays at zero until the script arrives, and reaches full once the script has been parsed and run. The region between the two lines, from the painted view until the script has run, is the uncanny valley.100%0Uncanny valleylooks ready, does nothingsolid: what the page showsdashed: what it can doInitial requestHTML arrivesView paintedScript arrivesScript parsed and run
The longer the script takes to arrive and run, the wider the valley. Every press inside it does nothing, with no message. The shape is a sketch, not a measurement.

Press the buttons in this demo as soon as it appears. The product and its buttons paint at once, and the script at the end of the body takes five seconds. The panel at the top notes each press that nothing answered. When the script arrives, the same buttons start working, and nothing about their appearance changes. If the script never arrives, they never work, and the panel says so.

The response time that people expect from a press is the subject of RAIL, and Performance covers how to set numbers for it.

Take 3: wait for the window

If danger.js waits to touch the DOM until the entire window has loaded, we could technically load it as early as we like, and it will wait.

// danger.js function alertTest() { alert('Danger!'); } window.onload = function () { let button = document.getElementById('button1'); button.onclick = function () { alertTest(); }; }; <head> <!-- meta content here --> <title>Linked Script</title> <script src="danger.js"></script> </head> <body> <!-- body content here --> <button id="button1"> Press Me </button> <!-- lots and lots of code & resources being pulled in here --> </body>

The error is gone. We are still not there. What happens if another included script does window.onload = function () { /* other stuff */ };? What if other included code has its own function alertTest() { }, or a variable of a similar name? The big challenge is that JavaScript shares a namespace with other included scripts. The possibilities are name collisions, event collisions, and misbehaving or even malicious code.

In this playground two scripts stand for two files. Each sets window.onload and each defines alertTest. Only the second of each survives, so the console shows one line, and no error appears anywhere to tell you the first was lost.

Take 4: a wrapper object and a listener

Two changes answer those collisions. Hanging our names from one wrapper object means that we occupy one global name in place of many. addEventListener adds a listener without replacing anyone else's.

// danger.js let UCSD = {}; // wrapper object UCSD.alertTest = function () { alert('Danger!'); }; window.addEventListener('load', function () { let button = document.getElementById('button1'); button.addEventListener('click', UCSD.alertTest); });

The one remaining global can still collide. If a second script also says let UCSD, that second script fails with a SyntaxError and none of it runs. Programming in the large follows the namespace problem further.

Take 5: stop waiting for images

But when does the load event actually fire? It fires when the entire page has loaded, which includes all the dependent resources, stylesheets and images. Do we really need all of those images before our interactivity? Does our script depend on them? In most cases it needs the HTML and can ignore the rest.

If we wait only until the DOM has been processed, with the DOMContentLoaded event, we can skip waiting for the entire page to load. It fires when the HTML has been completely parsed and all deferred scripts have run. It doesn't wait for images, subframes or async scripts.

Where DOMContentLoaded and load fall in a page loadA bar from left to right in five stages: network, the server's application time, network again, DOM processing, then images and other resources. Marks above it show the initial request, the request reaching the server, the server's code finishing, the first byte arriving, DOMContentLoaded at the end of DOM processing, and load at the end of the bar.Initial requestRequest reaches serverServer code finishesFirst byteDOMContentLoadedloadNetworkApplication timeNetworkDOM processingImages, other resourcesPainting starts during DOM processing. It waits for neither event.
DOMContentLoaded marks the end of parsing, and load the end of everything the page asked for. On a page with many images, the gap is large.
// danger.js let UCSD = {}; UCSD.alertTest = function () { alert('Danger!'); }; window.addEventListener('DOMContentLoaded', function () { let button = document.getElementById('button1'); button.addEventListener('click', UCSD.alertTest); });

The event is fired at the document and bubbles to window, so you will see the listener on either. Now the script waits as long as it needs to and no longer. That lets us put it in the head, where the browser finds it early. Shoving script elements into the head has its own cost, though.

<head> <!-- meta content here --> <title>Linked Script</title> <script src="https://slowload.example/large-file.js"></script> <script src="danger.js"></script> </head>

These scripts stop the parser while the browser waits for them. Nothing in the body has been read, so there is nothing to paint, and the visitor looks at a blank page until the slow server answers. Waiting for an event inside the file does nothing to help, because the wait happens before the file has even arrived.

Take 6: defer and async

Two attributes remedy this, defer and async. Both tell the browser to download the script while parsing continues. They differ in when it runs. The head lesson's timeline draws all four cases side by side.

<head> <!-- meta content here --> <title>Linked Script</title> <script defer src="https://slowload.example/large-file.js"></script> <script async src="danger.js"></script> </head>
  • defer downloads the script while parsing continues, then runs it after the document is parsed, before the DOMContentLoaded event. Deferred scripts run in the order they appear.
  • async downloads the script while parsing continues and runs it the moment it arrives, interrupting the parser, in whatever order the downloads finish.

So async all the things, and all your render-blocking woes are in the past? Well, not so fast. async runs things whenever possible, meaning that the order is unpredictable. What if script-3 depends on script-2?

<head> <!-- meta content here --> <title>Linked Script</title> <script async src="script-1.js"></script> <script async src="script-2.js"></script> <script async src="script-3.js"></script> <script async src="script-4.js"></script> <script async src="script-5.js"></script> </head>

The Load lab measures all of this. It loads the same three scripts as classic, defer, async and module, each in a new document, using delays you choose, and it logs when each one runs against parsing and DOMContentLoaded. Each trial also shows what is painted. A classic script in each trial's body holds the parser for the time you set, standing in for a long document. The route it uses belongs to the site's Worker, and the lab tells you if that route is missing.

With the delays it starts with, script-1 takes 1,500 ms, script-2 takes 200 ms, script-3 takes 800 ms and the page takes 1,000 ms to parse. These results came from running it, and your times will differ by a few milliseconds.

TrialParser reaches the bodyParser reaches the endScripts runDOMContentLoaded
classicabout 1,500 msabout 1,500 ms1, 2, 3, all at about 1,500 msabout 1,500 ms
deferat onceabout 1,000 ms1, 2, 3, all at about 1,500 msabout 1,500 ms, after the scripts
asyncat onceabout 1,000 ms2 at about 200 ms, 3 at about 800 ms, 1 at about 1,500 msabout 1,000 ms, before script-1
moduleat onceabout 1,000 ms1, 2, 3, all at about 1,500 msabout 1,500 ms, after the scripts

Read the classic row closely. The body waited 1,500 ms, not the 2,500 ms that the delays add up to, because a preload scanner looks ahead while the parser is stopped and starts the other downloads early. It can fetch ahead, but the scripts still run in order and the parser still waits. In the defer row, script-2 arrived long before it ran and waited its turn. In the async row, script-1 ran after DOMContentLoaded.

That last result is a trap for Take 5's code. Suppose danger.js is loaded with async, as in the first sample of this take, and it arrives after parsing has finished. It adds a listener for an event that has already fired, the listener never runs, and the button stays dead with no error. If a script may run late, check document.readyState before listening. A deferred script has no such problem. It runs before the event, so its listener works, and since the document is already parsed, it doesn't need the listener at all.

function setUp() { let button = document.getElementById('button1'); button.addEventListener('click', UCSD.alertTest); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setUp); } else { setUp(); }

One more, a modern take

The 2015 edition of JavaScript introduced modules, and HTML's module type of script loads them. A module has its own scope, so we no longer worry about polluting the global namespace.

<head> <!-- meta content here --> <title>Linked Script</title> <script type="module" src="danger.js"></script> </head>

With modules we can get rid of that wrapper object. A function or variable at the top of a module stays inside it, so two modules can each have an alertTest without meeting.

// danger.js function alertTest() { alert('Danger!'); } let button = document.getElementById('button1'); button.addEventListener('click', alertTest);

Modules are deferred by default, and the defer attribute has no effect on them, so the Load lab's module trial matches its defer trial. By the time this code runs, the document has been parsed and the button exists, so the listener for DOMContentLoaded is gone too. If you add async to a module, it behaves like the async trial: it runs as soon as it and its imports are ready, the button may not exist, and an event listener may be too late. For code that works with the page, leave async off.

Modules change some other things:

  • They run in strict mode automatically, and this at the top level is undefined.
  • Their names are not global, so the onclick="alertTest()" from section 7 fails with a ReferenceError, and you cannot call alertTest() from the console.
  • They need a server. Opening the HTML from a file:// URL gives CORS errors, and a module from another origin must be sent with CORS headers.
  • They still share the page. Another script can read the same DOM, replace window.onload or attach to your button.

import and export, which are the reason modules exist, are covered with a working demo in Programming in the large.

9. Which method is best

As you have probably learned by now in this course, the answer is It Depends™. You are the director of the production that is your web page. There is no one best way to film a scene in a movie, which is why each scene is filmed in its own way. Similarly, there is no one best way to load your script. You look at the tools and techniques available to you and decide which best fits the needs of what your JavaScript file requires.

The questions a director asks of a script are these. Does it need the DOM? Does anything depend on it, or does it depend on anything? Is the page useful without it, and for how long? This table turns the answers into a starting point.

The scriptA good starting pointBecause
Your application code, working with the pagetype="module"Runs after parsing, in order, with its own scope.
A classic script that works with the page, or that others depend ondeferSame timing and order, and it shares the global scope with other classic scripts.
Independent: depends on nothing and nothing depends on it, such as some analyticsasyncRuns as soon as it can, and order does not count.
Must run before the page is first painted, such as setting a saved color themeA small inline classic script in the headIt blocks, on purpose, and needs no download. Keep it tiny.
Depends on the images or other resources being finishedA listener for loadThe one case that needs the later event.

Whichever you pick, decide what the page does during the valley. Controls that work without script, such as links and forms, have no valley at all. A control that depends on script can wait to appear until it works. Those choices are the subject of How much JavaScript?

10. Multiple script tags

Developers commonly reference multiple JavaScript files separately. The value of this is questionable. Each request adds network cost, and classic files share the same namespace. HTTP/2 and later make many requests cheaper by sharing one connection, but each file is still fetched, and a chain of imports is discovered a step at a time.

<!-- Too many requests? --> <script src="lib1.js"></script> <script src="lib2.js"></script> <!-- Idea: compile to one file --> <script src="everylib.js"></script>

The mantra for this is code for yourself, prep for delivery. You keep files small and separate because that suits people, and a build step prepares what suits the network. That step was once often a task runner like Gulp. Today it is more likely a bundler such as esbuild or Rollup, often through Vite. This course does not teach those tools, and the same idea appears in Programming in the large.

11. Summary

ConceptKey points
Hello worlddocument.write writes into the parser's stream. Use it to get started and to read old code. It is deprecated, wipes a loaded page, and is ignored in deferred and async scripts.
IntermixtureAn HTML tag inside a script is a syntax error. </script> ends a script even inside a string.
Silent failureVisitors see nothing when script fails. An error stops that script at that line, and the page carries on. Keep the console open.
Four waysInline script, linked src, event handler attributes, and javascript: URLs. Prefer linked files, and read the other two only in older code.
Top to bottomClassic scripts run in order as the parser reaches them, and each can see only what has been parsed.
No scriptnoscript shows only when scripting is off, not when a script fails. A meta refresh inside it redirects visitors, and crawlers. Script masking is history.
Remote scriptsThe host has a performance and security impact on you. Use integrity, or host the file yourself.
Take 1, in the headThe button doesn't exist yet, so the lookup returns null.
Take 2, at the bottomWorks, and opens the uncanny valley: visible but not functional.
Take 3, window.onloadWaits, but another script can overwrite it. Scripts share a namespace.
Take 4, wrapper and listenerOne global name, and listeners that add up. load still waits for every image.
Take 5, DOMContentLoadedWaits only for parsing. The script in the head still blocks the parser while it downloads.
Take 6, defer and asyncBoth download during parsing. defer runs after parsing, in order, before DOMContentLoaded. async runs on arrival, in no fixed order, and may miss DOMContentLoaded.
ModulesDeferred by default, strict, with their own scope. No wrapper object and no event listener needed. Their names are not global, and they need a server.
ChoosingIt Depends™. You are the director. Start from what the script needs and what depends on it.
DeliveryCode for yourself, prep for delivery.