Responsive Imaging and Dynamic Media done Right — Part IV

Achim Koch
6 min readJan 14, 2024

--

Implementing a Custom Image Loader

Today’s websites are experienced on a multitude of different devices. Not only do screens vary in sizes, but they also vary in resolution, orientation, and aspect ratio.

Created with Adobe Firefly

Having different devices requires us to provide images in different resolutions and formats. HTML5 gives some clues how this can be achieved. But the standard is quite complex and it’s easy to make errors.

Here I propose a custom different low maintenance / high performance solution.

This is Part VI of a four-part series. The series is divided as follows:

Part I: What is Responsive Imaging? Why we need it and what use cases it has.

Part II: How can we use AEM Dynamic Media, to increase page loading performance?

Part III: How to implement Dynamic Media with HTML 5?

Part IV: Beyond HTML 5: Shrinking the page load time with a custom image loader.

An alternative approach

The examples from the Core Component Library (see Part III) illustrate, that the matter is quite complex, and it is not easy to get it right for all components and all use cases.

If you’ve read the last part of this article series, you know why:

Even though the HTML5 features look easy on paper, operationalizing them into re-usable components can be quite an undertaking. There are so many use cases and even more edge-cases.

HTML5 and CSS3 provide powerful tools for selecting responsive images. And Dynamic Media is a great companion to produce the renditions on the fly.

Usually I would say: If you can do something with standard HTML, do not re-invent the wheel with JavaScript.

However, I feel there should be another way. Why do I have to tell the browser how large an image is, when it can figure this out on its own after rendering the DOM. We could save some precious development time by implementing a smarter, more general solution that does not require so much individual tweaking.

I think that the people from the W3C had static websites and dynamic webapps in mind when they were designing the markup for responsive images. Here you would know the possible dimensions of images in advance, and you would upload images in the sizes required.

CMS projects usually have different premises:

  • We do not upload an image X to use on page P. Instead, you’d pick an asset from shared global asset library. The difference: Assets from the library are general purpose and probably not in the format you require. Thus, we need to transform them — usually crop and scale.
  • We are not in full control where the components are placed. This is decided by the content authors at runtime.
  • We re-use the same components in multiple contexts. The same component can be used on a single column or as in row of 2, 3, or 4 components — of course making the individual component much smaller of course. AEM components do not know that they are rendered in multiple columns so a component itself does not know what srcset it should render.
  • We can leverage the capabilities of Dynamic Media.

I believe that this context can justify re-inventing the wheel.

Here is the idea for a custom pixel perfect image loader:

  1. The horizontal layout is defined completely by the grid.
  2. Images always fit into the grid and cannot push other elements to the side. The width is always 100%.
  3. Images can push other elements down, though.
  4. A component “knows” if it requires art direction and what aspect ratios are required at which breakpoints. This is encoded into data attributes on the <img> tag:
    <img data-image-formats="3by1 600, 3by2 800, 1by1" data-image-path"..." data-image-smart>
    By default, we use a 1:1 aspect ratio, from 800px and smaller 3by2 and below 600px 3by1
  5. We register events when the page is ready and when it gets resized in JavaScript.
  6. The page renders.
  7. The event handler fires.
  8. We look for all images marked as data-image-smart
  9. a) We get the physical width dimensions from the image,
    b) calculate the physical width (wid) = logical width (lw) x device pixel ratio (dpr)
    c) lookup the required aspect ratio (ar) from the data-image-formats attribute given the current viewport width (vw) as index.
    d) calculate the physical height (hei) = ph / ar
    e) build a Dynamic Media URL with the calculated parameters

Example:

<img data-image-formats="3by1 600, 3by2 800, 1by1" 
data-image-path="https://techsupporteu.scene7.com/is \
/image/AEMEMEAPractice/AdobeStock_636568731"
data-image-smart>
  • Assume the Viewport Width is vw = 700
  • The algorithm picks the Aspect Ratio ar = 3by2
  • From introspection, we know the available Logical Width lw = 134
  • The monitor has a Device Pixel Ratio dpr = 2
  • This results in a Physical Width wid = 268
  • We calculate the Physical Height: hei=wid/ar=268/(3/2)=178

