This problem can be circumvented with a polyfill:
(function (window, document, undefined) {
const orig_add_ev_listener = Object.getOwnPropertyDescriptor(EventTarget.prototype, 'addEventListener').value;
var dom_avail = false;
var page_loaded = false;
function exec_event(p_event, p_handler, p_param)
{
var l_install_hand = true;
var l_custom_ev;
if(typeof p_event != 'string')
throw new TypeError('string expected');
if(typeof p_handler != 'function')
throw new TypeError('function expected');
if(typeof p_param == 'undefined')
p_param = { capture: false };
if(typeof p_param == 'boolean')
p_param = { capture: p_param };
if(typeof p_param != 'object' || Array.isArray(p_param))
throw new TypeError('boolean or non-array object expected');
switch(p_event)
{
case 'DOMContentLoaded':
if(this == document && dom_avail)
{
l_custom_ev = new Event('DOMContentLoaded');
l_install_hand = false;
}
break;
case 'load':
if(this == window && page_loaded)
{
l_custom_ev = new Event('load');
l_install_hand = false;
}
break;
}
if(l_install_hand)
orig_add_ev_listener.call(this, p_event, p_handler, p_param);
else
queueMicrotask(p_handler.bind(this, l_custom_ev));
}
document.addEventListener('DOMContentLoaded', p_event => { dom_avail = true; }, { once: true });
window.addEventListener('load', p_event => { page_loaded = true; }, { once: true });
Object.defineProperty(EventTarget.prototype, 'addEventListener', { value: exec_event });
})(window, document);
What this polyfill is doing is simple: The original function addEventListener
is stored, two flags for the two events that we are interested in (i. e. DOMContentLoaded
and load
) and a wrapper function to the EventTarget.prototype.addEventListener
are created. After this two event handlers are registered, each of which gets invoked on a different event from the list that we are interested in. Finally the native addEventListener
is replaced by our wrapper function.
So whenever you are loading a script asynchronously, the wrapper essentially checks which state the document is currently in when attaching one of the two problematic event handlers.
- The document is still loading.
If the asynchronously loaded script is started during this stage (that is, neither the DOMContentLoaded
nor the load
event have fired so far), any handlers to these two events are registered as per usual so they get invoked when the browser is firing the events.
- The DOM is available, but not all assets have been loaded.
Now the DOMContentLoaded
event already has fired so an event handler registered there won't be triggered any more.
What we are doing now is artificially create a DOMContentLoaded
event, bind this
and the newly created event to the event handler that we have received and add it to the browser's microtask queue. So once the thread that called addEventListener
completes, the added handler is immediately executed.
- The document has completed loading.
Here both events have already fired so any registered handlers for either of the two won't be executed any more. In order to handle this problem, both events are added to the microtask queue as per #2.
If you are registering any other events, those calls are passed straight to the native addEventListener
function.
With this polyfill (make sure to load this script synchronously as the very first script in this list!) you can now add async scripts without even having to modify them. That is, if you have a script that you are loading synchronously or with a delay (i. e. with the defer
attribute set), but you decide to switch to asynchronous loading later on, all you need to do is change the attribute without any need to rerig the scripts.
readyState == "interactive"
, it has already completed loading so you won't get any events any more even after the script may finally have loaded. Please note that since you are marking the script to be loaded as asynchronous, that isn't taken into account any more when determining a page'sreadyState
, but you should still be able to check any status code for the script in the browser's console. If you want to make sure that you still get the events, see my polyfill below.