Image Optimization using Higher Order Components

We’ve started using Higher Order Components at Grailed to balance image quality with performance.

Intro

People like pretty pictures. They like looking at big, colorful, glossy images. And if they’re about to spend $2,000 on a Maison Margiela jacket, they want to be able to zoom in and see the details on the tag.

People also like their websites to be fast and responsive. As builders of a marketplace of clothes, it’s our job to balance these competing interests.

At Grailed, I’m a member of the Core Squad. We’re responsible for performance, infrastructure, and tools that empower our teammates. As our site has grown, we’ve stashed image-rendering code under the kitchen sink, on shelves in the garage, and under the potted plant out back. Not only would it give Marie Kondo a stroke, it wasn’t delivering the best experience for our users. We decided to overhaul the way we handle images and set a few simple goals:

  • Hide complexity to make our teammates’ lives easier.
  • Render better-looking images.
  • Load pages faster.

The system we designed illustrates the power of Higher Order Components in React.

An Overview

Grailed uses two Content Delivery Networks to store and serve images: Contentful and Filestack (henceforth to be known as “Filepicker,” since that’s its legacy name and there’s way too much code in our codebase to wrap my head around the new name). Both come with a plethora of request options — height, width, quality, format, crop!

Let’s see a before and after. Here’s the code that rendered our homepage banner before this project, using mobileImage and desktopImage modules from Contentful:

Seems perfectly fine at first, but as we’ve scaled we’ve uncovered some problems:

  • Every developer must learn this Contentful-specific syntax.
  • isMobile is hacky — it just measures the width of an invisible <div/>
  • No ability to serve next-gen image formats like webp if the client supports it (Google Chrome, for example).
  • No ability to serve images of different sizes or resolutions if the client supports it (a Retina screen, for example).

It’s as if we ordered a shirt for a friend and didn’t first ask him what he liked or what his size was.

After, using our newly built <ContentfulBackgroundElement />

This will also return a <div/> with the correct className but the backgroundImage will be determined more thoughtfully, it uses a nice intuitive interface, and it lets you fadeIn! More on that later.

An image can be rendered in two ways (<img/> or backgroundImage) and we use two CDNs, so we’ll build four custom components:

Now a lot of this functionality can be shared:

Let’s define our building blocks.

Shared URL builders:

  • <filepickerSrcBuilder />
  • <contentfulSrcBuilder />

Shared renderers:

  • <Picture />
  • <BackgroundElement />

Additionally, we need two more pieces:

  • <withWebpAwareness /> Webp is a next-gen image format supported by Chrome and some other browsers. Webp images are about half the size of equivalent jpg images, so if we can use them, we should!
  • <imageLoader /> Wouldn’t it be nice to load your image then fire off an event when it’s ready that any developer could hook into? We thought so too.

So in total (are you with me?) we’re building FOUR components accessible to our developers by composing, in different permutations, SIX building blocks. If you’re into visuals, here’s a diagram:

WHOA. I thought this was supposed to make this easier?? And what’s the deal with instantiating all those components??

Well, yes, it’s complicated. But by putting this complexity under the hood, we’re lightening the burden on other developers. And even though instantiating new components takes time, it’s a lot faster than downloading a 400kb image when a 200kb version of the same will do.

Ok. But… why do we need all these components?

Let’s Dive In

We’ll start with <ContentfulPicture/>, which is simply:

We’re big into functional programming at Grailed, but that’s equivalent to:

Picture, the final argument in our chain, is a standard functional component, but the other two are Higher Order Components. It’s like an assembly line — HOCs take a component as an argument, add a prop, and pass it on.

Ok. So. WHAT’S UP WITH ALL THESE COMPONENTS??

The turd in the punchbowl, the fly in the ointment, the little seed we just couldn’t get out of the teeth of this project was one little word, asynchronous. And what handles asynchronous better than a stateful React component? As Santa Claus once told me (or maybe it was a Guinness commercial), “Good things come to those who wait.” If we wait to find out about webp support, we might be able to serve every image at half size.

But why do we have to wait?

Webp — it’s an image format that works on Chrome right? Well, yes. And some versions of Firefox. And Opera, maybe. So how can you tell what browser you’re in?

On Chrome, the “user agent”, (accessible at window.navigator.userAgent) is:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36

Huh, looks like there’s some Mozilla and Safari in there… Here’s Opera:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41

