Lazy loading images with collective.lazysizes and Diazo to improve site performance

The Problem

For the past couple of months, I've been developing a content riched Plone site with a lot of high-quality images. The performance (page speed) was pretty poor. The homepage, for instance, took 12s to load to load at least 18 high-quality photos (insane, right?). First contentful paint took around 3.8s and First meaningful paint 4.8s. I literally could tell by the analytics that the website is losing mobile visitors after 8s of waiting on the page to fully load, especially since the bounce route for mobile users is 100% and average time on site is 37s.

The stats for other devices are pretty great, so yea, performance when rendering a high-quality image based websites even with the large and preview image scaling.

The Solution

I installed the collective.lazysizes add-on and use the following Diazo rules to force lazy loading for all images within the content area of Plone that aren't scaled to an icon, mini and thumbnail sizes. It transforms images in the Plone content area before it is placed into the theme. All images' src is set to the thumbnail version if applicable and set the actual image that needs to be loaded to the data-src attribute. Now, the fully loaded time is down to 4s, but I need to get it under 2 seconds if not 1.

<?xml version="1.0" encoding="UTF-8"?>
<rules
    xmlns="http://namespaces.plone.org/diazo"
    xmlns:css="http://namespaces.plone.org/diazo/css"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
    xmlns:xi="http://www.w3.org/2001/XInclude">
    
    <!-- Transform images to support lazy loading -->
    <rules css:content="#content">
        <after css:theme-children="html head">
            <!-- Default height for preloader images -->
            <style>
                .lazyload {
                    background: #fff;
                    background-size: cover;
                    background-position: center;
                    background-position: center;
                    min-height: 100px;
                }
                
            </style>
        </after>
        <!-- Add lazyload to Plone preview scaled images  -->
        <replace css:content="img[src$='/@@images/image/preview']:not(.lazyload)">
            <xsl:copy>
                <xsl:copy-of select="@*" />
                <xsl:variable name="classlist" select="./@class"/>
                <xsl:attribute name="class">lazyload lazyload-preview <xsl:value-of select="$classlist" /></xsl:attribute>
                <xsl:variable name="src" select="./@src"/>
                <!-- Set the thumbnail scaled image as the src and lazy load the preview scaled image -->
                <xsl:attribute name="src"><xsl:value-of select="substring-before($src, '/preview')" />/thumb</xsl:attribute>
                <xsl:attribute name="data-src"><xsl:value-of select="$src" /></xsl:attribute>
                <xsl:apply-templates />
            </xsl:copy>
        </replace>
        <!-- Add lazyload to Plone large scaled images  -->
        <replace css:content="img[src$='/@@images/image/large']:not(.lazyload)">
            <xsl:copy>
                <xsl:copy-of select="@*" />
                <xsl:variable name="classlist" select="./@class"/>
                <xsl:attribute name="class">lazyload <xsl:value-of select="$classlist" /></xsl:attribute>
                <xsl:variable name="src" select="./@src"/>
                <!-- Set the thumbnail scaled image as the src and lazy load the large scaled image -->
                <xsl:attribute name="src"><xsl:value-of select="substring-before($src, '/large')" />/thumb</xsl:attribute>
                <xsl:attribute name="data-src"><xsl:value-of select="$src" /></xsl:attribute>
                <xsl:apply-templates />
            </xsl:copy>
        </replace>
        <!-- Set the thumbnail scaled image as the src and lazy load the original image that doesn't use Plone image scaling -->
        <replace css:content="img[src$='/@@images/']:not([src*='/@@images/image/']):not(.lazyload)">
            <xsl:copy>
                <xsl:copy-of select="@*" />
                <xsl:variable name="classlist" select="./@class"/>
                <xsl:attribute name="class">lazyload <xsl:value-of select="$classlist" /></xsl:attribute>
                <xsl:variable name="src" select="./@src"/>
                <!-- Set the thumbnail scaled image as the src and lazy load the original image -->
                <xsl:attribute name="src"><xsl:value-of select="substring-before($src, '/large')" />/thumb</xsl:attribute>
                <xsl:attribute name="data-src"><xsl:value-of select="$src" /></xsl:attribute>
                <xsl:apply-templates />
            </xsl:copy>
        </replace>
        <!-- Add lazyload images that Plone image scaling is not handling  -->
        <replace css:content="img:not([src*='/@@images/']):not(.lazyload)">
            <xsl:copy>
                <xsl:copy-of select="@*" />
                <xsl:variable name="classlist" select="./@class"/>
                <xsl:attribute name="class">lazyload <xsl:value-of select="$classlist" /></xsl:attribute>
                <xsl:variable name="src" select="./@src"/>
                 <!--Set the thumbnail scaled image as the src and lazy load the preview scaled image -->
                <xsl:attribute name="src">img/preloader.gif</xsl:attribute>
                <xsl:attribute name="data-src"><xsl:value-of select="$src" /></xsl:attribute>
                <xsl:apply-templates />
            </xsl:copy>
        </replace>
    </rules>

