Serving 7 million users more efficient images (part 1)
Note: this is part one in a two-part series where we'll conceptualize and then implement a deferred image loading solution. Posts go live on my Medium account first.
Millions of gamers around the world visit bethesda.net to learn about some of the most beloved franchises in gaming. I've spent far too many hours in the Fallout and Elder Scrolls universes over the last two decades, so helping fellow fans have a better experience when reading about and buying these games is an incredibly special opportunity.
Most of what visitors see is visual content like images and videos; as it should be, Bethesda Game Studios produces some beautiful games. However, this presents challenges from a performance perspective: the majority of the page content is composed of assets which have large sizes and can take a long time to download on anything but the fastest network connection.
Large amounts of visual content with big file sizes lead to lengthy load times on fallout.bethesda.net. As part of a broader effort at improving load times for the site, we wanted to dramatically reduce the portion of time it takes to load images. Our strategy for this was twofold:
- reduce the size of individual images using the API of our Content Management System, and
- defer the loading of high quality images until the user is scrolling near the location of that image in the document.
TIP: use the tools your current stack gives you to improve performance. Rarely will adding more libraries and modules be the right answer.
Using the Contentful Image API to reduce Image Sizes by 90% and Load Images Instantly
The CMS we use is Contentful, and they provide a powerful Images API that can be leveraged to optimize the loading of visual content. This means that content owners don't need to be aware of performance needs. They don't need to know about the most efficient way to upload images; whatever data they choose to upload, users will be served the most efficient data structure that their device is capable of. It also exposes an opportunity to keep individual developers "safe from themselves" - meaning, one can provide the team a component that always renders images efficiently instead of putting the weight of needing to know the Image API on the whole team.
The Contentful Image API is powerful: here's what it can do
Any Content Owner role in Contentful can upload images. At large companies, they're often working with high-quality assets and upload those directly. A page with the main content being high-quality JPGs can easily lead to request sizes in the dozens of megabytes. Contentful gives us a number of powerful tools to deal with that.
Manipulating image quality
The qimage parameter takes a number representing a percentage of the original image quality. You can use it to lower the image quality of a JPG by amounts too small to be seen by the user but which result in much smaller file sizes.
const yourContentfulSpace = ""
// Retrieve an image at 50% quality
yourContentfulSpace/mycat.jpg?&q=50
Using a more efficient file type
Some browsers allow you to use file formats more efficient than JPG, such as WebP. You can use the fm query param to specify other file formats. Note that you need to check the user's browser for support for your desired format.
// Retrieve an image as WebP format at 50% quality
yourContentfulSpace/mycat.jpg?&fm=webp&q=50
The Image API also allows you to modify image elements like height, width, and to adjust the focus area. Learn more here: https://www.contentful.com/developers/docs/references/images-api/#/introduction.
The IntersectionObserver
IntersectionObserver (IO) support began rolling out in major browsers in 2016. It allows you to asynchronously check whether a DOM element is visible to the user in a performant way. We'll use this to figure out whether an image is about to be in view for a user. By doing so, we can begin loading the high quality asset in the background. We'll orchestrate some magic behind the scenes, and the user? All they see is a page that loads quickly and beautifully.
So, how's it work? The IO API tries to replace the event handlers and loops of the past with a simple interface requiring just a few parameters to allow you to "watch" any given element.
You create a new IntersectionObserver with a callback function and options object.
const options = {
// What element do we want to observe?
root: document.querySelector('#myPicture'),
// How much space around the element do we want to watch? This is useful for "seeing" the element before it's actually in view, so we can start loading before the user sees the element
rootMargin: '350px',
// How much of the element and margin must be in view before running the callback function? Use the default of 0 to run as soon as any of the margin is visible.
threshold: 0
}
// We'll cover what callback to provide later
const observer = new IntersectionObserver(callback, options)
In our callback, that's where we want to start loading the high quality image. In React terms, that means we'll have a stateful component with one low quality image string as the default state, and we'll change state to "high quality" in our callback. In Part Two of this series, we'll implement this together.
Putting it all together: using the Contentful Image API and IntersectionObserver to lower file sizes by 90% and load images instantly
So, here's the practical portion you were looking for: the exact code you can use to reduce your file sizes by 90% and load images instantly (or, near instantly on very fast connections).
The experience we went for was similar to how Medium loads images: they achieve a fast First Contentful Paint (FCP) by loading in images at very low quality with a blur filter, then loading the high quality version in the background, and, finally, applying a transition from the low quality version to the high quality image.
Breaking that down, and keeping in mind the desire to hide this complexity from our fellow teammates, we need a few components:
-
<Picture>
: this is the component our teammates will use. It will take the following props: URL. That's it! When they use it, they'll magically get a picture on the page that loads efficiently with a beautiful UX. -
<BasicPicture>
: a private implementation detail, this component is the basic HTML and logic to display pictures from Contentful. This would include composing an appropriate URL for the image request based on your Contentful settings, browser support, and desired image quality. -
<LazyLoadPicture>
: a private implementation detail, this component consumes the<BasicPicture>
, figures out if the element is visible to the user (or going to be visible soon), and then determines whether to render a low or high quality image. -
<PicturePlaceholder>
: a private implementation detail, this component renders the placeholder blurred image that appears while the high quality version is loaded.
From a user experience perspective, you want to allow a low quality but beautifully blurred placeholder while high quality images load in the background. This allows the page to load quickly and with the correct layout without having to wait for every high-quality image on the page to load.
By using these components, and the Contentful Image API inside of <PicturePlaceholder>
, we were able to reduce image sizes and thus their load time by 90%. This was by both using more efficient image formats to reduce total request size, and reducing initial requests almost entirely due to deferred loading and lower initial load sizes. Notice how, despite this page containing many images below the fold, only a few requests need to be made upfront and they're quite small in size.
Even when loading PNGs, the request size for high resolution images is still a fraction of their true size.Note that on recognized supported browsers, many images are loaded as WebP images for the smallest file sizes.
What's it look like in action? Take a look: https://streamja.com/wak7q for a short video or head over to https://bethesda.net! They're small files. They load fast. The delays are invisible to the user. And best of all, it all looks smooth and beautiful.
In Part Two, coming soon, we'll code the implementation details of the above components together. If you want to know when Part Two is published, tap the follow button below! :)
This post was written by one of the instructors at Banyan Codecamp, a new coding bootcamp designed with a singular goal in mind: turn novice programmers into capable engineers under the leadership of senior engineers. Graduate, make six figures, and study on the beautiful island of Bali. To learn more, visit www.codeinbali.com.
Top comments (0)