Skip to content

Dynamic Imports

Dynamic imports allow you to load modules conditionally, improving performance and modularity. It enables developers to selectively import modules based on certain conditions, user interactions, or other runtime factors, rather than loading all modules upfront during the initial page load.

The traditional approach to importing modules in JavaScript involves using static imports, where modules are imported at the beginning of a file before any code execution. However, this can lead to slower page loads and unnecessary loading of modules that may not be immediately required.

Dynamic imports solve this issue by allowing modules to be imported only when they are needed, making your web applications faster and more efficient.

  • Improved Performance: Reduces the initial load time by only loading modules when necessary.
  • Modularity: Breaks down your code into smaller, reusable pieces.
  • Conditionally Load Code: Load specific code only after a user action (e.g., clicking a button, scrolling to a section).

However, it is important to note that dynamic imports are not always suitable for production without careful consideration. It’s crucial to dynamically import modules only when it makes sense, such as on user interactions (like button clicks) or based on specific events (like scrolling). Additionally, using a tool like BoostifyJS can help by handling dynamic imports on common events like scrolls, clicks, and intersections with ease.

// Traditional static import
import MyComponentClass from './myComponent.js';
// Initialize the component
const instance = new MyComponentClass();

Here’s an example of using dynamic imports with .then(). This approach is useful if you prefer using promises and don’t want to use async/await.

class MyComponent {
constructor() {
this.init(); // Initialize the component
}
init() {
// Dynamically import the module
import('./myComponent.js')
.then((module) => {
const MyComponentClass = module.default;
// Create an instance of the dynamically loaded class
new MyComponentClass();
})
.catch((err) => {
console.error('Error loading component:', err);
});
}
}

In this example, the myComponent.js module is only loaded when the init() method is called, potentially saving initial load time.

Using async/await provides a cleaner syntax compared to using .then(). Here’s an example:

class MyComponent {
constructor() {
this.init(); // Initialize the component
}
async init() {
try {
// Dynamically import the module
const module = await import('./myComponent.js');
const MyComponentClass = module.default;
// Create an instance of the dynamically loaded class
new MyComponentClass();
} catch (err) {
console.error('Error loading component:', err);
}
}
}

In this example, the dynamic import is wrapped in an async function to allow the use of await. This makes the code more readable and avoids chaining .then().

You can also destructure the dynamically imported module to access both the default export and named exports. This can be useful when a module exports multiple things.

class MyComponent {
constructor() {
this.init();
}
async init() {
try {
// Dynamically import the module and destructure its exports
const { default: MyComponentClass, namedExport } = await import('./myComponent.js');
// Use the default export
const instance = new MyComponentClass();
// Optionally, use any named exports
namedExport();
} catch (err) {
console.error('Error loading component:', err);
}
}
}

In this example, you can access both the default export (MyComponentClass) and a named export (namedExport) from the module.

Dynamic Imports for User Interactions (Best Practice) Dynamic imports are best used when triggered by user interactions, such as:

  • Button clicks: Only load the component when the user clicks a button.
  • Scroll events: Load a component when the user scrolls to a certain section of the page.
  • Intersection Observer: Load components when they come into view, optimizing performance for off-screen content. Here’s a common use case of using dynamic imports with a button click:
document.querySelector('.js--load-button').addEventListener('click', async () => {
const module = await import('./myComponent.js');
const MyComponentClass = module.default;
new MyComponentClass();
});

Using Libraries for Event-Based Dynamic Imports

Section titled “Using Libraries for Event-Based Dynamic Imports”

Here’s a rewritten version of your code example, with added comments and a more detailed explanation of what each part does:

// Handle expanding cards
var cardExpand = document.querySelectorAll('.js--card-expand');
// Check if there are any card elements with the class '.js--card-expand'
if (cardExpand.length) {
// We store the instances of the dynamically loaded module in this.instances["CardExpand"]
// This is necessary for proper memory management, especially when using page transition libraries.
// It's important to keep track of these instances so we can destroy them if we navigate away from the current page,
// preventing memory leaks or unwanted behavior.
this.instances["CardExpand"] = [];
// Use BoostifyJS to trigger a dynamic import when the user scrolls 50 pixels into the page.
// This ensures that we only load the module when it's needed, improving performance.
this.boostify.scroll({
distance: 50, // Trigger the callback when the user scrolls 50px
name: "cardExpand", // A unique name for this scroll event, useful to destroy it later.
callback: async () => {
try {
// Dynamically import the 'ExpandedCards' module from the specified path.
// This will only be loaded when the scroll event is triggered, optimizing page load performance.
const { default: ExpandedCards } = await import('@jsModules/ExpandedCards');
// Store the imported module globally if needed in window["lib"], which allows us to re-use the imported module later
// without triggering another dynamic import, saving resources.
window["lib"]["CardExpand"] = ExpandedCards;
// Loop through each card expand element and its children.
// For each child card, we create a new instance of ExpandedCards, initializing the animation or interaction
// specific to that card. We store each instance in this.instances["CardExpand"].
cardExpand.forEach((element) => {
Array.from(element.children).forEach((child, childIndex) => {
// Create a new instance of ExpandedCards for each child card and store it in the instances array.
// We pass the card wrapper (the parent container), the individual card, and the index of the card as options.
this.instances["CardExpand"].push(new ExpandedCards({
cardWrapper: element, // The wrapper that contains all the cards
card: child, // The individual card element within the wrapper
index: childIndex, // The index of the card within the wrapper (helpful for targeting specific cards)
}));
});
});
} catch (error) {
// Handle any errors that occur during the dynamic import
// For instance, if the module fails to load due to a network issue or any other problem,
// log the error to the console for debugging purposes.
console.error("Error loading expanding cards animation", error);
}
}
});
}

This example shows how you can optimize performance by using dynamic imports and event-driven logic with a scroll-triggered import using BoostifyJS. It’s an ideal solution when you need to delay loading animations or components until the user interacts with the page, ensuring that the initial page load remains fast and efficient.