Thus, the URL for the image is:

https://...scene7.com/is/image/AEMEMEAPractice/AdobeStock_636568731:3by2
?wid=268&hei=178

Dynamic Media is so fast that we can calculate pixel-perfect renditions saving a few hundreds of kilobytes we do not have to transmit.

If we are still concerned about caching, we could round the widand hei parameters in the request to decrease the number of possible URLs and thus increase the probable hit rate.

You can compare the two approaches here:

HTML5 + Dynamic Media:

https://ackoch.github.io/image-zoo/005-all-combined-html5.html

Custom Image Loader + Dynamic Media:

https://ackoch.github.io/image-zoo/006-pixel-perfect.html

The Cumulative Layout Shift

Wait! If we can load the images only after the page was rendered, does that mean that the page would be re-rendered a second time, and the content will shift around? And wouldn’t that result in a bad user experience and a low Google Lighthouse score?

Short answer: No. In our case, images are not influencing the layout: They fit into the space defined by the CSS, only and do not “push” against their boundaries.

The width always is defined via CSS using:

width: 100%;

And the height is defined proportionally by the component:

aspect-ratio: 3/1;

This, in part, is facilitated by Dynamic Media, too: While without Dynamic Media you can use the aspect-ratio CSS directive, with Dynamic Media you can also define a cropping that is aligned to that ratio.

Comparison and Conclusion

The custom image loader can load images only after the page was rendered the first time. Loading of the images starts a bit later compared to the HTML5 approach. However, the images then are then pixel-perfect and thus smaller in file size. The image loader can catch up and eventually be faster than the HTML5 loader — depending on bandwidth, image size and device pixel ratio.

Also, the custom loader shines where you want to re-use components in multiple contexts. I.e. you can more easily use the same Image component in the main area of a page (say, at 66vw) and in the margin (at 34%) — without having to manually configure the media queries.

The Lighthouse Score of both variations are comparable, but this was a very simple page. On real-life pages there might be a difference. Individual testing is advised.

The custom loader can be a jumpstart, to easily implement a re-usable general-purpose image component in an early project stage. You could decide later whether to refactor individual components on a case-by-case base — if time and budget allows.

I can imagine that you would be using the more “classic” HTML5 approach for images above the fold. I.e. it could make sense to implement the typical Hero Banner component using HTML5 to gain a low LCP (Largest Contentful Paint) value.

For all other components, you can then use the custom loader to reduce the overall weight of the page.

I could observe a significant difference in the total amount of bytes transferred:

  • The HTML5 version totaled in 866kB images,
  • the custom loader only loaded 339kB worth of image data.

And yes — the HTML5 version could be further optimized by introducing finer grained steps candidates in the srcsetand the width hints in the sizes attributes could also be tweaked to be more accurate.

But projects where you have the time to do performance fine tuning are quite rare. There are always more important things to do.

This is not necessary for the custom image loader. It always gets the best results — without the need of manual optimizing individual components.

I find this approach intriguing and very promising.

Plus: Using a custom loader, is the only way to implement the dynamic cropping case on the server side. I leave that to you to implement.

Until next time

-achim

Acknowledgements

Thanks to my dear colleagues Eryk Lagun and Rob Freeman for inspiration, proofreading, and fact-checking.

References

[1] https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images

[2] https://experienceleague.adobe.com/docs/dynamic-media-developer-resources/image-serving-api/image-serving-api/http-protocol-reference/command-reference/c-command-reference.html

[3] https://experienceleague.adobe.com/docs/experience-manager-learn/assets/dynamic-media/images/smart-crop-feature-video-use.html

[4] https://experienceleague.adobe.com/docs/experience-manager-cloud-service/content/assets/dynamicmedia/image-profiles.html

[5] https://github.com/ackoch/image-zoo/blob/master/README.md

[6] https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset

[7] https://wcm.io/handler/media/usage.html

--

--

Achim Koch

Principal Architect at Adobe Consulting // MSc Computer Science // Former developer - now business consultant & architect// 18 years AEM experience