Page Speed - Trying to get LCP time down

Google is saying the LCP time of my Grav site should be lower. I’m trying to follow their “Eliminate render-blocking resources” suggestion, but am not a programmer and would appreciate some pointers.
For example, the “https://cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.css” file (called by the FeatherLight plugin) is said to be one of the culprits by PageSpeed Insights.
How could I postpone the moment when such file (or the FeatherLight plugin in general) is called, so it doesn’t interfere with LCP loading time?

Hi @nicolas, just to let you know that featherlight itself seems like it hasnt been updated for a few years, so might have not the best practises in its LCP time.

Do remember that your images will be being called at the time of the script running, so if the images size or quantity of images are too high, you might find it interfears

Is featherlight being called at the start of the page in the head block or at the bottom of your page . See Asset Manager | Grav Documentation

...
{% block stylesheets %}
    {% do assets.addCss('theme://css-compiled/spectre.css') %}
    {% do assets.addCss('theme://css-compiled/theme.css') %}
    {% do assets.addCss('theme://css/custom.css') %}
    {% do assets.addCss('theme://css/line-awesome.min.css') %}
{% endblock %}

{% 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 %}
</head>

<body>
...

{% block bottom %}
    {{ assets.js('bottom')|raw }}
{% endblock %}
</body>

As you seen in the example, taken from the base_html.twig page, in the head section , the javascripts are declared but are with the grouping of bottom, then after the page has painted, it will get to the bottom of the page and call the

Is the site live ? Can you provide a link so a fresh set of eyes. If still in developement maybe post your base.html.twig code on here and people can advised of potential bottlenecks.

It might be time for a new lightbox is required. It depends on your existing requirements, but it may be worth checking out.

Under plugins there is shortcode gallery+ which has been updated and includes a lightbox function, and then a commercial light box by the grav developers (Trilby Media) which is premium product.

Also lightslider is going under a major rewrite by the original JS developer, whether then the plugin can be updated but it is only in beta 2 sachinchoolur/lightGallery: A customizable, modular, responsive, lightbox gallery plugin. (github.com)

If you can give us more clairty of how you are using it in the site and what features you need etc. It might just be a bit of simply tweaking.

@nicolas, I’m a bit confused, because afaik, plugin FeatherLight:

  • does not load the css file from a cdn, but from your site itself (self-hosted):
    <link href="/user/plugins/featherlight/css/featherlight.min.css" type="text/css" rel="stylesheet">
    
  • and the css version being used by the plugin is 1.7.7 and not 1.7.14.

You said:

but am not a programmer

Unfortunately, there is no quick fix. I’m afraid you will need to get your hands dirty in code unless plugins and/or theme are/is being redesigned.

@spamhater Thanks for your detailed response. This is the associated base template:

{% use 'blocks/base.html.twig' %}
<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: grav.config.site.default_lang }}">
<head>
{% block head deferred %}
    <meta charset="utf-8" />
    <title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>

    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {% include 'partials/metadata.html.twig' %}

    <link rel="icon" type="image/png" href="{{ url('theme://images/favicon.png') }}" />
    <link rel="canonical" href="{{ page.url(true, true) }}" />
{% endblock head %}

{% block stylesheets %}
    {% do assets.addCss('theme://css-compiled/spectre'~compress) %}
    {% if theme_var('spectre.exp') %}{% do assets.addCss('theme://css-compiled/spectre-exp'~compress)  %}{% endif %}
    {% if theme_var('spectre.icons') %}{%  do assets.addCss('theme://css-compiled/spectre-icons'~compress) %}{% endif %}
    {% do assets.addCss('theme://css-compiled/theme'~compress) %}
    {% do assets.addCss('theme://css/custom.css') %}
    {% do assets.addCss('theme://css/line-awesome.min.css') %}
{% endblock %}

{% 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 %}

 <! ––  LIGHTBOX     ––>
