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.
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.
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);
}
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);
}
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()
.
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);
}
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);
}
}
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');
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);
});
}
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.