How to safely implement dynamic script loading in JavaScript?

Content verified by Anycode AI
July 21, 2024

Dynamic script loading in JavaScript lets you load JavaScript files on demand. This can improve page load times and enhance user experience since scripts are only loaded when necessary. But, if not done right, it can cause security issues and performance hiccups. So, let's go through some steps and best practices for safely implementing dynamic script loading in JavaScript, with some neat code examples.

Understanding Dynamic Script Loading

Dynamic script loading means adding <script> elements to the DOM via JavaScript after the page has initially loaded. Such an approach is handy for single-page applications (SPAs), where certain features are only needed under specific conditions.

Create a Function for Loading Scripts Dynamically

You need a helper function that creates and adds a <script> element to the document. This function should handle various scenarios, including script loading errors and ensuring the script doesn't load more than once.

function loadScript(url, callback, errorCallback) {
  if (document.querySelector(`script[src="${url}"]`)) {
    if (callback) callback(); 
    return;
  }

  const script = document.createElement('script');
  script.type = 'text/javascript'; 
  script.src = url;
  script.async = true;

  script.onload = function() {
    if (callback) callback();
  };

  script.onerror = function() {
    if (errorCallback) errorCallback(`Failed to load script: ${url}`);
  };

  document.head.appendChild(script);
}

Use Subresource Integrity (SRI) for Security

To ensure the integrity of the loaded script, use Subresource Integrity (SRI). SRI lets browsers check that a fetched resource hasn't been tampered with.

function loadScriptWithIntegrity(url, integrity, callback, errorCallback) {
  if (document.querySelector(`script[src="${url}"]`)) {
    if (callback) callback();
    return;
  }

  const script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = url;
  script.async = true;
  script.integrity = integrity; 
  script.crossOrigin = 'anonymous';

  script.onload = function() {
    if (callback) callback();
  };

  script.onerror = function() {
    if (errorCallback) errorCallback(`Failed to load script: ${url}`);
  };

  document.head.appendChild(script);
}

Avoid Inline JavaScript and Eval

Inline JavaScript and eval() can be huge security risks. They can allow potentially malicious code to run. Always keep your script logic external and stay away from eval().

Validate Script URLs

Only load scripts from trusted sources. Don't load scripts directly from user input without validating the URL.

const trustedScriptDomains = ['https://trusted.cdn.com'];

function isValidScriptUrl(url) {
  return trustedScriptDomains.some(domain => url.startsWith(domain));
}

function loadTrustedScript(url, callback, errorCallback) {
  if (!isValidScriptUrl(url)) {
    if (errorCallback) errorCallback(`Untrusted script URL: ${url}`);
    return;
  }
  loadScript(url, callback, errorCallback); 
}

Use Promises for Better Control Flow

Need to load multiple scripts in sequence? Promises can really come in handy.

function loadScriptWithPromise(url) {
  return new Promise((resolve, reject) => {
    loadScript(url, resolve, reject);
  });
}

This lets you use async/await or .then() for smoother control:

async function loadScriptsSequentially() {
  try {
    await loadScriptWithPromise('https://trusted.cdn.com/script1.js');
    await loadScriptWithPromise('https://trusted.cdn.com/script2.js');
    console.log('All scripts loaded');
  } catch (error) {
    console.error(error);
  }
}

Monitor Performance and Errors

Dynamically adding scripts can impact page performance. Use browser developer tools to monitor how your scripts are loading.

function monitorPerformance(url, loadCallback) {
  const start = performance.now();
  function wrappedLoadCallback() {
    const end = performance.now();
    console.log(`Script(${url}) load time: ${end - start}ms`);
    if (loadCallback) loadCallback(); 
  }
  loadScript(url, wrappedLoadCallback, error => console.error(error));
}

monitorPerformance('https://trusted.cdn.com/script3.js');

Implement Retry Logic for Robustness

Sometimes, script loading fails due to network issues. Adding retry logic can make your setup more reliable.

function loadScriptWithRetry(url, retries = 3) {
  return new Promise((resolve, reject) => {
    function attemptLoad(attempt) {
      loadScript(
        url,
        resolve,
        (error) => {
          if (attempt <= retries) {
            console.log(`Retrying to load script (${url}), Attempt: ${attempt}`);
            attemptLoad(attempt + 1);
          } else {
            reject(error);
          }
        }
      );
    }
    attemptLoad(1);
  });
}

Conclusion (Oops, no conclusion!)

Here's a full example combining the discussed techniques:

const trustedScriptDomains = ['https://trusted.cdn.com'];

function isValidScriptUrl(url) {
  return trustedScriptDomains.some(domain => url.startsWith(domain));
}

function loadScriptWithIntegrity(url, integrity, callback, errorCallback) {
  if (document.querySelector(`script[src="${url}"]`)) {
    if (callback) callback();
    return;
  }

  const script = document.createElement('script');
  script.type = 'text/javascript';
  script.src = url;
  script.async = true;
  script.integrity = integrity;
  script.crossOrigin = 'anonymous';

  script.onload = function() {
    if (callback) callback();
  };

  script.onerror = function() {
    if (errorCallback) errorCallback(`Failed to load script: ${url}`);
  };

  document.head.appendChild(script);
}

function loadScriptWithPromise(url) {
  return new Promise((resolve, reject) => {
    loadScriptWithIntegrity(url, 'sha256-BASE64HASH', resolve, reject);
  });
}

async function loadScriptsSequentially() {
  try {
    await loadScriptWithPromise('https://trusted.cdn.com/script1.js');
    await loadScriptWithPromise('https://trusted.cdn.com/script2.js');
    console.log('All scripts loaded');
  } catch (error) {
    console.error(error);
  }
}

loadScriptsSequentially();

Implementing these methods ensures that your script loading is both secure and efficient. This greatly boosts the security and performance of your web applications.

Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode