If You Read One Article About How To Link Javascript in HTML Read this One

If You Read One Article About How To Link Javascript in HTML Read this One


  • Share on Pinterest

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!

Codesandbox

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>

Codesandbox

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>

Codesandbox

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;

Codesandbox

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>

Codesandbox

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>
Additionally, there are two scripts with async and defer attributes that will grab all the elements with the class content.
If you load the 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.
The reason is the different behavior that I already described.
You can see the logs below
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

Codesandbox

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

Codesandbox

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.

Codesandbox

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.

Codesandbox

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

Codesandbox

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
Codesandbox
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);
}

Codesandbox

Below you can see the logs and the error that is thrown
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}
Codesandbox
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);
You can see the output logs below
url: https://yi26l5.csb.app/module.js
Codesandbox
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

Codesandbox

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}

Codesandbox

External scripts

There are two major differences for scripts that have type=”module”.

  1. External scripts with the same src run only once
  2. 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

Codesandbox

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 "../".

Codesandbox

“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>

Codesandbox

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

Codesandbox

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!

LET’S KEEP IN TOUCH!

We’d love to keep you updated with our latest news and offers 😎

We don’t spam! Read our privacy policy for more info.