Skip to main content

Load JavaScript 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.


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


Anchor to Define an import map in your layoutDefine 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.

{%- 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>

Anchor to Use bare specifiers in section scriptsUse bare specifiers in section scripts

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

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

// Section-specific code
</script>
{%- 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.

Anchor to Don't load your own ,[object Object]Don't load your own es-module-shims

Shopify automatically includes the es-module-shims polyfill when the browser needs it. Loading your own copy causes duplicate polyfill execution and potential version conflicts:

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

Anchor to Before: Each section bundles its own copyBefore: 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.

{%- 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.

Anchor to After: Shared modules through the import mapAfter: Shared modules through the import map

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

Anchor to When to use import maps and other approachesWhen to use import maps and other approaches

ApproachUse when
Import mapsMultiple sections share the same modules. You want centralized dependency management without a build step.
Traditional <script> tagsYou have a single small script with no shared dependencies.
A bundler, such as Vite or webpackYou 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 applies to classic (non-module) scripts only.


  • 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.


Was this page helpful?