Dynamic Imports

Code splitting with dynamic imports

Dynamic Imports

Packem supports dynamic import() expressions and uses them as code-split points. Modules loaded with import() are emitted into separate chunks that are only fetched when the importing code actually runs, keeping the initial bundle small.

Overview

Because Packem builds on Rollup, any import() call in your source becomes a code-splitting boundary. This is useful for:

  • Lazy-loading heavy or rarely used features
  • Splitting optional functionality out of the main entry
  • Reducing startup cost for CLIs and servers

:::note Dynamic imports are a supported, first-class feature of Packem (see the feature list in the package README). :::

Project Structure

dynamic-imports-example/
├── src/
│   ├── index.ts
│   ├── heavy-feature.ts
│   └── reporter.ts
├── package.json
├── packem.config.ts
└── tsconfig.json

Source Files

src/heavy-feature.ts

// A large module we only want to load on demand
export function runHeavyFeature(input: string): string {
  // ...imagine a lot of code here
  return `processed: ${input}`
}

src/index.ts

export async function maybeRun(flag: boolean, input: string) {
  if (!flag) {
    return input
  }

  // Loaded lazily — emitted into its own chunk
  const { runHeavyFeature } = await import('./heavy-feature')

  return runHeavyFeature(input)
}

Configuration

No special option is required — dynamic import() is split automatically.

packem.config.ts

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  transformer,
  declaration: true,
  sourcemap: true
})

package.json

{
  "name": "@myorg/dynamic-imports",
  "version": "1.0.0",
  "type": "module",
  "files": ["dist"],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs"
    }
  },
  "scripts": {
    "build": "packem build"
  },
  "devDependencies": {
    "@visulima/packem": "^2"
  }
}

Build Output

The dynamically imported module is placed in its own chunk and referenced via import() in the output:

$ packem build

dist/index.mjs            # entry — keeps the import() call
dist/heavy-feature.mjs    # split chunk, loaded on demand
// dist/index.mjs (simplified)
export async function maybeRun(flag, input) {
  if (!flag) return input
  const { runHeavyFeature } = await import('./heavy-feature.mjs')
  return runHeavyFeature(input)
}

Notes

  • Each unique dynamic import target becomes a separate chunk; shared dependencies between chunks are hoisted automatically by Rollup.
  • If you want code shared across multiple bundles without a separate entry, prefer the Shared Modules convention instead.
  • Use Bundle Analysis to confirm how your code splits across chunks.
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues