For single-page applications developed using frameworks like Vue and React, it is often necessary to rely on third-party JS files for special functionality on certain pages. If CDN resources are globally imported, redundant files may be loaded. In this case, it is best to use dynamic loading.
Dynamic loading of JS scripts refers to the practice of only importing dependency files on certain special pages, rather than globally importing them. This avoids loading unnecessary resources on these pages, improves page loading speed, and makes the entire project more modular.
The Document Object Model (DOM) allows for the dynamic creation of HTML using JavaScript. The <script>
element is no different from other elements on the page, so you can manually create <script>
tags to load JS files.
defer and async#
The <script>
element has two attributes, defer
and async
, which represent two different loading and execution modes for JS scripts.
defer: This Boolean attribute indicates to the browser that the script should be executed after the document has been parsed.
async: This Boolean attribute indicates to the browser that the script should be executed asynchronously if possible.
For defer
, you can think of it as placing the external JS file at the bottom of the page. The loading of the JS file does not block the rendering and loading of resources on the page. defer
executes the JS file in the original order.
For async
, its purpose is to load and execute scripts asynchronously, also without blocking the rendering and loading of resources on the page. Once the script is loaded, it will be executed immediately. In the case of async
, the JS file will be executed as soon as it is downloaded, so it is very likely that it will not be executed in the original order. If multiple script files have mutual dependencies, using async
may lead to errors.
In simple terms, the browser first requests the HTML document, then uses the corresponding resource loader to asynchronously request various resources in the document, while performing DOM rendering. It continues to parse the DOM until it encounters the <script>
tag. At this point, the main process stops rendering and waits for this resource to finish loading before executing it, and then continues with DOM parsing. If the async
attribute is added, it is equivalent to opening a separate process to load and execute independently, while defer
has the same effect as placing the <script>
tag at the bottom of the <body>
.
The blue line in the above diagram represents network loading, the red line represents execution time, and the green line represents HTML parsing.
const loadJS = (url, defer) => {
const recaptchaScript = document.createElement('script')
recaptchaScript.setAttribute('src', url)
if (defer) {
recaptchaScript.defer = true
} else {
recaptchaScript.async = true
}
recaptchaScript.onload = () => {
console.log('Loading complete', url)
}
document.head.appendChild(recaptchaScript)
}
Here's an example. We are loading five JS scripts, where jquery-ui
and fullcalendar
depend on jquery
, and locale
depends on fullcalendar
. In this case, we need to load and execute the JS files in a certain order based on their dependencies.
const assets = [
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/fullcalendar.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/locale/zh-cn.js',
]
assets.forEach((url) => loadJS(url, true))
The Reality is Harsh#
However, in real-world scenarios, browsers do not necessarily execute deferred scripts in order, nor do they necessarily execute them before the DOMContentLoaded event is triggered. Therefore, there is a significant risk in using only defer
to control the execution order of script files. But you can use the onload
event to determine if the file has finished loading, and use Promise
to wait for the previous script file to finish loading before loading the next file, thus achieving sequential loading and execution of scripts.
const loadJS = (url) => {
return new Promise((resolve) => {
const recaptchaScript = document.createElement('script')
recaptchaScript.setAttribute('src', url)
recaptchaScript.defer = true
recaptchaScript.onload = () => {
resolve()
}
document.head.appendChild(recaptchaScript)
}).catch(console.error)
}
// Load JS files in sequence
const loadAssets = async () => {
for (const url of assets) {
await loadJS(url, true)
}
}
Just enjoy it ฅ●ω●ฅ