Look familiar? The point is, sniffing the user agent is a bad idea. So the only way to tell if a client can parse a webp image is to try it and see if it works, which is… asynchronous. Our version looks like this:

Shout-out to https://davidwalsh.name/detect-webp for the inspiration

This function tries to render a 1x1 white webp pixel and memoizes whether it was successful. We use this in our <withWebpAwareness /> component, which stores whether webp is supported in state:

This is the pattern we’ll use in the rest of our Higher Order Components. We take a component as an argument and render that same component, passing its props through and adding something extra once we figure it out, in this case, webpSupport. The only prop we blacklist is onWebpSupportDetermined since it’s unlikely any other component needs to know about that.

Let’s go back to our composition:

The next piece of the chain is <contentfulSrcBuilder />.

Let’s go back to our analogy: if you were ordering a shirt for your friend, would you guess his size, or send him all the options, tell him to measure himself, and let him pick?

The second option, while not great gift-giving etiquette, is going to get your friend the right size. <contentfulSrcBuilder /> has one job: given some images and Contentful’s API, generate all possible srcs and srcSets. We’ll let something else measure itself and choose.

We’re taking in a component and some props (webpSupport and images), and based on those props, we’re generating some assets. Note that we don’t generate these assets until we hear about webpSupport.

I’ve left out the propsToAssetBundles function here since it’s specific to Contentful, but if the developer passes three different images (mobile, desktop, and tablet), here’s an example of the assets generated:

An aside about srcSet:

Modern browsers support <picture/> and <img/> tags with an srcSet attribute that the browser can use to serve the correct image based on the capabilities of the device on which it’s running.

<img srcSet="<url>?h=200 1x, <url>?h=400 2x" src="url?h=200" />

In the above example, on a modern browser, on a Retina screen, the browser will request the url with a height of 400, since the 2x density is supported. If the browser is old and doesn’t know about srcSet, it will fall back on src. We include densities of 1x, 2x, and 3x, so if the developer passes three different images (one for each device), nine URLs will be generated (for each device, three different densities).

So now we have all our options. Great. Now <Picture /> just has to measure itself and choose:

Note that props like className are passed all the way down with {…rest}. Let’s look at those helper functions:

We require a “fallback” asset which we define as the asset matching “desktop.”

matchesCurrentMedia is the heart of this component — we wanted to use a <picture/> element which allows you to pass multiple srcSets and media queries and will use the browser’s native code to determine which is appropriate, but it caused some CSS snafus to have <img/> wrapped in <picture/>. So choosing the image becomes a two-step process:

  1. matchesCurrentMedia uses a media query to choose the asset (see example assets array above).
  2. Within the asset, the browser chooses the src from the srcSet based on pixel density.

The Remaining Components

Phew. That was a lot. The good news is, we’re most of the way to understanding <ContentfulBackgroundElement />. This component will render a <div/> or an <a/> with a backgroundImage.

<withWebpAwareness /> and <contentfulSrcBuilder /> remain the same, as they should! The renderer doesn’t need to know how it got the options.

Let’s look at <imageLoader />. Just like <withWebpAwareness />, this is a stateful component with one job:

It looks a little complicated, but there’s not a lot going on here — we wait until we have some assets, pick the appropriate one (using the same logic we saw in <Picture />), load its src, and pass down whether or not it’s ready.

We use <imageLoader /> only with <BackgroundElement /> because a <div/> doesn’t know how to choose a backgroundImage based on media queries or densities, only <img/> and <picture/> know how to do that. So we make our choice in <imageLoader /> and let <BackgroundElement /> take only imageUrl and isSourceImageLoaded:

There’s one fun React trick hiding here that I didn’t know — we can render a <div/> or any other element by passing the string “div”, “a”, etc. as the TagName prop. Note also that passing down isSourceImageLoaded allows that nice fadeIn I mentioned way back at the beginning — if we want a fadeIn, we don’t display the element until the image has loaded.

Let’s look at our remaining two components:

There’s only one piece of this puzzle we haven’t talked about! And guess what, I’m not even going to write out <filepickerSrcBuilder />. Its return statement is exactly the same as <contentfulSrcBuilder />, it just uses a different propsToAssets function to format URLs for Filepicker’s API.

Conclusion

And there you have it! From six building blocks, some of which were Higher Order Components, we composed four components to optimize our images. Now every picture you see on Grailed and Heroine should be beautiful and performant. If it’s not, let us know — this should be your best bet.