← Back to all posts

How to Set Up Dynamic Imports in Laravel Mix and TypeScript

Updated: July 17, 2026
How to Set Up Dynamic Imports in Laravel Mix and TypeScript

Maintenance note: Laravel now uses Vite for new applications and describes Laravel Mix as a legacy package that is no longer actively maintained. This guide is for existing Laravel Mix 6 and webpack 5 projects. For a new Laravel application, start with the current Laravel Vite documentation.

Dynamic import() lets webpack create a separate chunk for code that is not needed on every page. In a component- or block-based site, that means the entry bundle can discover which blocks exist in the document and load only their JavaScript modules.

This guide covers the parts that are easy to misconfigure in a Laravel Mix 6 and TypeScript project:

  1. Preserve dynamic imports for webpack in tsconfig.json.
  2. Make the filesystem output path and browser-facing chunk URL agree.
  3. Use a constrained module loader so webpack can determine the possible chunks.

Preserve import() for webpack

Laravel Mix 6 uses webpack 5 and a Babel version that can parse dynamic imports. You do not normally need @babel/plugin-syntax-dynamic-import in that stack; Babel has included the syntax parser since @babel/core 7.8.

TypeScript must leave the import expression for webpack to process. For a Mix 6-era project, set the module target to esnext:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es2018",
    "strict": true
  }
}

If TypeScript converts modules to CommonJS before webpack sees them, webpack cannot preserve import() as a code-splitting point.

Configure emitted chunks and their public URL

Two similarly named settings solve different problems:

  • mix.setPublicPath() sets the filesystem directory where Mix writes compiled assets and mix-manifest.json.
  • webpack’s output.publicPath sets the URL prefix the browser uses when it requests an asynchronous chunk.

Those locations must describe the same deployed asset directory from two perspectives.

const path = require("node:path");
const mix = require("laravel-mix");

mix
  .setPublicPath(
    path.resolve(__dirname, "wp-content/themes/my-theme/assets/build"),
  )
  .ts("assets/ts/app.ts", "app.js")
  .webpackConfig({
    output: {
      chunkFilename: "chunks/[name].[contenthash].js",
      publicPath: "/wp-content/themes/my-theme/assets/build/",
    },
  });

In this example:

  • chunkFilename is relative to webpack’s output directory.
  • A chunk such as chunks/block-gallery.abc123.js is written beneath the directory passed to setPublicPath().
  • The browser requests it from /wp-content/themes/my-theme/assets/build/chunks/block-gallery.abc123.js.

If your assets are served from a CDN or a different public path, use that deployed URL instead. The important part is that output.publicPath + output.chunkFilename resolves to the file Mix emitted.

Content hashes are useful for long-lived caching, but deployment strategy matters: a cached runtime bundle may still request an older hashed chunk. Use atomic deployments or retain previous chunks long enough for old entry bundles to expire.

Load only the modules a page uses

A fully interpolated expression such as import(`./${name}`) does not allow webpack to load an arbitrary file at runtime. Webpack creates a context module containing the files that could match the expression, and a broad expression can pull more modules into that context than expected.

For a known collection of blocks, an explicit loader map produces predictable chunks and makes unsupported names easy to handle:

type BlockModule = {
  default: (elements: HTMLElement[]) => void | Promise<void>;
};

type BlockName = "gallery" | "hero";

const blockLoaders: Record<BlockName, () => Promise<BlockModule>> = {
  gallery: () => import(
    /* webpackChunkName: "block-gallery" */
    "./blocks/gallery"
  ),
  hero: () => import(
    /* webpackChunkName: "block-hero" */
    "./blocks/hero"
  ),
};

Next, group matching DOM elements by their block identifier:

function collectBlocks(selector = "[data-block][data-has-js-file]") {
  const groups = new Map<string, HTMLElement[]>();

  for (const element of document.querySelectorAll<HTMLElement>(selector)) {
    if (!element.hasAttribute("data-has-js-file")) continue;

    const name = element.dataset.block?.trim();
    if (!name) continue;

    const elements = groups.get(name) ?? [];
    elements.push(element);
    groups.set(name, elements);
  }

  return groups;
}

hasAttribute() is the appropriate boolean check here. getAttribute() returns a string or null, not true | null.

Finally, import and initialize each supported block:

async function initializeBlocks() {
  const groups = collectBlocks();

  for (const [name, elements] of groups) {
    const loader = blockLoaders[name as BlockName];

    if (!loader) {
      console.warn(`No JavaScript module is registered for block: ${name}`);
      continue;
    }

    let blockModule: BlockModule;
    try {
      blockModule = await loader();
    } catch (error) {
      console.error(`Could not load the ${name} block module.`, error);
      continue;
    }

    try {
      await blockModule.default(elements);
    } catch (error) {
      console.error(`The ${name} block failed during initialization.`, error);
    }
  }
}

initializeBlocks();

Separating import errors from initialization errors prevents a runtime bug inside a block from being reported as a missing file.

A block module can stay small:

export default function initializeGallery(elements: HTMLElement[]) {
  for (const element of elements) {
    element.classList.add("is-ready");
    // Initialize the gallery instance for this element.
  }
}

Verify that code splitting works

Do not assume that using import() automatically improved performance. Build the production bundle and verify the result:

  1. Confirm that webpack emitted separate files under the chunks/ directory.
  2. Load a page without the block and confirm its chunk is not requested.
  3. Load a page with the block and confirm the chunk URL returns 200.
  4. Check that production caching and deployment do not leave the runtime pointing to a deleted hash.
  5. Measure bundle transfer and JavaScript execution before making a performance claim.

References