How to correctly defer Plugin Javascript

I’m currently trying to improve my site’s google page speed ranking, following the advice in the asset manager docs .
I have set up my theme’s base.html.twig as described there (block assets deferred) and get a very bad google page speed ranking (33) telling me “Ressources block First Paint, javascripts should be deferred”.
changing js_pipeline to true in system.yaml improves ranking to ~60, but plugin javascript does no longer work (getting errors like “jquery.min.js:2 Uncaught TypeError: $(…).featherlight is not a function” in developer console) and, “javascripts should be deferred message” is still present.
anyone can give me advice how to improve this situation ?

@hoernerfranz, There is not much info given in your question to ponder upon…

I think it would help if you could share the code for adding javascript ({% block javascripts %}) and outputting javascript ({{ assets.js() }})

no problem, here is the relevant part of my base.html.twig:

{% block javascripts %}
    {% do assets.addJs('jquery', 101) %}
    {% do assets.addJs('theme://js/jquery.treemenu.js', {group:'bottom'}) %}
    {% do assets.addJs('theme://js/site.js', {group:'bottom'}) %}
{% endblock %}

{% block assets deferred %}
    {{ assets.css()|raw }}
    {{ assets.js()|raw }}
{% endblock %}

and just an additional note:
adding “, [‘loading’ => ‘defer’]” to any “$assets->addJs()” code directly in a plugin seems to work, as mentioned loading time in page speed analysis is then away for that plugin.
But I suppose there must be a better method than editing every plugin’s source code ?

@hoernerfranz, thanks for the info. It doesn’t seem to be the whole picture though. The plugin ‘featherlight’ is being referred to in your errors and I don’t see it in your code blocks.

Also, some insight in when scripts are loaded in your resulting Html and with what attributes might give a clue.

General note:
When scripts depend on each other (e.g. featherlight depending on JQuery), the timing of the imported scripts are important. Async (loading when available) and defer (loading in order of definition) cause scripts to be loaded at other moments in time. When not applied correctly to plugin and dependency, errors may occur.

Have a look at a blog of Flaviocopes on async and defer.

1 Like

@pamtbaau, thanks for the hints, much appreciated.
and, yes, I already found the menioned blog article on async and defer.
as for the plugin featherlight, yes, I’m using this, but did not include its javascript in base.html.twig, as that is already done in the plugin code itself via asstes->addJS() call.
and, defer seems to work if I add “, [‘loading’ => ‘defer’]” there.
my confusion is now: should I add all javascript used by plugins also in the block javascripts in base.html.twig so that defer would work although not explicitely used in the plugin(s) code ?
(changing plugins code is not a good idea, I think, as it will break updates).

@hoernerfranz, You can also add the ‘defer’ attribute to the code outputting the javascript:

