---
title: Load JavaScript modules with import maps instead of bundlers
description: >-
  Import maps let theme sections share JavaScript modules without bundling.
  Define a single import map in your layout to deduplicate dependencies across
  sections and simplify module management.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-import-maps-for-modules
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-import-maps-for-modules.md
api_name: liquid
---

# Load Java​Script modules with import maps instead of bundlers

Import maps let theme sections share JavaScript modules without bundling. Define a single import map in your layout to deduplicate dependencies across sections and simplify module management. Shopify automatically includes the `es-module-shims` polyfill for older browsers, so don't load your own copy.

***

## Why

Without import maps, each theme section that uses JavaScript modules has to reference dependencies by full URL or relative path. If multiple sections depend on the same utility module, then each one either bundles its own copy or relies on fragile relative paths. This leads to duplicated code across sections, which increases total JavaScript weight and affects [LCP](https://web.dev/lcp/) by delaying the main thread.

Import maps solve this by providing a centralized mapping from short names (bare module specifiers) to file URLs. Every section can import from `cart-api` or `utils`, and the browser resolves those to the correct asset URLs defined in the map. The module is fetched once, cached once, and shared across all sections.

This also matters for dependency management. When you update a shared module, you change the URL in one place (the import map) rather than updating every section that references it.

***

## How

### Define an import map in your layout

Place a single `<script type="importmap">` block in your theme layout. Use the `asset_url` filter so Shopify appends cache-busting version parameters.

```liquid
{%- comment -%} layout/theme.liquid {%- endcomment -%}
<script type="importmap">
{
  "imports": {
    "cart-api": "{{ 'cart-api.js' | asset_url }}",
    "utils": "{{ 'utils.js' | asset_url }}",
    "product-form": "{{ 'product-form.js' | asset_url }}"
  }
}
</script>
```

### Use bare specifiers in section scripts

After you define the import map, any `<script type="module">` block in any section can import by name.

```html
{%- comment -%} sections/featured-collection.liquid {%- endcomment -%}
<script type="module">
  import { addToCart } from 'cart-api';
  import { formatMoney } from 'utils';


  // Section-specific code
</script>
```

```html
{%- comment -%} sections/product-page.liquid {%- endcomment -%}
<script type="module">
  import { addToCart, getCart } from 'cart-api';
  import { formatMoney } from 'utils';
  import { initForm } from 'product-form';


  // Section-specific code
</script>
```

Both sections share the same `cart-api` and `utils` modules. The browser fetches each module once.

### Don't load your own `es-module-shims`

Shopify automatically includes the [`es-module-shims`](https://github.com/guybedford/es-module-shims) polyfill when the browser needs it. Loading your own copy causes duplicate polyfill execution and potential version conflicts:

```html
<!-- Don't do this. Shopify handles it automatically. -->
<script
  async
  src="https://unpkg.com/es-module-shims@1.5.4/dist/es-module-shims.js"
></script>
```

***

## Examples

### Before: Each section bundles its own copy

Without import maps, two sections that both need cart functionality either duplicate the code or use script tags with global variables.

```html
{%- comment -%} sections/featured-collection.liquid {%- endcomment -%}
<script type="module" src="{{ 'featured-collection.js' | asset_url }}"></script>
{%- comment -%} featured-collection.js includes its own copy of cart logic {%- endcomment -%}


{%- comment -%} sections/product-page.liquid {%- endcomment -%}
<script type="module" src="{{ 'product-page.js' | asset_url }}"></script>
{%- comment -%} product-page.js also includes its own copy of cart logic {%- endcomment -%}
```

The cart logic is downloaded, parsed, and compiled twice.

### After: Shared modules through the import map

```liquid
{%- comment -%} layout/theme.liquid {%- endcomment -%}
<script type="importmap">
{
  "imports": {
    "cart-api": "{{ 'cart-api.js' | asset_url }}",
    "utils": "{{ 'utils.js' | asset_url }}"
  }
}
</script>
```

```html
{%- comment -%} sections/featured-collection.liquid {%- endcomment -%}
<script type="module">
  import { addToCart } from 'cart-api';
  // featured-collection-specific code
</script>


{%- comment -%} sections/product-page.liquid {%- endcomment -%}
<script type="module">
  import { addToCart, getCart } from 'cart-api';
  // product-page-specific code
</script>
```

The `cart-api` module is fetched and parsed once. Both sections share it.

### When to use import maps and other approaches

| Approach | Use when |
| - | - |
| Import maps | Multiple sections share the same modules. You want centralized dependency management without a build step. |
| Traditional `<script>` tags | You have a single small script with no shared dependencies. |
| A bundler, such as Vite or webpack | You need tree-shaking, TypeScript compilation, or complex dependency graphs. The bundler output can still use import maps for shared chunks. |

**Info:**

Because ES modules have their own scope, you don't need to wrap module scripts in IIFEs to prevent variable collisions. The IIFE pattern described in [Defer non-critical scripts](https://shopify.dev/docs/storefronts/themes/best-practices/performance/defer-scripts) applies to classic (non-module) scripts only.

***

## Testing

* Open the **Network** panel in Chrome DevTools and filter by **JS**. Verify that shared modules, such as `cart-api.js`, appear once in the waterfall, not once per section.
* Check the **Console** panel for module resolution errors. A missing entry in the import map causes a `TypeError` when the browser tries to resolve a bare specifier.
* View the page source to confirm that Shopify includes the `es-module-shims` polyfill automatically. You shouldn't see a second copy loaded by your theme.
* Test in a browser without native import map support (Safari 16.3 or earlier) to verify that the polyfill resolves modules correctly.
* Use the **Coverage** tab to check whether shared modules contain unused exports that you can split into smaller files.

***

## References

* [The Shopify platform: Polyfills](https://shopify.dev/docs/storefronts/themes/best-practices/performance/platform#polyfills)
* [`asset_url`](https://shopify.dev/docs/api/liquid/filters/asset_url) filter
* [MDN: Import maps](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/script/type/importmap)
* [es-module-shims on GitHub](https://github.com/guybedford/es-module-shims)
* [MDN: JavaScript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
* [Defer non-critical scripts](https://shopify.dev/docs/storefronts/themes/best-practices/performance/defer-scripts)
* [Load JavaScript on user interaction](https://shopify.dev/docs/storefronts/themes/best-practices/performance/load-javascript-on-user-interaction)

***
