Tutorial

How to Dynamically Import JavaScript with Import Maps

DevelopmentJavaScriptCSSHTML

The author selected Creative Commons to receive a donation as part of the Write for DOnations program.

Introduction

External libraries can add complexity to a new JavaScript project. To be able to install and use external code libraries, you’ll need a build tool that can parse the code and bundle the libraries that you import into a final format. After the build is set up, you can add and integrate new code with ease, but there are still some problems.

For example, you may need a library in just one part of your application, a part of the application most users may never need, like an admin page. But by default most build systems will bundle all the code into one large file. The user will load the code regardless of whether they ever need to execute it. The build systems are flexible enough that they can be configured to load code as needed, but the process takes some work.

Build tools are an important part of the development experience, but a spec called import maps will allow you to both import external code into your project without a build tool and it will only load the code when it is needed at runtime. Import maps won’t completely replace build tools that perform many other valuable actions like building style sheets and handling images, but they will allow you to bootstrap new JavaScript applications quickly and easily using only the native browser functionality.

In this tutorial, you’ll use import maps and JavaScript modules to import code without build tools. You’ll create a basic application that will display a message and you’ll create an import map that will tell your browser where to locate external code. Next, you’ll integrate the imported code into your JavaScript and will use the third-party code without any need to download the code locally or run it through a build step. Finally, you’ll learn about current tools that implement many aspects of import maps and work on all modern browsers.

Prerequisites

Step 1 — Creating an HTML Page and Inserting JavaScript

In this step, you will create an HTML page, use JavaScript for dynamic activity, and start a local development server to track your changes.

To start, in a new directory, create a blank HTML document.

Open a file called index.html in a text editor:

  • nano index.html

Inside of the file, add a short, blank HTML page:

index.html
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Hello World</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body>
  </body>
</html>

This document has a few standard <meta> tags and an empty <body> element.

Next add a <script> tag. The src attribute for the script tag will be a new JavaScript file you are about to create called hello.js:

index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script defer src="./hello.js"></script>
</head>
<body>
</body>

Notice that you are adding a defer attribute to the <script> tag. This will delay execution of the script tag until after the document is parsed. If you don’t defer, you may receive an error that says body is not found when you try to add to the element.

Next, create a JavaScript file named hello.js in the same directory asindex.html:

  • nano hello.js

Inside of the file, write some JavaScript to create a new text element with the text "Hello, World":

hello.js
const el = document.createElement('h1');
const words = "Hello, World!"
const text = document.createTextNode(words);
el.appendChild(text);

document.body.appendChild(el);

Now that you have your script, you can open the index.html file in a browser. In the same directory as your index.html file, run npx serve. This will run the serve package locally without downloading into your node_modules. The serve package runs a simple webserver that will serve your code locally.

npx serve

The command will ask you if you want to install a package. Type y to agree:

Need to install the following packages:
  serve
Ok to proceed? (y) y

When you run the command you will see some output like this:

npx: installed 88 in 15.187s

   ┌────────────────────────────────────────┐
   │                                        │
   │   Serving!                             │
   │                                        │
   │   Local:  http://localhost:5000        │
   │                                        │
   │   Copied local address to clipboard!   │
   │                                        │
   └────────────────────────────────────────┘

When you open your web browser to http://localhost:5000, you’ll see your code. You can either leave the server running in a separate tab or window or close it with CTRL+C after previewing your code.

Hello, World in a browser

Now you are displaying a basic page in your browser, but you are not yet able to take advantage of third-party code and JavaScript packages. In the next step, you’ll dynamically import code and import the functions into your script without a build tool.

Step 2 — Writing a Hello World Script Using ES6 Modules

In this step, you’ll write code that uses external packages. You’ll modify your code to import JavaScript code using ES imports. Finally, you’ll load the code in your browser using the module type so the browser will know to dynamically load the code.

To begin, open up hello.js:

  • nano hello.js

You are going to import some code from lodash to dynamically change your text.

