June 29, 2017

Lazyloading Flexslider in Drupal

Lazyloading Flexslider in Drupal
A recent website performance audit for one of our clients lead us to look into reducing the number of images loaded on certain pages, a lot of which were being used in Flexslider slideshows. If we could load only the first few images in a slideshow when the page loaded, and then only load the rest when we actually needed them we would save a fair bit of time on that initial, non-cached load. Unfortunately, this wasn't a behaviour provided by Flexslider out of the box, so we would have to develop our own solution!

Our specific situation involved a main Flexslider slideshow with a smaller navigation slideshow below that displayed 5 images, all created using the Flexslider Drupal module. We only implemented lazyloading on the main slideshow as the images in the nav slideshow were very small.

The basic idea of "lazyloading" the images in the Flexslider slideshow is a pretty straight forward one: for the images we don't want to load, we would move the image path out of the "src" attribute and place it in a "data-src" attribute on the element instead. Then, when you wanted to load an image, you would just swap the value in the "data-src" attribute into the "src" attribute. There were essentially two steps to implementing this: first we needed to modify the output of the Flexlider module so the image path was in this new location, and second we needed to perform the actual swapping of the path as needed.

Modifying Flexslider output

This was fairly straightforward as we change how the image path was being output just by tweaking the theme_flexslider_list function. Adding to the default function, we would first check if the slider was a nav or not,

// We only lazy load the main slideshows, not thumbnails.
$lazy_load = (!isset($vars['settings']['optionset_overrides']['asNavFor'])) ? TRUE : FALSE;

and if it wasn't, we would simply modify how the element was formatted

foreach ($items as $i => $item) {
  // Only load the first 5 and the last image. The rest will be lazy loaded.
  if ($lazy_load && ($i > 4) && ($i < (count($items) -1))) {
    preg_match('/src="([^"]*)"/i', $item['slide'], $src);
    $item['slide'] = str_replace($src[0], 'data-src="' . $src[1] . '" src=""', $item['slide']);
  }
}

We're loading the first 5 images as those will all be directly accessible via the nav slider in our case. The last image needs to be loaded for Flexslider to work properly and you obviously need the first image to be loaded as well.

Loading the images

At this point we were extremely lucky that the Flexslider module developer has included some JS events that we could hook our behaviour into to load the images as we needed. The event we'll be making use of is the "before" event which fires right before any movement happens in the slider. We will need to load images whenever someone advances the main slideshow as-well-as when they adjust the nav.

First we'll deal with the more straightforward movement of the main slider. Whenever the slideshow is moved forward or backward, we will load one slide in the given direction:

$(this).on('before', function(event) {
  var slider = $(event.target).data('flexslider');

  if (!(slider.asNav)) {
    var slides = [];
  
    if (slider.direction == 'next') {
      slides = [$(slider.slides).find('[data-src]')[0]];
    }
    else if (slider.direction == 'prev') {
      slides = [$(slider.slides).find('[data-src]').last()];
    }
  
    $.each(slides, function(index, slide) {
      $(slide).prop('src', $(slide).data('src'))
        .removeAttr('data-src');
    });
  }
}

Now, there is one gotcha in this. In our case, if a user moves the slideshow backwards from while on the first slide, it loops back around to the last slide in the set. When this happens, we need to make sure the last 5 slides are loaded as each of them are suddenly reachable via the nav slideshow:

else if (slider.direction == 'prev') {
  // If we wrapped around from the first slide, we need to load
  // a full navigations worth of images.
  if (slider.animatingTo == slider.last) {
    slides = $(slider.slides).find('img').slice((slider.last -5), slider.last);
  }
  else {
    slides = [$(slider.slides).find('[data-src]').last()];
  }
}

Now we can deal with advancement of the nav slider. Of course, if you don't have a nav slider, you can completely ignore this part (and all the mentions of loading 5 images at a time above). Whenever the nav is moved in either direction, we need to make sure the 5 images it displays are loaded in case the user clicks one of them:

if (slider.asNav) {
  var slides = $('#' + slider[0].id.slice(0, -4) + ' .slides li').not('.clone');

  var shown = slider.visible;
  var index = slider.animatingTo * shown;

  // Load images in main slideshow that correspond to the images
  // just revealed in the thumbnails.
  for (var i = 0; i < shown; i++) {
    var $slide = $('img', slides[index + i]);

    if ($slide.attr('data-src')) {
      $slide.prop('src', $slide.data('src'));
      $slide.removeAttr('data-src');
    }
  }
}

 


 

And that's pretty much it for our lazyloading Flexslider! There is one final problem to fix and that is that since a lot of our slides are now hidden, they don't take up any space. This creates a problem when we wrap to the last slide from the first slide as Flexslider will miscalculate the amount of space it has to move to get to the last slide. This can easily be fixed however with some CSS:

.flexslider:first-child .slides img {
  padding-top: 1px;
}

 This will simply ensure that each image has an actual width for Flexslider to use in its calculations.

And that should give you a lazyloading Flexslider slideshow in Drupal! This will cut the images loaded on page load down to the first and last plus however many are in your nav. Of course, if you don’t have many more than are in your nav you won’t notice much of an improvement, but for large slideshows, the improvement is quite a bit!