<link href="//cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.css" type="text/css" rel="stylesheet" />

</head>
<body id="top" class="{% block body_classes %}{{ body_classes }}{% endblock %}">
    <div id="page-wrapper">
    {% block header %}
        <section id="header" class="section">
            <section class="container {{ grid_size }}">
                <nav class="navbar">
                    <section class="navbar-section logo">
                        {% include 'partials/logo.html.twig' %}
                    </section>
                    <section class="navbar-section desktop-menu">

                        <nav class="dropmenu animated">
                        {% block header_navigation %}
                            {% include 'partials/navigation.html.twig' %}
                        {% endblock %}
                        </nav>

                        {% if config.plugins.login.enabled and grav.user.username %}
                            <span class="login-status-wrapper"><i class="fa fa-user"></i> {% include 'partials/login-status.html.twig' %}</span>
                        {% endif %}

                    </section>
                </nav>
            </section>
        </section>
        <div class="mobile-menu">
            <div class="button_container" id="toggle">
                <span class="top"></span>
                <span class="middle"></span>
                <span class="bottom"></span>
            </div>
        </div>
    {% endblock %}

    {% block hero %}{% endblock %}

        <section id="start">
        {% block body %}
            <section id="body-wrapper" class="section">
                <section class="container {{ grid_size }}">
                    {% block messages %}
                        {% include 'partials/messages.html.twig' ignore missing %}
                    {% endblock %}
                    {{ block('content_surround') }}
                </section>
            </section>
        {% endblock %}
        </section>

    </div>

    {% block footer %}
        {% include 'partials/footer.html.twig' %}
    {% endblock %}

    {% block mobile %}
    <div class="mobile-container">
        <div class="overlay" id="overlay">
            <div class="mobile-logo">
                {% include 'partials/logo.html.twig' with {mobile: true} %}
            </div>
            <nav class="overlay-menu">
                {% include 'partials/navigation.html.twig' with {tree: true} %}
            </nav>
        </div>
    </div>
    {% endblock %}

{% block bottom %}
    {{ assets.js('bottom')|raw }}
{% endblock %}

<! ––  LIGHTBOX     ––>
<script src="//code.jquery.com/jquery-latest.js"></script>
<script src="//cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.js" type="text/javascript" charset="utf-8"></script>

</body>
</html>

So from what I understand the plugin is being called at the end of the page, but the .min.css file is being called at the head block. Where would be the best place for it in your opinion?

Thanks for the heads up regarding substitute plugins, I’ll look into them as well.

There are also 2 other resources (not related to FeatherLight) that google is saying are “render-blocking” (to a lesser extent though), a .css and a .js:

/assets/5ce715a4e759ae61f0a5e1c339fd3b24.css?g-b59e8687
/assets/eb61294d9675af23985a9eb6d98cda25.js?g-b59e8687)

Could this also be fixed with some simple tweaking?

@pamtbaau I don’t know, should be using FeatherLight v1.5.0 according to the Admin panel. But I started using the plugin some years ago and don’t remember whether at the time I manually inserted something directly at the base template, maybe I did and it wasn’t updated accordingly?

@nicolas looking at your page, its is NOT using the plugin and the template has been had the elements added to the template, in where I can see Jquery is loading twice (which could be an issue and will definetly slow it down) as external cdn will be slower than internally based files (js,css in this case)

With your skills issues, I would download a fresh copy of grav to a your computer, in a new directory, copy users directory from your existing / working / live copy to the fresh install.

I would suggest firstly installing featherlight from plugins admin panel

plugins --> add --> featherlight 

once loaded, you click the plugin name and you will be shown default values, which can be edited

Then tidy up your page code removing the featherlight which is hard coded and used the cdn, remove the second copy of jquery

Replace your base.html.twig to

 {% use 'blocks/base.html.twig' %}