</rules>

Other consideration for improving performance

  • Allow Plone to detect if the user agent is a mobile device and serve a preview version of the image instead of the large image. This can be done in the following ways:
    • add a custom image handler for autoscaling
    • use Diazo to swap the large photo to the preview version of the image.
  • Implement infinite scrolling so that I can load 6 images first, pre-load next 6 and load the others on upon scrolling. I've noticed that there's a collective.infinitescroll addon, but it seems outdated.
  • Generate a mobile-only version of the site using Gatsby or Volto
  • PWA version of the site using Volto or Gatsby
  • AMP version of the site using collective.behavior.amp
  • Service Workers for caching images
  • Implement Image optimization with CDN‎ (https://cloudinary.com/)

Are there any other technique I can use to improve website performance? I'm open to suggestions.

2 Likes

Since 5.1.5 you can configure javascripts for async/defered loading.
Made a huge difference especially on mobile devices.

To make it work with the existing bundles, I split up the Plone bundle in a plone-base with the absolute necessities and the plone bundle with the rest. Depending on the setup you could split up the plone bundle even more.

This is my registry.xml so far:

<?xml version='1.0' encoding='UTF-8'?>
<registry xmlns:i18n="http://xml.zope.org/namespaces/i18n">

  <!-- Plone bundle resources -->
  <records prefix="plone.resources/plone-base"
            interface='Products.CMFPlone.interfaces.IResourceRegistry'>
      <value key="js">++plone++ploneresources/plone-base.js</value>
  </records>

  <!-- Bundles -->
  <records prefix="plone.bundles/plone-base"
            interface='Products.CMFPlone.interfaces.IBundleRegistry'>
    <value key="resources">
      <element>plone-base</element>
    </value>
    <value key="merge_with">default</value>
    <value key="enabled">True</value>
    <value key="jscompilation">++plone++ploneresources/plone-base-compiled.min.js</value>
    <value key="last_compilation">2018-10-08 18:00:00</value>
  </records>

  <records prefix="plone.bundles/plone"
            interface='Products.CMFPlone.interfaces.IBundleRegistry'>
    <value key="depends">plone-base</value>
    <value key="load_async">True</value>
    <value key="merge_with"></value>
    <value key="resources">
      <element>plone</element>
    </value>
    <value key="enabled">True</value>
    <value key="jscompilation">++plone++ploneresources/plone-compiled.min.js</value>
    <value key="csscompilation">++plone++ploneresources/plone-compiled.css</value>
    <value key="last_compilation">2018-10-08 12:00:00</value>
    <value key="stub_js_modules">
      <element>jquery</element>
      <element>pat-registry</element>
      <element>mockup-patterns-base</element>
    </value>
  </records>

</registry>

I also registered a custom ploneresource directory, where i put the custom plone-base.js and plone.js

This is what my plone-base.js defines as required:

require([
  'jquery',
  'pat-registry',
  'mockup-patterns-base',
], function($, registry, Base) {

And this is what's left in the plone.js

require([
  'mockup-patterns-select2',
  'mockup-patterns-pickadate',
  'mockup-patterns-autotoc',
  'mockup-patterns-cookietrigger',
  'mockup-patterns-formunloadalert',
  'mockup-patterns-preventdoublesubmit',
  'mockup-patterns-formautofocus',
  'mockup-patterns-markspeciallinks',
  'mockup-patterns-modal',
  'mockup-patterns-livesearch',
  'mockup-patterns-contentloader',
  'mockup-patterns-moment',
  'bootstrap-dropdown',
  'bootstrap-collapse',
  'bootstrap-tooltip',
], function() {

I'll continue to test this setup with other projects and will probably do a pull request then for Plone 5.2 at some point.
Feedback is very welcome!

p.s. if you need resources and bundles only on certain pages, loading them on request is also a good idea :wink:

3 Likes

Also quite some improvement on the size of moment.js in Plone 5.2 https://github.com/plone/Products.CMFPlone/issues/1779 thanx to @davilima6 at #alpinecitysprint

2 Likes

Thanks a lot. I actually didn't know this. I will test this approach and observe the perfromance using Google Lighthouse

1 Like