shinsenter / defer.js

🥇 A lightweight JavaScript library that helps you lazy load (almost) anything. Defer.js is dependency-free, highly efficient, and optimized for Web Vitals.
https://shinsenter.github.io/defer.js/
MIT License
277 stars 45 forks source link

executing inline scripts and js files in order, when mixing defer and delay #115

Closed peixotorms closed 2 years ago

peixotorms commented 2 years ago

Hi there,

I am trying to have some scripts delayed, while having some other scripts deferred, however I am finding a situation when the scripts that are lazy loaded, execute before the deferred scripts.

For example:

If I define something on an inline script and add the type="deferjs" attribute, and then later create a regular js file with a type="delayjs" that depends on the deferred script above, and call later on the page, Defer.all('script[type="wprdelay"]', 0, true);

I would expect that the type="deferjs" scripts to always run before the type="delayjs" scripts, and this is true for most cases, except when the actions that trigger interaction are already being triggered (for example, continuously press SHIFT while clicking reload, or moving the mouse while it's refreshing).

When this happens, apparently the scripts that require interaction run before the other deferred scripts can finish, thus I get undefined errors when the delayed script run.

I tried to wrap the script that runs the delayed code like this: <script type="deferjs">Defer.all('script[type="wprdelay"]', 0, true);</script> but when I do this, the wprdelay scripts run right after the deferred scripts without any interaction, thus basically the 3rd parameter is ignored.

So I need to ensure that in these situations, the deferred scripts always run before the ones that are meant to run on interaction.

I think that perhaps it's easier (just an idea), to trigger the deferred scripts earlier if there is interaction as well.

For example, if the deferred scripts that are meant to run without interaction, are currently listening to the load event to run, then if interaction happens before that event, it should run immediately without waiting for the load event.

What do you think can be done about this situation?

Thank you so much

peixotorms commented 2 years ago

Edited the description and title, after further testing.

shinsenter commented 2 years ago

@peixotorms

I think there are 2 small problems in your setup.

The first is that the type=deferjs tags will be initialized last by this library, after it has deferred all other user-defined code. In this case, due to user interaction, lazyjs tags will be executed first.

You have two workarounds, either use a different name instead of the default deferjs, or increase the timeout of lazyjs tags a bit so that it executes after the deferjs tags.

Second, it's pretty pointless to lazy load scripts as soon as the page has finished loading, so when it knows the page has finished loading, the library won't delay it and execute the scripts as soon as possible.

In your example, you are calling the Defer.all inside a script tag with type=wprdelay, that means all tags with type=wprdelay will be executed according to the behavior of the wrapper script tag's type=deferjs attribute rather than by type=wprdelay. I think the Defer.all should not be call within a script with type=deferjs.

peixotorms commented 2 years ago

I think maybe I didn't explain correctly, so allow me to rephrase it and add more information.

Conditions:

The only render blocking scripts, are the ones to initialize the deferjs scripts. There are no other render blocking scripts or scripts with the defer attribute.

Case 1

I have all inline scripts and js files as type="deferjs", everything runs in order, on page load, always.

Case 2

I have all inline scripts and js files as type="delayjs", and initialize it with Defer.all('script[type="delayjs"]', 0, true); everything runs in order, on user interaction, always.

Case 3

If I mix them, let's say some scripts as type="delayjs" and others as type="deferjs" , and refresh the page, it runs in order, first the deferjs scripts, and later, upon interaction, the delayjs... but not always (this is the problem).

When you want to clear the cache and refresh the page, you normally hit the SHIFT key and click the reload button while still pressing it. Or for example, if you are rushing, you may be moving the mouse around while you refresh (or following a link and moving the mouse waiting for it to finish loading).

In that situation, it will match the keydown or mousemove before it has the chance to run the type="deferjs" scripts, so effectively, the type="delayjs" run before the type="deferjs" scripts.

This is a problem because now the order of the scripts is not respected, and the scripts that are supposed to be delayed, have unmet dependencies from the deferjs that did not run yet.

I tried to replace the type="deferjs" with type="deferjs2" for example, and initialize it with Defer.all('script[type="wprdefer2"]', 0, false); before Defer.all('script[type="delayjs"]', 0, true); .

However, even if the manual initialization of the scripts is render blocking, it will always run the type="delayjs" scripts before any deferjs or deferjs2 code has the chance to run, when we are trying to interact with the page before the DOM is fully loaded (because the script is listening to ["mousemove","keydown","touchstart","wheel"] for the delayjs, but only for the pageshowfor the deferjs scripts).

In other words, we have a situation where the delayjs scripts run before the deferjs scripts, because in those cases, interaction happens before the pageshow event.

I tried to make the wprdelay scripts wait, with stuff like this and it doesn't work. I also tried to replace DOMContentLoaded with pageshow or load (I still want to load them only on interaction, but not before the deferjs) , same effect.

Defer.all('script[type="wprdefer2"]', 0, false);
window.addEventListener('DOMContentLoaded', (event) => {
    Defer.all('script[type="delayjs"]', 0, true);
});

Adding a waiting timeout partially solve the issue, for example: Defer.all('script[type="delayjs"]', 200, true); but if you open the dev tools and simulate a slower internet speed... it happens again, because the deferjs scripts haven't completely downloaded yet.

So in my opinion, there are two ways to solve this.

a) we need some sort of flag (or callback, custom event, etc) that allows us to prevent the delayed scripts from running until all deferjs scripts finish (if any, else it should run immediately), or

