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 such cases, it is best to use dynamic loading.
Dynamic loading of JS scripts refers to the practice of only importing dependency files on specific pages, rather than globally. 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 it can be manually created 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 is set to indicate to the browser that the script should be executed after the document has been parsed.
Async: This boolean attribute is set to indicate to the browser that the script should be executed asynchronously if possible.
For defer
, it can be thought of 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. The defer
attribute executes the scripts in their 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 presence of async
, the scripts may not be executed in their original order. If multiple script files have interdependencies, using async
may lead to errors.
In simple terms, the browser first requests the HTML document, then asynchronously requests various resources using the corresponding resource loaders, while performing DOM rendering. It continues this process until it encounters a <script>
tag. At this point, the main process stops rendering and waits for the resource to finish loading before executing it, and then continues with DOM parsing. If the async
attribute is added, it is equivalent to loading and executing the script in a separate process, while defer
has the same effect as placing the <script>
tag at the bottom of the <body>
.
Defer and Async
In the above diagram, the blue line represents network requests, 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 the JS files in a specific 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, relying solely on defer
to control the execution order of script files carries significant risks. However, by listening for the onload
event and using Promise
to wait for the previous script file to finish loading before loading the next one, we can achieve 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 sequentially
const loadAssets = async () => {
for (const url of assets) {
await loadJS(url, true)
}
}
Just enjoy it ฅ●ω●ฅ