{{ assets.js('bottom', {'loading': 'defer}) }}

This will output the following code for all scripts grouped as ‘bottom’:

<script src="/grav/site-stillness/assets/f18e85178a1ad504311901c439bd30b0.js" defer></script>  

You’re right, changing the plugins code would indeed not be a wise idea…

ok, I tried that, pagespeed is ok then, but none of my plugin’s javascript is then loaded, so no surprise better pagespeed :smiley: .
in addition, to my understanding this code:

{% block assets deferred %}
{{ assets.css()|raw }}
{{ assets.js()|raw }}
{% endblock %}

is supposed to load all what is inside as deferred, isn’t it ?
(at least according to this reference )

@hoernerfranz, Since I cannot look at your code and efforts you have tried combined with the corresponding output, my help is quite limited at the moment…

Some final remarks:

  • Did you perhaps replace {{ assets.js()|raw }} from post #3 with {{ assets.js('bottom', {'loading': 'defer}) }} ?

    In that case I can image some jscripts are not being loaded, because all scripts grouped for ‘head’ or with default grouping (= head) will not be loaded anymore…

  • {% block assets deferred %} ‘defers’ the rendering of the block, it does not add ‘defer’ to the links inside the block.

  • In you first post, you mention error “Uncaught TypeError: (…).featherlight is not a function". This indicates that jquery is not being loading yet when "(…).featherlight” is being called by the inline script.

1 Like

@pamtbaau, thanks again for your comments.
and yes, you are right with your comments #1 and #3, that was also what I supposed is happening.
as for #2, I’m not sure if I understand that right.
does that mean I would have to add defer in the block javascripts before to actually have those deferred ?
anyways, I think all this fiddling with the theme’s base.html.twig would not in any case affect the loading of javascript embedded in plugins, which is (was) my primary intent.
for the time beeing, I have added the defer instructions directly into my my own plugin’s code, which works fine and significantly reduces page loading time.
the same applies to 3rd pary plugins which heavily rely on javascript, e.g. map marker leaflet
but with those, there is the already mentioned problem with updates.
I wonder if it would be a better idea to propose including the defer code in the plugins to the developer(s).

@hoernerfranz, I did some playing with the asset manager to get some ideas. Maybe you get some inspiration from it for your own site.

Note the following:

  • When in ‘system.yaml’, property assets:js_pipeline is set to false, the ‘loading’ option set by assets.addJs(asset, [options]) is used when assets are being rendered.

  • When in ‘system.yaml’, property assets:js_pipeline is set to true, the ‘loading’ option set by assets.js([group, [options]]) is used when assets are being rendered.

  • In the sample below, all scripts from plugins with default ‘head’ as group will be rendered at bottom of page and loaded with ‘defer’.

  • Scripts loaded by plugins can be overridden in your template and options can be added. No need to edit the code inside plugins. See below how asset from plugin ‘featherlite’ is overridden.

  • Inline scripts may cause some issues with respect to timing of jquery.
    Esp. featherlite’s inline script is nasty since it is not an external script, but created hardcoded in php.

  • You will have to test your own site because some plugins my cause errors if not loaded at the right time.

  • The sample below is based on Quark with ‘featherlight’ added. The code is used in template ‘/user/themes/quark/templates/partials/base.html.twig’

I created the following to play with scripts loaded by theme and plugins:

  • In <head>
        {% block javascripts %}
           {# Load jquery in <head> without delay because else it will load too late #}
           {% do assets.addJs('jquery', {priority: 101, group: 'head-jquery'}) %}
           {# Override the loading of assets of plugins #}
           {% do assets.addJs('plugin://featherlight/js/featherlight.min.js', {group:'head-defer', loading: 'defer'}) %}
           {# Load theme assets in <head> and load with 'defer' #}
           {% do assets.addJs('theme://js/jquery.treemenu.js', {group:'head-defer', loading: 'defer'}) %}
           {% do assets.addJs('theme://js/site.js', {group:'head-defer', loading: 'defer'}) %}
        {% endblock %}
    
        {% block assets deferred %}
           {# {{ assets.js()|raw }} #}       <-- no longer used
           {{ assets.js('head-jquery')|raw }}
           {{ assets.js('head-defer', {loading: 'defer'})|raw }}
        {% endblock %}
     </head>
    
  • Add the following at the bottom just above </body>
       {% block bottom %}
         {# All scripts with default 'head' will be rendered here #}
         {# Scripts for assets that need to be loaded in <head> can be overridden #}
         {{ assets.js('head', {loading: 'defer'})|raw }}
         {# Load scripts that are assigned to group 'bottom' with attribute 'refer' #}
         {{ assets.js('bottom', {loading: 'defer'})|raw }}
       {% endblock %}
    </body>
    

Next:

  • Use a similar approach for css.
  • Unable css loaded by unused plugins like ‘notices’, ‘login’, etc.
  • To ‘defer’ the loading of stylesheets you can use the following trick:
     {{ assets.css('bottom', { 'media': "nope!", 'onload': "this.media='all'" }) }}
    
  • If you load stylesheets at the bottom, you must add inline critical css in the <head>. I use uncss to extract critical css from all loaded css files.
1 Like

@pamtbaau,
thank yo very much for this brief explanation, which enabled me to achieve what I was looking for.
from reading the Asset Manager Docs it was not clear for me which options and/or syntax to use depending on the js_pipeline setting in system.yaml.
I would therefore recommend your description to be included in the official docs !