PreviousNext: Optimise Your Page Loads with Lazy Loading Javascript
How to optimise your progressively decoupled Drupal frontend with the new Intersection Observer API.
by rikki.bochow / 14 February 2023Read on for front-end tips backed by code you can use! You can also watch the full video at the end of this post.
What is Lazy Loading?
Lazy Loading isn’t a new concept; however, let's quickly recap what we know about it.
“Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It’s a way to shorten the length of the critical rendering path, which translates into reduced page load times.” Mozilla Developer Network.
Why is lazy loading significant?
Good performance offers many benefits.
How often have you given up and closed a web page because it took too long to load? Especially when you’re on your mobile or experiencing a poor connection. It’s easy to forget that not everyone has regular access to fast, reliable internet connections or devices.
There are plenty of benefits to lazy loading:
- Improved initial page load times
- Better perceived performance
- Decreased data usage
- Positive impact on SEO rankings
- Higher conversion rates
- Improved user experience
If you’d like to dive deeper into these metrics, check out Jake Archibald’s F1 series for before and after speed tests.
The basic principles of lazy loading
Stylesheets
Because stylesheet files are render-blocking, we need to determine what is critical or above-the-fold CSS and inline it. We then defer the rest with Javascript attribute swapping and use Drupal’s Library system to reduce unused CSS.
Javascript
We also need to determine our critical Javascript and consider inlining it. Definitely defer any JS that isn’t critical and load asynchronously where applicable.
ES6 modules are deferred by default and supported by modern browsers, so can be combined with code splitting. Again, we can use Drupal’s Library system to reduce unused Javascript.
Media
Media can slow down pages too. That’s why the loading attribute is gaining support in both Drupal and browsers.
has the most comprehensive support, so you should avoid using Javascript for these and also avoid lazy loading images that are likely to be above the fold.
Always put height and width attributes to prevent layout shift and use the responsive images module.
But we want to lazy load more!
And with the Intersection Observer API, we can.
So what is the Intersection Observer API?
“The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.” Mozilla Developer Network
Simply put, it means: “tell me when an element has scrolled into or out of view”.
This API isn’t new (having been around for roughly five years); however, the recent demise of Internet Explorer now means it has full browser support.
iframes
Let's take iframe in Firefox as an example. If you have a lot of iframes (because, let’s face it, sometimes you just need to) and you’d like to save your Firefox users from long pages loads, then you can use oembed_lazyload (which uses Intersection Observer).
Alternatively, you can write a very simple Intersection Observer to populate the iframe src
attribute from the data-src
value.
In the video below, I ran through a basic introduction to the Intersection Observer API. I used all the default settings and checked the isIntersecting
property, swapping the attributes, and then Unobserving the iframe (so it didn’t trigger again when it scrolled into the viewport subsequent times).
//
const lazyLoadIframe = iframe => {
const obs = new IntersectionObserver(items => {
items.forEach(({ isIntersecting }) => {
if (isIntersecting) {
iframe.src = iframe.dataset.src
obs.unobserve(iframe)
}
})
})
obs.observe(iframe)
}
window.addEventListener('load', () => {
document.querySelectorAll('iframe').forEach(iframe => lazyLoadiFrame(iframe))
})
Javascript applications
We can expand on this idea of deferring assets that are below the fold and think about our progressively or partially decoupled projects.
In the following example, we’ll imagine a Drupal site with complex javascript applications embedded here and there. The apps are written in React, Vue or even Vanilla JS; they fetch data from Drupal as a JSON file and load their CSS. There may also be multiple apps on a page.
If we load these apps, as usual, we’ll load everything, including dependencies (JSON, CSS etc.) on the initial page load, regardless of whether we defer or async the javascript. It’s not render-blocking, but users who don’t scroll down to see the App are still downloading it.
Instead, we can combine the Intersection Observer with a Dynamic Import and truly defer loading all the apps’ resources until the mounting element is in the user's viewport.
In the below code example, the load()
function is only called upon intersection, so none of the apps’ dependencies are requested until the container scrolls into the viewport, significantly decreasing the initial page load time.
React example;
const lazyLoadApp = (
container,
componentPath,
props = {},
callback = () => []
) => {
const load = async () => Promise.all([
import("react-dom"),
import("react"),
import(componentPath),
]).then(([
{ render },
React,
{ default: Component }
]) =>
render(, container, callback)
)
const obs = new IntersectionObserver(items => {
items.forEach(({ isIntersecting }) => {
if (isIntersecting) {
load()
obs.unobserve(container)
}
})
})
obs.observe(container)
}
We use Promise.all to ensure all of our dependencies are met, and then we destructure what we need from those dependencies and render our app.
After this happens, we unobserve the container.
You can also adjust the load()
function as needed, i.e. import Vue and createApp instead–whatever your setup requires.
Example for Vue 3;
const load = async () => Promise.all([
import("vue"),
import(componentPath),
]).then(([
{ createApp },
{ default: Component }
]) => {
const app = createApp(Component, props)
callback(app)
app.mount(container)
})
Example for React 18;
const load = async () => Promise.all([
import("react-dom/client"),
import("react"),
import(componentPath),
]).then(([
{ createRoot },
React,
{ default: Component }
]) => {
const root = createRoot(container)
root.render()
// callback function moves into Component.
})
Then usage would be something like:
//
window.addEventListener('load', () => {
document.querySelectorAll('[data-app-example]').forEach(container =>
lazyLoadApp(
container,
'./path-to/component.jsx',
{
title: container.dataset.title,
id: container.id,
},
() => container.setAttribute('data-mounted', true)
))
})
Here's the breakdown;
- pass in the container (div to render/mount to)
- the path to a specific component
- any props needed (maybe simple data attributes or
drupalSettings
) - even a callback function after mounting has occurred
Accessibility considerations
We need to remember that the WCAG have findability rules for hidden content. Adding a heading (maybe even a description) inside the container with a button that triggers the load function might help with this. They get replaced by the rendered app but are available for screen readers and keyboard navigation.
You’ll also need to consider the following:
- How important is the content in the JS app?
- Is the content shown elsewhere, or can it be?
- Is the app itself accessible?
- What will happen if JS isn’t enabled?
UX considerations
The unmounted container is also a good use case for Skeleton User Interfaces. Start by giving the container a grey background with a rough height/width for the rendered app, then add a loading animation, and you’ll help reduce the “jump” of the suddenly rendered app whilst also improving the perceived performance.
This approach is also a great way to prevent Layout Shift issues. Also, remember to notify the user if something has failed to load.
You can tweak the Intersection Observer’s settings to increase or decrease the point of intersection, allowing for sticky headers, for example.
What else can we do with the Intersection Observer?
Other use cases for the Intersection Observer include:
- Scroll-spy type components that update sticky anchor navigation
- Animations that only start once in the viewport or should stop once outside the viewport.
- Infinite scrolling pagination
- Pausing videos when scrolled passed
- Bookmarking where an article has been read to
Let’s not forget that the post-Internet Explorer world is full of Observers, including:
- Mutation Observer. For DOM changes, such as attributes or markup/content injection (i.e. knowing when one app has rendered).
- Resize Observer. A more performant version of the window.resize event for element dimension changes.
These all follow the same Observe/Unobserve pattern, so once you learn one, you won’t be able to stop yourself from using them all!
Talking Drupal: Talking Drupal #386 - Drupal Vs Wordpress
Today we are talking about Drupal & Wordpress with Maciej Palmowski.
For show notes visit: www.talkingDrupal.com/386
Topics- What is Wordpress
- Do you have Drupal experience
- Pros of Drupal over Wordpress
- Pros of Wordpress over Drupal
- Selecting a CMS
- What sites don’t work well with Wordpress
- What sites don’t work well with Drupal
- Headless in Wordpress
- Will Wordpress use Symfony?
- Who wins?
- Talking Drupal #315 - Communities: Wordpress Vs Drupal
- Talking Drupal #16 - Wordpress Vs Drupal
- Yoast for Drupal (Real-time SEO)
- 295 31 Days of Migration
- 352 Migration
- 381 A Modular Web
- PHP Compatibility
- Statamic
- Sanity Io
- Cassidy Williams Talk
- Matt Mullenweg on licensing derivative works
- Code and Coffee
- Security report
Maciek Palmowski - maciekpalmowski.dev @palmiak_fp
HostsNic Laflin - www.nLighteneddevelopment.com @nicxvan John Picozzi - www.epam.com @johnpicozzi Katherine Druckman - katherinedruckman.com @katherined
MOTW CorrespondentMartin Anderson-Clutz - @mandclu WordPress Migrate Supports migrating Wordpress exports into Drupal, including posts, pages, attachments, tags, and categories.
The Drop Times: Remembering Rachel Olivero
Four years have passed since Rachel Olivero left us. Drupal rever her by naming the current default frontend theme, 'Olivero.' Drupal is now WCAG compliant, and our added support for ATAG 2.0 proves that we live by her legacy. Drupal encourages the use of assistive technologies. It is all the more important for a person with vision disabilities. Accessibility, Diversity, and Inclusion are the cardinal ideals we follow, and Rachel was involved in all these areas. Her memorial in Drupal.org recalls that as a person who was blind, transgender, and a lesbian, Rachel understood a lot about the importance of diversity. She passed away on February 03, 2019. Let us keep on her vision forward, keeping Drupal open and accessible for everyone.
Last week, TheDropTimes (TDT) ran two interviews with the organizers of Florida DrupalCamp. AmyJune Hineline (volkswagenchick) emphasized the importance of accessibility in her interview, stating accessibility is not an option or an add-on; it should be a default. In the second interview, Adam Varn (hotsaucedesign) opines that the success of Olivero and Claro as modern, accessible themes shows that Drupal still has plenty to offer a themer interested in Drupal, and with the new Starterkit project coming to stabilization in Drupal 10, there will be even more tools.
Drupal 10 Development Cookbook got released last week, thanks to Matt Glaman, Kevin Quillen, and Justin Cornell. Drupal Association announced Rosa Ordinana and Lynne Capozzi joining as its new board members. As part of diversity and inclusion, the association is aligning with Black History Month. On Monday, 21 February 2021, at 10:00 am EST (15:00 hrs UTC), Joi Garrett, will introduce the new 'Black in Drupal' program.
Scroll further down for the rest of the stories from the past week.
Sincerely,
Sebin A. Jacob
Editor-in-Chief,
TheDropTimes
The Drop Times: Upcoming conversation with founder of DXPR; Jurriaan Roelofs
Morpht: In 2023, Why Are Most Websites Still so Dim-Witted?
Cameron Eagans: Seeking feedback on the dev version of Composer Patches
The Drop Times: Don't Exert Too Much Effort on Headless: Adam Varn | FLDC
Matt Glaman: The Drupal 10 Development Cookbook is out!
The Drupal 10 Development Cookbook is officially out! Special thanks to Kevin Quillen for his amazing assistance in writing the book. And my good friend Justin Cornell as a technical reviewer. You can order the print or ebook on Amazon (affiliate link) or Packt! This is technically the 3rd edition of the book. The first edition came out with the Drupal 8.0 release, and the second was around Drupal 8.5.0. So this covers everything that came with Drupal 9 and was added for Drupal 10!
We have 450 pages and fourteen chapters with walkthroughs and breakdowns for development with Drupal 10.