b) the deferjs scripts should listen, not only to the pageshow or similar DOMContentLoaded events, but also to the same ["mousemove","keydown","touchstart","wheel"] events, so that whatever even happens first, they will still run right away, thus at least hopefully preserving the order of appearance on the page.

btw, b) also makes sense for me because if the user is immediately trying to interact with the page, their experience is slightly degraded if the deferjs scripts are not running also on interaction, when that interaction happens before the pageshow event, AND if there is no interaction, the behavior will not change anyway.

Or maybe I am missing something, in which case please advise.

If you need a demo and screen recording to illustrate this problem, let me know.

shinsenter commented 2 years ago

@peixotorms

Regarding your case 3, I think I tried explaining before, sorry my English is not good so I didn't make you understand what I wanted to say.

https://github.com/shinsenter/defer.js/blob/f380120a325ce6ede17e372025c0fcbb0dcddde6/src/defer.js#L412-L427 If you can take a look at the source code of this library, you'll see that script tags with type="deferjs" are always called before user events are bound to the document. So I think the script tags with type="deferjs" will run before your custom type="delayjs".


As for the two ideas you've come up with, I'll give it some more thought, but before that I want you to make them clearer by giving me a few examples that you're considering.

In the idea b), launching script tags on the DOMContentLoaded event will be no different from script tags with a defer attribute. To gain better performance than DOMContentLoaded is the reason why I made this library.


I'm really curious about the HTML you're having, can you share with me a minimal HTML that reproduces the problem? Please upload it to https://jsfiddle.net.

I look forward to hearing from you. Regards.

shinsenter commented 2 years ago

@peixotorms Please check again with the latest version to see if the errors still occur or not. https://cdn.jsdelivr.net/npm/@shinsenter/defer.js@3.3.0/dist/defer.min.js

peixotorms commented 2 years ago

Thank you, unfortunately, it did not fix the issue.

Basically, I use type="wprdefer" for scripts I want to defer, and type="wprdelay" for scripts I want to delay. I initialize them both in the footer, first the wprdefer, then the wprdelay.

Here is a demo: https://dev2.fastvelocity.com/

And here is a screencast: https://dl.dropboxusercontent.com/s/jxm2k421wz19d4m/zX0Y5Gevzv.mp4

In the video:

When you open the URL and don't interact with the page, it defers the scripts. Once the mouse starts moving on the browser window (0:06) the additional delayed scripts load and there are no errors on the console (correct loading order)

On the next reload (0:11) I hit refresh and then quickly move the mouse around the browser window. Once the page refreshes, it loads the scripts out of order (dependencies broken).

So on the demo, I have 27 scripts in total. The correct loading order is always: 1,3,4,7,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,26,27 (defer) + 2,5,6,8,10,25 (delay).

When I interact with the page before the page finishes loading, the order changes, sometimes randomly. Perhaps you are loading the scripts async? Example:

function loadScriptSync (src) {
    var s = document.createElement('script');
    s.src = src;
    s.type = "text/javascript";
    s.async = false;                                 // <-- this is important
    document.getElementsByTagName('head')[0].appendChild(s);
}

If there is something you can do to make sure the order is respected, first defer, then delay, that would be perfect. Thank you so much!

shinsenter commented 2 years ago

@peixotorms

After many tries with your site, I found that the execution order of script tags in the same group is as expected.

With Defer.all(), depending on its delay parameter or when events occur on the document, script groups are executed independently of others. The script tags of each group will be executed in its order in the group without being dependent on scripts of other groups.

I think that what you mentioned is not the order of script tags in each group. Did you want the script groups to be executed in the order in which they are defined (the order of calling Defer.all)?

peixotorms commented 2 years ago

Each group loads correctly, but independently. The group 2 should always load after group 1 is finished.

This is because when optimizing speed, we will defer important scripts, and delay what is not essential.

If we "intend" to delay until interaction, we assume that defer will always execute on page load, followed by delayed scripts.

In other words, group 2 depend on group 1 to work.

shinsenter commented 2 years ago

@peixotorms

Letting groups of scripts depend on each other may cause a worse experience if a group that gets loaded first experiences problems (e.g. the connection to one of the external script files gets timeout), and other groups will have to wait due to dependency.

I understand what you're trying to do, but that's just a special case and it's simply not in the design of this library.

I think you may try another workaround instead.

Regards.