From my understanding, script elements do not provide progress events. The workaround is to use XHR to fetch the script's body and rely on browser cache for a subsequent download. However, there are no specific events for browser parsing of the script.
My solution is entirely in JS, making it adaptable to any framework. It assumes that downloading will take about 70% of the total time, with 20% allocated to browser parsing. I've used an unminified version of the impressive three.js 3D library as a sizable source file.
If the script is served from a different sandbox, progress calculation may be inaccurate. But if you serve your own script, this should not be an issue.
Keep in mind that this implementation is basic. For instance, I've used a simple HR element as a progress bar.
// This gives an estimate of file size for the example
let TOTAL_ESTIMATE = 1016 * 1024;
// Using hr as a
let bar = document.getElementById("progressbar");
let button = document.getElementById("dlbtn");
var js; // to store the created DOM element
var fileName; // to keep the cacheBusted script address
/* This function will be called multiple times during the initial download, providing information on how much data has been loaded */
function onProgress(e) {
var percentComplete = e.loaded / TOTAL_ESTIMATE;
if (e.lengthComputable) {
percentComplete = e.loaded / e.total;
}
p = Math.round(percentComplete * 100);
console.log("progress", p + "%,", e.loaded, "bytes loaded")
bar.style = "width: " + (5 + .6 * p) + "%"; // Assuming download will take around 60-70% of total time
}
/* This function is triggered when information is received. After the initial download completes, the readyState will be 4 and we set the src attribute of the file, triggering a re-download but taking advantage of the browser's cache. Although not ideal, simply eval-ing the data might yield better results. I assumed you wanted a <script> tag on your page that needed evaluation. */
function onReadyState(e) {
let r = e.target;
if (r.readyState != 4 || r.status != 200)
return;
let l = r.responseText.length;
console.log("Success!", l, "total bytes (" + Math.round(l / 1024) + " KB)");
bar.style = "width: 70%";
js.src = fileName;
bar.style = "width: 80%";
var ref = document.getElementsByTagName('script')[0];
ref.parentNode.insertBefore(js, ref);
};
// Called after the script has been evaluated:
function onScriptLoaded() {
bar.style = "width: 100%; background-color: lightgreen;";
button.disabled = false;
console.log("Script evaluated?", THREE ? "yes" : "no"); // Demo file exposes window.THREE
}
function downloadFile(file) {
button.disabled = true;
(function(d) {
// This part helps test the script multiple times. Remove in production
fileName = file + "?bustCache=" + new Date().getTime();
console.log("Inserting new script");
js = d.createElement('script');
js.type = "text/javascript";
js.defer = "defer";
js.async = "async";
var r = new XMLHttpRequest();
bar.style = "width: 5%"; // react immediately
r.addEventListener("progress", onProgress);
r.open("GET", fileName, true);
r.onreadystatechange = onReadyState;
js.onload = onScriptLoaded;
r.send();
}(document));
}
#progressbar {
height: 6px;
border-radius: 3px;
width: 0%;
border-color: green;
background-color: green;
}
<button onclick="downloadFile('https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.js', this)" id="dlbtn">Download</button>
<script id="dummy" type="text/javascript">
console.log("Dummy script ready")
</script>
<hr id="progressbar" align="left" />