<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: grav.config.site.default_lang }}">
<head>
{% block head deferred %}
    <meta charset="utf-8" />
    <title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>

    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    {% include 'partials/metadata.html.twig' %}

    <link rel="icon" type="image/png" href="{{ url('theme://images/favicon.png') }}" />
    <link rel="canonical" href="{{ page.url(true, true) }}" />
{% endblock head %}

{% block stylesheets %}
    {% do assets.addCss('theme://css-compiled/spectre'~compress) %}
    {% if theme_var('spectre.exp') %}{% do assets.addCss('theme://css-compiled/spectre-exp'~compress)  %}{% endif %}
    {% if theme_var('spectre.icons') %}{%  do assets.addCss('theme://css-compiled/spectre-icons'~compress) %}{% endif %}
    {% do assets.addCss('theme://css-compiled/theme'~compress) %}
    {% do assets.addCss('theme://css/custom.css') %}
    {% do assets.addCss('theme://css/line-awesome.min.css') %}
{% endblock %}

{% 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 %}

</head>
<body id="top" class="{% block body_classes %}{{ body_classes }}{% endblock %}">
    <div id="page-wrapper">
    {% block header %}
        <section id="header" class="section">
            <section class="container {{ grid_size }}">
                <nav class="navbar">
                    <section class="navbar-section logo">
                        {% include 'partials/logo.html.twig' %}
                    </section>
                    <section class="navbar-section desktop-menu">

                        <nav class="dropmenu animated">
                        {% block header_navigation %}
                            {% include 'partials/navigation.html.twig' %}
                        {% endblock %}
                        </nav>

                        {% if config.plugins.login.enabled and grav.user.username %}
                            <span class="login-status-wrapper"><i class="fa fa-user"></i> {% include 'partials/login-status.html.twig' %}</span>
                        {% endif %}

                    </section>
                </nav>
            </section>
        </section>
        <div class="mobile-menu">
            <div class="button_container" id="toggle">
                <span class="top"></span>
                <span class="middle"></span>
                <span class="bottom"></span>
            </div>
        </div>
    {% endblock %}

    {% block hero %}{% endblock %}

        <section id="start">
        {% block body %}
            <section id="body-wrapper" class="section">
                <section class="container {{ grid_size }}">
                    {% block messages %}
                        {% include 'partials/messages.html.twig' ignore missing %}
                    {% endblock %}
                    {{ block('content_surround') }}
                </section>
            </section>
        {% endblock %}
        </section>

    </div>

    {% block footer %}
        {% include 'partials/footer.html.twig' %}
    {% endblock %}

    {% block mobile %}
    <div class="mobile-container">
        <div class="overlay" id="overlay">
            <div class="mobile-logo">
                {% include 'partials/logo.html.twig' with {mobile: true} %}
            </div>
            <nav class="overlay-menu">
                {% include 'partials/navigation.html.twig' with {tree: true} %}
            </nav>
        </div>
    </div>
    {% endblock %}

{% block bottom %}
    {{ assets.js('bottom')|raw }}
{% endblock %}


</body>
</html>

The plugin should be enabled and when you visit a page in your browser , the source code should now show up

<script src="/system/assets/jquery/jquery-2.x.min.js"></script>

added.

It is loading at the top (which the plugin can be altered to load at the bottom of the page quite easily if still issues with featherlight - but lets leave that for another day)

grav-plugin-featherlight/README.md at master · getgrav/grav-plugin-featherlight (github.com)

The ability to target images directly with featherlite injection, may be crucial in getting a faster lcp, where the way you have it set up, could be trying to inject every image it comes across as featherlight, which again would slow things down.

Now we need to see how you are implenting how you are using featherlight, as the plugin allows a simpler way of using featherlight if you are using page media

 `![Sample Image](sample-image.jpg?lightbox=1024&cropResize=200,200)`

As you can see, you can access the lightbox and settings with the ?lightbox=1024&cropResize=200,200

With you using the script, I presume your pages, will have a more complex way of it calling the images and using lightbox features.

