Loading CSS and Javascript On Demand in a CMS Environment

Introduction

In modern web development, particularly within a Content Management System (CMS) environment, the efficiency of resource management is paramount. Loading JavaScript and CSS libraries selectively — only when they are associated with a specific block or component — offers significant advantages. This approach not only enhances performance by reducing the overall page load time but also optimises the user experience by minimising unnecessary code execution.

By ensuring that libraries are only loaded when their corresponding features are present in the page content, developers can avoid bloating the page with unused scripts and styles. This practice is particularly useful in a CMS where content can be highly dynamic and varied. Moreover, when a feature is repeated multiple times within a page, it is essential to load the associated libraries only once. This prevents redundant downloads and executions, leading to a cleaner, more efficient application that conserves bandwidth and resources.

This also helps to keep code self-contained - you need only place the call to load those libraries in the code responsible for rendering those components leaving the calling parent page untouched.

This article will demonstrate one strategy for implementing on-demand loading of JavaScript and CSS in a CMS context, thereby enhancing performance and maintainability. We'll define two JavaScript functions, one each for CSS and JavaScript and load these on the calling parent page instead.

Loading CSS on Demand

When loading CSS on demand in a CMS, the objective is to add stylesheets to the document only when necessary, without adding redundant or duplicate tags. Unlike JavaScript, where we often need to know if a script has fully loaded, CSS files need only be added to the document, and it’s not essential to monitor whether they have finished loading. The task here is to ensure that each stylesheet is added just once, and no unnecessary duplication occurs if the same CSS file is required in multiple components.

The include_css function below is designed to passively handle the process of loading CSS. Its primary function is to check if the stylesheet has already been added to the document by looking for an existing <link> tag with the same href. If it finds a matching <link> element, it skips the process, preventing multiple instances of the same stylesheet from being added.

Here’s a breakdown of how it works:

Copy
const include_css = (css, options = {}) => {
  let link_tag = document.querySelector(`link[href="${css}"]`);
  if (!link_tag) {
    try {
      const head = document.head || document.getElementsByTagName('head')[0];
      link_tag = document.createElement('link');
      link_tag.rel = 'stylesheet';
      link_tag.href = css;
      link_tag.type = options.type || "text/css";
      if (options.media) link_tag.media = options.media;
      if (options.integrity) link_tag.integrity = options.integrity;
      if (options.crossorigin) link_tag.crossOrigin = options.crossorigin; 
      head.appendChild(link_tag);
    } catch (error) {
      console.error(`Failed to load ${css}:`, error);
    }
  }
};
  • Checking if CSS is Already Loaded:
    The function first checks if a <link> element with the specified href (the URL of the CSS file) already exists in the document. This is done using document.querySelector to search for any matching <link> tags. If such a tag is found, no further action is taken, ensuring that the stylesheet is not loaded again.
  • Adding the CSS if Not Present:
    If no existing <link> tag is found, the function creates a new <link> element and assigns it attributes like rel="stylesheet" and href pointing to the CSS file. It then appends this tag to the <head> of the document, effectively loading the stylesheet.
  • Customisation with options:
    The options parameter allows for additional attributes such as:
    • media: To specify media-specific stylesheets (e.g., print or screen).
    • integrity: For security purposes, to check the integrity of the resource.
    • crossorigin: To handle cross-origin resource loading.
    • type: Defaults to "text/css" but can be adjusted if needed.
  • Graceful Error Handling:
    In case an error occurs during the creation or appending of the <link> tag, the function catches it and logs a message to the console. This ensures the page does not crash if a CSS file fails to load.

Loading JavaScript on Demand

When loading JavaScript on demand, the process becomes more involved than for CSS. This is because scripts often depend on other scripts, and their execution timing matters. A key requirement is to monitor when a script has fully loaded so that any dependent code can execute only once it is safe to do so. For example, if one script depends on another for certain objects or methods, the second script should only run after the first has loaded and executed.

The include_js function below is designed to load JavaScript files dynamically, ensuring that scripts are not added more than once and handling the timing of their execution. By leveraging Promises, the function can signal when a script has successfully loaded or if it fails, making it possible to chain dependent code only after the script is fully available.

Here’s a detailed breakdown of how it functions:

Copy
const include_js = (js, options={}) => {
  return new Promise((resolve, reject) => {
    let script_tag = document.querySelector(`script[src="${js}"]`);
    if (!script_tag) {
      const head = document.head || document.getElementsByTagName('head')[0];
      script_tag = document.createElement('script');
      script_tag.src = js;
      script_tag.type = options.type || 'text/javascript';
      if (options.integrity) script_tag.integrity = options.integrity;
      if (options.crossorigin) script_tag.crossOrigin = options.crossorigin;
      if (options.defer) script_tag.defer = true;
      if (options.async) script_tag.async = true;
      script_tag.onload = () => {
        script_tag.dataset.scriptLoaded = true; // Set attribute once loaded
        resolve();
      };
      script_tag.onerror = () => {
        console.error(`Failed to load script: ${js}`);
        reject(new Error(`Script load error: ${js}`));
      };
      head.appendChild(script_tag);
    } else {
      // Script tag exists, check if it's fully loaded
      if (script_tag.dataset.scriptLoaded === "true") {
        // Script is already fully loaded, resolve immediately
        resolve();  
      } else {
        // Script is already added but still loading, add event listeners
        script_tag.addEventListener('load', resolve);
        script_tag.addEventListener('error', reject);
      }
    }
  });
};
  • Handling Script Loading with Promises:
    The function begins by returning a Promise, which will either resolve when the script has fully loaded or reject if an error occurs. This structure allows any dependent code to wait for the script to be fully available before continuing execution. The function sets up an event listener for the onload event to signal the script’s successful loading, and an onerror event to catch any failures.
  • Checking if Script is Already Added:
    Like with CSS, the function first checks if a <script> tag with the same src (the URL of the script) already exists in the document. If it does, the function skips adding the script again to avoid duplicate requests. If the script is still in the process of loading, it waits for it to finish before resolving the Promise. If the script has already fully loaded, it resolves immediately, allowing any dependent code to continue without delay.
  • Using a data-script-loaded Attribute:
    Once a script has successfully loaded, the function marks the <script> tag by adding a data-script-loaded="true" attribute. This allows the function to check in future calls if the script is fully loaded and to resolve the Promise instantly if it has. This handles the case where a script has already been added but has not yet fully loaded. In this case, an additional load event listener is added to the script which resolves this promise when triggered.
  • Error Handling:
    If the script fails to load, the onerror event triggers, rejecting the Promise and logging an error to the console. This ensures that errors are caught early and the application can handle missing or faulty scripts gracefully.
  • Customisation with options:
    Similar to the CSS loader, the include_js function allows for additional attributes to be passed via the options parameter, such as:
    • crossorigin: To manage cross-origin script loading.
    • integrity: For verifying the integrity of the loaded script.
    • defer: To delay execution of the script until the HTML document has been fully parsed.
    • async: To load the script asynchronously while the rest of the page continues to process. This is the default behaviour since we are loading via Promise.
    • type: Defaults to "text/javascript" but can be adjusted if necessary.

Example Usage

Here are two examples demonstrating how to use include_js to load two JavaScript files, script1 and script2, with different dependency rules before calling a method that requires both scripts to be loaded first.

Simultaneous Loading

In this scenario, both scripts are independent of each other and can be loaded in parallel. We wait for both promises to resolve before calling the doSomething() function.

Copy
Promise.all([include_js(script1), include_js(script2)])
  .then(() => {
    // Both scripts have successfully loaded
    doSomething();
  })
  .catch((error) => {
    console.error("One or more scripts failed to load:", error);
  });

Dependent Loading

In this case, script2 depends on script1 being fully loaded before it can be loaded. Once both scripts are loaded, doSomething() is called.

Copy
include_js(script1)
  .then(() => {
    // script1 has loaded, now load script2
    return include_js(script2);
  })
  .then(() => {
    // Both script1 and script2 have successfully loaded
    doSomething();
  })
  .catch((error) => {
    console.error("Failed to load script:", error);
  });

Conclusion

In this article, we have explored the importance of loading JavaScript and CSS on demand in a CMS environment. By loading resources only when necessary, we can significantly improve performance and reduce unnecessary overhead, particularly in pages that only use specific components or blocks.

The include_css function ensures that stylesheets are only added once to the document and are not redundantly loaded if the same CSS is requested multiple times. This approach optimises page performance by reducing network requests and ensuring that only the necessary styles for the current page are applied.

Similarly, the include_js function provides an efficient way to load JavaScript scripts on demand. It uses Promises to control the timing of script execution, allowing developers to handle script dependencies effectively. By ensuring scripts are only added once and executed in the correct order, the function helps to optimise resource management and avoid redundant script loading.

Together, these methods enable a streamlined, efficient approach to resource management in a CMS, enhancing both performance and maintainability.


  Please feel free to leave any questions or comments below, or send me a message here