Inside of the file, change the text from Hello World to all lower case: hello world. Then at the top of the file, import the startCase function from lodash using the standard ES6 import syntax:

hello.js
import startCase from '@lodash/startCase';

const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(words);
el.appendChild(text);

document.body.appendChild(el);

Finally, call startCase with the words variable as an argument inside of document.createTextNode:

hello.js
import startCase from '@lodash/startCase';

const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(startCase(words));
el.appendChild(text);

document.body.appendChild(el);

If you closed your webserver, open a new terminal window or tab and run npx serve to restart the server. Then navigate to http://localhost:5000 in a web browser to view the changes.

When you preview the code in a browser, open the developer console. When you do, you’ll see an error:

Output
Uncaught SyntaxError: Cannot use import statement outside a module

Module Error

Since the code is using import statements, you’ll need to modify the <script> tag inside of index.html to handle JavaScript that is now split between multiple files. One file is the original code you wrote. The other file is the code imported from lodash. JavaScript code that imports other code are called modules.

Close hello.js and open index.html:

  • nano index.html

To run the script as a module, change the value of the type attribute on the <script> tag to module:

hello.js
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>

Notice, you also removed the defer attribute. JavaScript modules will not execute until the page is parsed.

Open your browser and you’ll still see an error`:

Output
Uncaught TypeError: Failed to resolve module specifier "@lodash/startCase". Relative references must start with either "/", "./", or "../"

Unknown specifier

Now the problem is not the import statement. The problem is the browser doesn’t know what the import statement means. It’s expecting to find code on the webserver, so it looks for a file relative to the current file. To solve that problem, you’ll need a new tool called import maps.

In this step, you learned how to modify your JavaScript code to load external libraries using ES imports. You also modified the HTML script tag to handle JavaScript modules.

In the next step, you’ll tell the browser how to find code using import maps.

Step 3 — Loading External Code with Import Maps

In this step, you’ll learn how to create import maps to tell your browser where to find external code. You’ll also learn how to import module code and see how code is lazy-loaded in a browser.

An import map is a JavaScript object where the key is the name of the import (@lodash/startCase) and the value is the location of the code.

Inside of index.html add a new script tag with a type of importmap. Inside of the script tag, create a JavaScript object with a key of imports. The value will be another object:

hello.js
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="importmap">
    {
      "imports": {
      }
    }
  </script>
  <script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>

Be sure that you do not add any trailing commas in the object. The browser will not know how to handle them.

Now that you have your basic object, you can add the location of your code. The structure of an import map will be like this:

{
    "imports": {
        "nameOfImport": "locationOfCode",
        "nameOfSecondImport": "secondLocation"
    }
}

You already know the name of your import @lodash/startCase, but now you need to find where a location to point the import map to.

A great resource is unpkg. Unpkg is a content delivery network (CDN) for any package in npm. If you can npm install a package, you should be able to load it via unpkg. Unpkg also includes a browsing option that can help you find the specific file you need.

To find the startCase code, open https://unpkg.com/browse/lodash-es@4.17.21/ in a browser. Notice the word browse in the URL. This gives you a graphical way to look through the directory, but you should not add the path to your import map since it serves up an HTML page and not the raw JavaScript file.

Also, note that you are browsing lodash-es and not lodash. This is the lodash library exported as ES modules, which is what you will need in this case.

Browse the code and you’ll notice there is a file called startCase.js. This code imports other functions and uses them to convert the first letter of each word to upper case:

import createCompounder from './_createCompounder.js';
import upperFirst from './upperFirst.js';

/**
 * Converts `string` to
 * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).
 *
 * @static
 * @memberOf _
 * @since 3.1.0
 * @category String
 * @param {string} [string=''] The string to convert.
 * @returns {string} Returns the start cased string.
 * @example
 *
 * _.startCase('--foo-bar--');
 * // => 'Foo Bar'
 *
 * _.startCase('fooBar');
 * // => 'Foo Bar'
 *
 * _.startCase('__FOO_BAR__');
 * // => 'FOO BAR'
 */
var startCase = createCompounder(function(result, word, index) {
  return result + (index ? ' ' : '') + upperFirst(word);
});

export default startCase;

The browser will follow the import statements and import every file necessary.

Now that you have a location for your import map, update the file import map with the new URL. Inside of index.html, add @lodash/startCase along with the URL. Once again, be sure to remove browse:

index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="importmap">
    {
      "imports": {
        "@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js"
      }
    }
  </script>
  <script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>

Save the file. Refresh your browser and you will see Hello World.

Working

NOTE: Import maps are not yet widely supported. Open the code in the latest version of Edge or Chrome or check out the latest supported browsers.

Your browser shows “Hello World”, but now something much more interesting is happening. Open the browser console in your browser, select Inspect Elements, and switch to the Network tab.

After opening the Network tab, refresh the page and you’ll see that the browser is loading the code dynamically. Whenever it finds a new import statement, it imports the relevant code:

Network Tab

More importantly, notice that all of the code is loaded lazily. That means that the browser does not import any code until it is specifically needed. For example, even though startCase is in the import map and the import map is defined before the script for hello.js, it is not loaded until after hello.js loads and imports the code.

If you were to add other entries in your import map, the browser would not load them at all since they are never imported into code. The import map is a map of locations, and doesn’t import any code itself.

One major problem is that import maps are not yet fully supported by all browsers. And even when they are supported, some users may not use a supported browser. Fortunately, there are different projects that use the import map syntax while adding full browser support.

In this step you created an import map. You also learned how to import module code and how the code will be lazy loaded in the browser. In the next step, you’ll import code using SystemJS.

Step 4 — Building Cross-Browser Support with SystemJS

In this step, you’ll use import maps across all browsers using SystemJS. You’ll export code as a SystemJS build and how to set the import map type to use SystemJS format. By the end of this step, you’ll be able to take advantage of import maps in any browser and will have a foundation for building more complex applications and microfrontends.

Import maps will remove many of the complicated build steps from an application, but they are not yet widely supported. Further, not all libraries are built as ES modules so there is some limitation to how you can use them in your code.

Fortunately, there is a project called SystemJS that can use create import maps for cross-browser support and use a variety of package builds.

The lodash library is convenient because it is compiled in an ES format, but that’s not the case for most libraries. Many libraries are exported in other formats. One of the most common is the Universal Module Definition or UMD. This format works in both browsers and node modules.

A major difference is that unlike the ES imports, a UMD build typically combines all of the code into a single file. The file will be a lot larger and you’ll end up with more code then you’ll probably execute.

To update your project to use SystemJS and a UMD build of lodash, first open hello.js:

  • nano hello.js

Change the import statement to import the startCase function directly from lodash.

hello.js
import { startCase } from 'lodash';

const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(startCase(words));
el.appendChild(text);

document.body.appendChild(el);

Save and close the file.

Next, to build the file as a SystemJS build, you will need a simple build step. You can use another build tool such as webpack, but in this example you’ll use rollup.

First, initialize the project to create a package.json file. Add the -y flag to accept all of the defaults:

npm init -y

After the command runs you’ll see a success output:

{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "homepage": ""
}

Note: Your output may be slightly different depending on the version of npm you are using.

Next, install rollup as a devDepenceny:

npm install --save-dev rollup

After a moment, you will see a success message:

+ rollup@2.56.2
added 1 package from 1 contributor and audited 2 packages in 6.85s
found 0 vulnerabilities

Next, create a simple build configuration. Open a new file called rollup.config.js:

  • nano rollup.config.js

Then add a configuration that will output the code in SystemJS format:

rollup.config.js
export default {
  external: ["lodash"],
  input: ["hello.js"],
  output: [
    {
      dir: "public",
      format: "system",
      sourcemap: true
    }
  ]
};

The external key tells rollup not to include any of the lodash code in the final build. SystemJS will load that code dynamically when it is imported.

The input is the location of the root file. The output tells rollup where to put the final code and the format it should use which in this case is system.

Save and close the file.

Now that you have a build step, you’ll need to add a task to run it. Open package.json:

  • nano package.json

In the scripts object, add a script called build that will run rollup -c. Change the main key to hello.js:

package.json
{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "hello.js",
  "devDependencies": {
    "rollup": "^2.56.2"
  },
  "scripts": {
    "build": "rollup -c"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "homepage": ""
}

Save and close the file, and run the build command:

npm run build

The command will run briefly, then you will see a success message:

> rollup -c


hello.js → public...
created public in 21ms

You will also see a new directory called public that will contain the built file. If you open public/hello.js you’ll see your project compiled in a system format.

  • nano public/hello.js

The file will look like this. It’s similar to hello.js with a surrounding System.register method. In addtion, lodash is in an array. This will tell SystemJS to load the external library during run time. One of the maintainers created a video that further explains the module format.

public/hello.js
System.register(['lodash'], function () {
    'use strict';
    var startCase;
    return {
        setters: [function (module) {
            startCase = module.startCase;
        }],
        execute: function () {

            const el = document.createElement('h1');
            const words = "hello, world";
            const text = document.createTextNode(startCase(words));
            el.appendChild(text);

            document.body.appendChild(el);

        }
    };
});
//# sourceMappingURL=hello.js.map

Save and close the file.

The final step is to update your index.html to handle the new file:

Open index.html

  • nano index.html

First, you’ll need to import the SystemJS code. Use a regular <script> tag with the src attribute pointing to a CDN distribution.

Put the <script> tag right below the import map:

index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="importmap">
    {
      "imports": {
        "@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
  <script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>

Next, you’ll need to update your import map. The format is similar to what you completed in Step 3, but there are three changes:

  • Update the import map type.
  • Update the reference to lodash.
  • Add a reference to the hello.js script.

First, update the type. Since this is not the native browser version of an import map, but a systemjs version, change the type to systemjs-importmap:

index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Hello World</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="systemjs-importmap">
    {
      "imports": {
        "@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
  <script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>

Next, update the references. Change @lodash/startCase to lodash. You’ll be importing the full library. Then change the location to the UMD build at unpkg.

Then add a new entry for hello and point that to the compiled version in the public directory:

index.html
...
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script type="systemjs-importmap">
    {
      "imports": {
        "hello": "./public/hello.js",
        "lodash": "https://unpkg.com/lodash@4.17.21/lodash.js"
      }
    }
  </script>
...

Now that you are importing systemJS and have updated the import maps, all that’s left is to load the module.

Change the type attribute on the script tag for the module to systemjs-module. Then change the src to import:hello. This will tell systemjs to load the hello script and execute:

hello.js
...
  <script type="systemjs-importmap">
    {
      "imports": {
        "hello": "./public/hello.js",
        "lodash": "https://unpkg.com/lodash@4.17.21/lodash.js"
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
  <script type="systemjs-module" src="import:hello"></script>
</head>
...

Save and close the file.

When you do, the browser will refresh and you’ll see Hello World.

Unlike native import maps, this will work in any browser. Here’s the result in FireFox:

Hello in firefox

If you look at the Network tab. You’ll see that as with import maps, the code is lazy loaded as needed:

Firefox network tab

In this step, you used import maps across browsers with SystemJS. You changed your script to use the UMD build of lodash, created a rollup build to output the code in system format, and changed the import map and module types to work with SystemJS

Conclusion

In this tutorial you used import maps to dynamically load JavaScript code. You rendered an application that dynamically loaded an external library without any build step. Then, you created a build process to generate your code in SystemJS format so that you can use import maps across all browsers.

Import maps give you opportunities to start breaking large projects into smaller independent pieces called microfrontends. You also do not need to limit yourself to statically defined maps as you learned in this tutorial; you can also create dynamic import maps that can load from other scripts. You also can use a single import map for multiple projects by using scopes to define different versions of a dependency for different scripts that import them.

There are new features in progress and you can follow them on the official spec.

Creative Commons License