Also with this plugin you can also speed up page loads by turning the script off loading on pages where you are not using it by editing the page.yaml, also provide overides to the plugin settings

---
title: Sample Code with Featherlight disabled
featherlight:
    active: false
---

@nicolas, Before you download a fresh Grav and start recreating your site…

  • Judging from the base.html.twig template it can not be concluded that FeatherLight is not being used. Access to your website might therefor provide more information.
    It might be it is not being loaded, but that can not be concluded based on the template.
    • Plugin FeatherLight adds its assets from PHP and not from Twig templates.
    • If the plugin has not been loaded, I suspect you would have noticed it…

Some more remarks:

  • The template is not the right place to start analysing your site. The generated HTML would be a better place to start with, because it is the result of both PHP and Twig.
    Then, when conclusions can be drawn, you go back to Twig or PHP to make any changes.
  • As said, it looks like jQuery is being loaded twice.
  • There is something peculiar with how jQuery is loaded by your code.
    <script src="//code.jquery.com/jquery-latest.js"></script>
    
    • The url code.jquery.com/jquery-latest.js points to a version 1.11.1 of jQuery. The latest is v3.6.0 …
  • I cannot judge whether the FeatherLight plugin is being loaded on the page, nor how the images are being loaded on which the FeatherLight plugin, or the downloaded original featherlight.js should react.
    However both libraries require different attributes on the generated <img> tags. Which means that if you adapted the image tags for the original FeatherLight library, then FeatherLight plugin wouldn’t recognise the tags. If you generated the image tags for the Featherlight plugin, then the original FeatherLight lib won’t recognise the images.
  • The original FeatherLight css and js files are being loaded on every page.
    Are you using FeatherLight on every page, or on just a single or few pages? If the latter I would advise to only set active: true on pages that do require FeatherLight.
  • What has been the rationale to manually load the original FeatherLight css and js?

Back to your original question about featherlight.min.css and PageSpeed…

  • There is no one-size-fits-all approach on how asses should be loaded.
  • If done properly, it requires thought, time, effort and testing.
  • The approach taken depends on the file being loaded: Is it critical or not, what is its functionality?
    • Which means nothing can be said about /assets/5ce715a4e759ae61f0a5e1c339fd3b24.css?g-b59e8687 and
      /assets/eb61294d9675af23985a9eb6d98cda25.js?g-b59e8687 without knowing their contents.

I don’t know what was going on in my mind when I hard coded the FeatherLIght css and js in the base template. Must have had a (very wrong) rationale, but can’t remember what it was.

Now those lines are gone, the plugin is working fine, and the featherlight.min.css is no longer delaying rendering according to Google Page Speed Insights.

I also followed your advice and set `featherlight active to false in the header of all pages not using the plugin.

Regarding the other asset files, I’ll try to find out more about them and what they do.

Thanks again for your help, your diagnosis of the problem (caused by stupid me) was spot on.

@nicolas, Glad the issue has been resolved.

You said:

I also followed your advice and set `featherlight active to false in the header of all pages not using the plugin.

If there are more page not using FeatherLight then pages that do use FeatherLight, I would suggest to set active: false in user/config/plugins/featherlight.yaml and only activate the plugin on pages that need the plugin. See the README for more info.

I’ll try to find out more about them and what they do

By using the following setting in user/config/system.yaml the css and js files will not be bundled and you will see each loaded file separately in the generated HTML.

assets: 
  css_pipeline: false 
  js_pipeline: false

@pamtbaau Ok. I set the pipelines to false and now can see which are the the files being loaded on the head section. For most pages, they are something like:

