As a front-end developer, you will have to master three basic skills. These are HTML, CSS, and Javascript. The HTML will be used to structure the DOM tree of a page, the CSS is used for styling and the Javascript language for interactivity with a web page. In this article, we are going to see all the possible ways how to link javascript to an HTML page.
Inline Javascript Code
In general, you can use this method when you want to add some handling logic on an event that will take place for a DOM element. Let’s see some examples
//index.html
<button onclick="alert('Hello!')">Click me!</button>
In the code above I use the onclick event handler to call the
function alert when the user clicks the button Click me!
The script tag
In order for you to embed a client-side script, you will have to use the script tag. Usually, it will point to an external script through the src attribute.
//index.html
<button onclick="hello()">Click me!</button>
<script src="script.js"></script>
In the code above I use the function hello
in the onclick
event handler for the button Click me!.
Also, I link the javascript code with the HTML code with the directive <script src="script.js"></script>
As an alternative and usually, for a smaller amount of code, you can use the script tag to embed code between the opening and closing tags <script> …code </script>
Let’s see an example
//index.html
<script>
function hello() {
alert("Hello from embedded javascript code");
}
</script>
In the code above, I define the function hello
inside the script
tag and use it as an onclick
event handler for the button Click me!
Async vs Defer
If your code in Javascript performs any kind of heavy calculation it will block the parsing of the HTML page until the javascript code completes its execution.
Let’s see an example on this
//script.js
const LOOPS = 200;
const startTime = new Date();
for (let i = 0; i < LOOPS; i++) {
console.log("Loop:", i);
}
const endTime = new Date();
const diff = endTime.getMilliseconds() - startTime.getMilliseconds();
console.log(`diff: ${diff} ms`);
//debugger;
In the code above I use a for..loop
to print a message to the console for 200 loops. I also calculate the time that this code gets to execute in milliseconds which is about 2 ms for 200 loops.
In the HTML code, I also render two paragraphs one before the script execution and one after the script is complete. You can see the script blocking effect since the two paragraphs are blinking. The second paragraph becomes visible to the user at about 2 ms. This time period may be slightly different depending on your CPU speed and memory.
Additionally, you can uncomment the debugger
statement to see the exact timing of when the script execution completes and that the second paragraph is not rendered yet.
You can experiment with this example and increase the loops
constant but be careful because it may break your browser.
We can improve this issue using the attributes async
and defer
in the script tag.
Async
The async
attribute is a boolean attribute of the src tag and works only on external scripts.
If it is set the script will download in parallel to parsing the page. The execution of the script will take place as soon as the script is completely downloaded.
At that point, the parsing of the page is interrupted and the execution of the script is taking place before the parsing of the rest of the page continues
//index.html
<script src="script.js" async></script>
Similar to the previous example, if you uncomment the debugger
statement you can see that the HTML code is rendered independently from the execution of the script code. Also, no blinking happens in this case.
Defer
The defer
attribute is similar to the async
attribute but also has some differences.
Like async
, the script is downloaded in parallel to the parsing of the page. One difference is that the execution of the script will take place after the page is completely parsed and not when the script has completed the downloading phase. This difference becomes more obvious to the user when the HTML is quite long.
Let’s see an example of a page that contains a really big set of DOM elements.
In this Github repo, I prepared an example where you can clone and play with it.
You can see that the code on the index.html
page follows the structure below.
<head>
...
<script src="defer.js" defer></script>
<script src="async.js" async></script>
....
</head>
<body>
...
<!-- 2800 divs with dummy content -->
...
</body>
index.html
page in the Chrome browser and open the console in the dev tools you can see that the async script did not grab all of the available div elements that exist on the page whereas the defer grabbed all of them.Async: 11751
async.js:3 Executed async script
defer.js:2 Defer: 28000
defer.js:3 Executed defer script
One more difference is that defer
respects the sequence of scripts whereas async
doesn’t.
<script src="script1.js" async></script>
<script src="script2.js" async></script>
<script src="script3.js" async></script>
In the code above I use three async scripts. You can see that the sequence of the log messages for the three async scripts is unpredictable.
Executed script 2
Executed script 1
Executed script 3
Let’s see the same example using the defer
attribute.
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
<script src="script3.js" defer></script>
The logs below show the difference.
Executed script 1
Executed script 2
Executed script 3
How to call an external javascript file
You can call an external javascript that belongs to a third-party domain file directly in the src. A common use case of this approach is when someone wants to load a library from a CDN.
//index.html
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" defer></script>
<script src="script.js" defer></script>
In the code above I use the CDN URL for the Lodash library. You can find all the available CDN URLs here. Also, I use the defer
attribute in both script tags to respect the loading order and ensure that the Lodash library is properly loaded when the script.js
is executing.
Positioning javascript code inside HTML document
If none of the async or defer attributes are present then the position of
the script tag matters in terms of the execution time and is related to
the parsing of the page.
The script tag in the head section
Everything that is included in the head section of a page is preloaded. So if you include a script tag in the head the Javascript code will run before any parsing of the page takes place.
As a result of this and depending on the code logic you will have to ensure that every DOM element that the code uses preexists. This is a common scenario that may trigger issues and throw errors. A possible solution is to use defer
attribute or the DOMContentLoaded
content listener as is shown in the example below.
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="script1.js"></script>
<script src="script2.js"></script>
<script src="script3.js" defer></script>
<script src="script4.js" async></script>
</head>
In the logs, you can see that for scripts without defer
or the DOMContentLoaded
event handling the output results are not predictable.
content null
content from defer <div id="content">Content</div>
content from dom content listener <div id="content">Content</div>
content from async <div id="content">Content</div>
Regarding the async
attribute that really depends on the size of your dom content and the loading time that is required to be fully parsed.
The script tag in the body section
When you place javascript code inside a body it will block the parsing of the page. Next, the code will execute and the parsing of the page will continue when the execution of the code completes. That means that if your code accesses a DOM element that does not exist then an error will be thrown.
As a best practice, it is advised to place all of your scripts at the bottom of the page to avoid similar errors.
Another way to avoid these errors is to use event listeners that the DOM content is fully loaded. Then you could apply your logic with no errors as I already it is shown in the previous example.
Modules
During the early days of Javascript didn’t support a language-level module syntax. Initially, the scripts were not so complex so that wasn’t really a problem.
Because of the development of more complex scenarios in terms of user interactivity and with the creation of the NodeJS the need for more complex code was increasing too.
So the community created several ways to structure code into modules and special libraries to load modules that would facilitate the reusability of a code snippet.
Some examples of those ways are
- CommonJS (links)
- AMD
- UMD
- ES6 modules
The ES6 modules are the latest standard that appeared in 2015 and are now supported by all major browsers and in NodeJS. The rest of the ways that I mentioned are a bit of history and maybe you can find them in older projects.
Import/export
A module is just a file that contains Javascript code.
Modules can use the directives export
and import
to share functionality between each other.
So for example, if you define two functions in separate files (modules) you can load the second function from the first file. By doing that you can reuse your code among different modules.
Let’s see an example
<script src="script.js" type="module"></script>
//module.js file
export function hello() {
console.log("Hello from module");
}
In the code above I use the attribute type
with the value module
. In the script with the name script.js
I import the function hello
from the script module.js
and then execute it. You can see the output logs below.
Hello from module
Characteristics of modules
In this section, we will see the basic differences between modules and regular scripts. These are core characteristics that apply both for browser and server-side JavaScript code.
Strict mode
Modules are always executed in strict mode. Let’s see an example
<script src="script.js" type="module"></script>
//module.js
export function test() {
try {
// not using a variable declaration will throw error
// because modules always use strict mode of javascript
a = 5;
} catch (e) {
console.log("Error:", e);
}
}
Below you can see the logs and the error that is thrown
Uncaught ReferenceError: a is not defined
Scope
Each module defines its own top-level scope. That means that top-level
variables and functions are not defined in other scripts.
Let’s see an example
//index.html
<script src="./module1.js" type="module"></script>
<script src="./module2.js" type="module"></script>
//module1.js
const stats = {
views: 1000,
clicks: 10
};
//module2.js
try {
//stats belongs to module1 scope and should be imported
console.log("statistics", stats);
} catch (e) {
console.log("Error:", e);
}
ReferenceError: stats is not defined
If you want to share variables from another module you will have to export and import those variables.
//index.html
<script src="module1.js" type="module"></script>
//module1.js
import { stats } from "./module2.js";
console.log("stats", stats);
//module2.js
export const stats = {
views: 1000,
clicks: 10
};
You can see the output logs below
stats {views: 1000, clicks: 10}
Another way to share variables between scripts is using the window object in the case of a browser environment and using the global object in the case of a NodeJS environment. Despite the fact that this will work it is considered to be a bad practice.
Module evaluation
A module will be executed only once upon its first import even if it will be imported multiple times. After its execution, all of its exported functions and variables will be available to the rest of the scripts.
//index.html
<script src="module1.js" type="module"></script>
<script src="module2.js" type="module"></script>
//module1.js
import "./logs.js";
//module2.js
import "./logs.js";
//module3.js
export const stats = {
views: 1000,
clicks: 10
};
//logs.js
import { stats } from "./module3.js";
console.log("stats: ", stats);
You can see in the output logs below that the message is printed only once, despite the fact that it is imported both in scripts module1.js
and module2.js
stats: {views: 1000, clicks: 10}
import.meta
You can retrieve information about the current module using the import.meta
property.
Its content depends on the environment.
//index.html
<script src="module.js" type="module"></script>
//module.js
console.log("url: ", import.meta.url);
url: https://yi26l5.csb.app/module.js
The ‘this’ keyword
The this
keyword in the top-level scope of a module is undefined while compared to a non-module script is a global object.
Let’s see an example
//index.html
<script src="./module.js" type="module"></script>
//module.js
try {
console.log("this", this);
} catch (e) {
console.log("Error:", e);
}
You can see the output logs below
this undefined
Modules are deferred
Modules are always deferred in a browser environment. That means that the execution of the script will take place after the parsing of the page is complete and not when the script has completed the downloading phase.
Async with modules
As I mentioned already for regular scripts the async attribute works only on external scripts whereas module scripts work on inline scripts too. That’s good for functionality that doesn’t depend on other parts of code, like ads or event listeners on DOM elements.
Let’s see an example
<script async type="module">
import { stats } from "./module.js";
console.log("stats:", stats);
</script>
You can see the output logs below
stats: {views: 100, clicks: 10}
External scripts
There are two major differences for scripts that have type=”module”.
- External scripts with the same src run only once
- When you fetch scripts from a different origin you will have to be aware of CORS issues. In order to resolve this, you will have to provide an “Access-Control-Allow-Origin” in the server that will allow the fetch.
//index.html
<script src="./module.js" type="module"></script>
<script src="./module.js" type="module"></script>
//module.js
console.log("Hello");
You can see the logs below
Hello
External scripts that are fetched from another origin (e.g. another site) require CORS headers, as described in the chapter Fetch: Cross-Origin Requests. In other words, if a module script is fetched from another origin, the remote server must supply a header Access-Control-Allow-Origin allowing the fetch.
Bare modules
The URL that you use with the import directive must be either relative or absolute. The modules that do not use any path are called bare
modules and are not allowed to be used with import in browser environments.
In several environments and bundlers (like NodeJS or Webpack) bare modules are allowed but they use their own path resolving algorithm for importing modules.
For instance, this import is invalid:
//index.html
<script src="module.js" type="module"></script>
import { hello } from "hello";
Output
Uncaught TypeError: Failed to resolve module specifier "hello". Relative references must start with either "/", "./", or "../".
“nomodule” attribute
Older versions of browsers do not support the module value for the type attribute. In such a case the module scripts are ignored and you can provide a fallback with the use of nomodule
attribute.
// index.html
<script src="module.js" type="module"></script>
<script nomodule>
console.log("Type=mdule and nomodule will work in modernbrowsers");
console.log(
"Older browsers wiill ignore unknown type=module and will execute only scripts with type nomodule"
);
</script>
Build tools
During the development of a web application, it is not so common to use modules directly in a browser. Usually, the developers are using bundlers like Webpack or Parcel in order to bundle all the modules together to a single build script and deploy them to the production server.
Below is a shortlist of the advantages of using bundlers are
- minify the code
- remove any debugger and console directives
- remove any unreachable code
- allow bare modules
- apply polyfills to provide compatibility with older browsers
versions - tree-shaking
Dynamic modules loading
You can use dynamic modules loading when you want to increase the loading of your code or when you want to load code under certain conditions. This can be done using the function-like expression import(). This expression will return a Promise and you can call it from any place in the code
Let’s see an example
//index.html
<script src="module1.js" type="module"></script>
//module1.js
const greet = async () => {
const { name } = await import("./module2.js");
return `Hello ${name}`;
};
greet().then((greeting) => {
console.log(greeting);
});
//module2.js
export const name = "John";
Output logs
Hello John
The dynamic modules loading can be useful in the following indicative scenarios
- code-splitting
- lazy loading component
- adding error boundaries components in react
- decrease the size of your final bundle script
Summary
In this article, we saw every possible way to link Javascript in HTML and other environments like NodeJS.
More specifically you can link Javascript code using the script tag
- in any place of an HTML document
- with the attributes async/defer
- with the type “module”
- and external scripts with the src attribute
- to load code from any local files with the src attribute
- with the import/export directives and with ES6 modules
Happy coding!