Dynamic Module Pattern for JavaScript

Dynamic Module Pattern for JavaScript

Dynamically load JavaScript bundles only when they are needed using HTML and WebPack

Note: Please make sure that you are already aware of the code-splitting and dynamic import mechanism. This pattern is module bundler agonistic as long as it provides code splitting and dynamic import features. With some work, you can have own implementation of this pattern.

Users often leave a website in about 10 to 20 seconds as soon as they visit it. Slow websites increases bounce rate. If a website is slow and have a high bounce rate, it will inevitably drop the website ranking. Besides, even if it maintains good score, users will have no choice but to leave the website after a few seconds.

If performance of a website is an utmost priority, we may implement lazy loading of images, use picture elements, use caching, CDN, among tons of other optimization techniques. However, the problem that I noticed was that people were struggling with loading JavaScript on demand.

Hand-picking JavaScript libraries has its own problems. For instance, we might need to include different script tags for each page that makes use of certain markup or library. This can lead to clutter and maintenance issues. Some may have an arbitrary implementations that may or may not work under certain conditions. Even Google Tag Manager can be cumbersome.

In order to solve this problem, I introduce...

The Dynamic Module Pattern

The Dynamic Module Pattern is a pattern where you define, right in your markup, what associated JavaScript modules should be loaded. Suppose, you have a slider module in your app that makes use of the library called, flickity.js. When you include the markup, the dynamic module pattern will load appropriate JavaScript bundles for it and if you completely remove the slider, no JavaScript will be loaded. You don't have to worry about manually removing it.

This not only takes away the headache of micromanaging libraries in your markup using script tags or a list of if statements in case you are using a templating engine. Another great thing about this pattern is that you don't really need to worry about where the markup is coming from as long as certain attributes are defined (See explanation section on more benefits).

For instance, it could be a Shopify snippet or section. A WordPress post or shortcode, Laravel or Node based server-side sites using templates, static sites, this pattern also work perfectly with all of them. Unless of course, your development environment already provides a code-splitting mechanism like create-react-app or vue-cli, in which case you don't really have to worry about this.

How Does it Work?

I'll provide the code snippets and after that I'll explain what's going on. I'm using this pattern for a WordPress theme that uses WebPack and Svelte. The same can be done for React or Vue especially if you are making isolated snippets or widgets. The Shortcode provides user the ability to give it a module name and the associated JavaScript bundle will be loaded for it. Magic! 🎩

Markup

<div data-module="slider"></div>

JavaScript

const modules = Array.from(document.querySelectorAll('[data-module]'));

modules.forEach((module) => {
  const componentName = module.getAttribute('data-module');

  import(`./components/${componentName}.svelte`)
    .then((component) => {
      if (component && component.default) {
        new component.default({
          target: module,
        });

        console.log(`${componentName}.svelte loaded.`);
      }
    })
    .catch((error) => {
      console.warn(`${componentName}.svelte failed to load.`, error);
    });
});

Explanation

The HTML is quite simple. We define a simple HTML div element with the attribute of data-module which is also the name of the component aka file that we need to import in order to bring this component to life. This element is simply the root element for svelte slider component.

The JavaScript is however, interesting. It first fetches all the elements present in DOM that have the data-module attribute defined. It loops through all those elements and for each single one, it gets the data-module attribute.

After that, it tries to dynamically import a certain component that exists in the components folder (./components/{component-name}.extension). If the component is successfully loaded, we're notified instantly. If the component is not present or fails to load, we receive a warning.

The best thing about this pattern is that I can add and remove this markup or I can use it multiple times in my page. This pattern will make sure that the appropriate JavaScript is loaded or not loaded.

Does this load JavaScript bundles multiple times if I make use of the data-module several times in the page markup? Please, keep reading. I'll answer that soon!

Without Module Bundlers?

You can definitely modify this pattern to fit your needs. For instance, you can use intersection observers and or events like key events, mouse events, hover, scroll and what not to dynamically load JavaScript. Imagine, you can prefetch or pre-connect components on user events and fetch them whenever they are needed. 🚀

Like I said, you can use this pattern without module bundlers. You can implement the Dynamic Module Pattern using a custom import statement that can load JavaScript from CDN or locally from your own website. However, keep in mind that this may not be as easy as you think. There are several problems that you need to keep in mind.

Caveats With Custom Implementations

Repeated data-module Elements: If an element repeated more than once, a naïve implementation will dynamically load script tags for each individual element. For instance, if an element is used in four places that makes use of bundles that weight about 80 KBs, you've just downloaded 320 KBs of JavaScript!

Dependencies: This is a major issue with custom implementations. Module bundlers can easily nest or map out the dependency tree but in custom implementation, any bundle that is imported must be and need to be available at a global scope, unless they are isolated containers that one does not really need to worry about.

This also begs the question of, "What if I need to load flickity.js and then my custom JavaScript in order to make my slider functional?" This is an actual issue. You'd have to handle the dependency tree on your own which is not a simple task IMO.

Caveat With Original Implementations

Parallel Scripts Loading: This pattern can be definitely tweaked in order to support parallel script loading. Right now, my method does not support that. For instance, you can load Vue along with your custom JavaScript bundle for which Vue is a dependency. As soon as they are both loaded, you appropriately initialize them by passing Vue as a parameter.

Bonus: Naïve Custom Implementation

This is just for fun in case you want to test things about without the headache of setting up a module bundler!

const customImport = (src) =>
  new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.async = true;
    script.onload = resolve;
    script.onerror = reject;
    document.body.appendChild(script);
  });

const modules = Array.from(document.querySelectorAll('[data-module]'));

modules.forEach((module) => {
  const componentName = module.getAttribute('data-module');

  // This could be anything, CDN or a local asset.
  customImport(`${componentName}.extension`)
    .then(function() {
        // script context IE window.Vue etc
    })
    .catch((error) => console.warn('failure', error));
});

Let me know if you found Dynamic Module Pattern helpful. Please, do share your thoughts on this, I would love to hear how this might help you. Please, make sure to react and share this too. Thank you for reading!

Cover image credits: Anthony Shkraba from Pexels