/user/plugins/breadcrumbs/css/breadcrumbs.css?g-d525549d
/user/plugins/form/assets/form-styles.css?g-d525549d
/user/plugins/image-captions/css/image-captions.css?g-d525549d
/user/plugins/simplesearch/css/simplesearch.css?g-d525549d
/user/plugins/login/css/login.css?g-d525549d" type="text/css
/user/plugins/featherlight/css/featherlight.min.css?g-d525549d
/user/plugins/login/css/login.css?g-d525549d
/user/themes/mytheme/css-compiled/spectre.css?g-d525549d
/user/themes/mytheme/css-compiled/theme.css?g-d525549d
/user/themes/mytheme/css/custom.css?g-d525549d
/user/themes/quark/css/line-awesome.min.css?g-d525549d
/user/plugins/form/assets/form-styles.css?g-d525549d
/user/plugins/shortcode-ui/css/ui-accordion.css?g-d525549d
/system/assets/jquery/jquery-2.x.min.js?g-d525549d
/user/plugins/featherlight/js/featherlight.min.js?g-d525549d

, and for any given page Google Pagespeed Insights lists some of them as impacting rendering (the list changes from page to page, but jquery-2.x.min.js is always present).

Most of these files should not impact the visual aspect of the first paint, so I would like to postpone their loading. I did read the Asset Manager documentation but am unsure how to proceed. Can you give me some extra pointers? This is the current asset section on the base.html.twig template:

 {% block stylesheets %}
    {% do assets.addCss('theme://css-compiled/spectre'~compress) %}
    {% if theme_var('spectre.exp') %}{% do assets.addCss('theme://css-compiled/spectre-exp'~compress)  %}{% endif %}
    {% if theme_var('spectre.icons') %}{%  do assets.addCss('theme://css-compiled/spectre-icons'~compress) %}{% endif %}
    {% do assets.addCss('theme://css-compiled/theme'~compress) %}
    {% do assets.addCss('theme://css/custom.css') %}
    {% do assets.addCss('theme://css/line-awesome.min.css') %}
{% endblock %}

{% 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 %}

@nicolas,

Some background:

  • To bundle or not to bundle, that’s the question…
    With the advent of HTTP/2, should assets still be bundled? Read HTTP/2: As web developer, What you need to change.
  • defer vs async
    Both load strategies start loading immediately in the background, but the main difference is that async scripts run as soon as they are loaded and do not preserve order, while defer scripts run at the end and in order of how they appear in the page.
    Use defer when scripts depend on each other. For example, jQuery must be loaded first before all others.
    See MDN: Script loading strategies

Asset manager:
Because of all the possible permutations the options provide, it makes it a bit hard to explain… I’ll pick a few scenarios:

  • Adding defer to bundled scripts:
    Add defer to output function for entire bundle:
    {{ assets.js('bottom', {loading: 'defer'}) | raw }}
    
    <script src="/assets/14866b56b6250aa9c2d7ce9b9fb2cae4.js" defer=""></script>
    
  • Adding defer to non-bundled Javascript file; output at bottom; positioned ‘after’ the pipelined files.
    {% do assets.addJs(
      '/path/to/javascript.js', 
      {   
        group: 'bottom',
        position: 'after',
        loading: 'defer', 
      }
    ) %}
    
    <script src="/path/to/javascript.js" defer=""></script>
    
    Use this for example when extra Javascript is required on a specific page. E.g. contact page.
  • Loading non-critical css ‘async’
    Separating your css into critical and non-critical isn’t an easy task. You will have to do it manually (Chrome can show you what is critical and what is not) or use available tools. You could give https://uncss-online.com/ a try, or update your workflow with offline UnCSS.
    Once you have separated the two, you load the critical css inline in the <head> of the page and the non-critical using the following Twig at the bottom of the page:
    {{ assets.css('bottom', {
       'rel': 'preload',
       'as':'style',
       'onload': "this.onload=null;this.rel='stylesheet'"
     }) | raw }}
    <noscript>{{ assets.css('bottom') | raw }}</noscript>
    
    Result:
    <link rel="preload" href="styles.css" as="style" 
       onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="styles.css"></noscript>
    
    For more information, read Defer non-critical css

Have fun with it…

1 Like