Craig Buckler Freelance UK IT consultant specialising in HTML5 webby stuff.

Jank-free page loading with media aspect ratios

7 min read 2131

Jank-Free Page Loading With Media Aspect Ratios

Responsive webpages often rearrange content while loading. This can lead to considerable frustration for users when the text they are reading is suddenly moved off-screen. In this article, we’ll look at several new and existing ways to solve this performance problem.

Pre-RWD layouts

Fluid layouts were possible from the moment Sir Tim Berners-Lee developed the first web browser (as well as HTML, a page editor, web server, HTTP and DNS). Web content was only constrained by the width of the browser viewport.

In the days of dial-up access, developers religiously added width and height attributes to every img tag. This ensured appropriate space was allocated on the page before the image started to download and reflows were avoided.

<img src="myimage.jpg" width="400" height="300" alt="my image" />

The unitless dimensions refer to pixels. In this case, a 400×300 area would be reserved and the image would be stretched or squeezed into that space, even if the real dimensions did not match.

As the web evolved, designers and developers moved toward fixed-width layouts, which were easier to comprehend, design, and code. Image width and height attributes became more critical when they were used to determine layout dimensions.

Unfortunately, fixed layouts were only effective when the browser was wide enough to accommodate the design. A 960px-width layout design looked good on a 1024×768 monitor, but somewhat sparse on a larger screen, and required awkward horizontal scrolling on a smaller device.

The smartphone revolution — and the iPhone launch in particular — changed everything. Small screens rapidly became important and, while the devices were capable of rendering standard webpages, panning and zooming to read text was laborious. Web developers initially solved the problem by duplicating content into alternative mobile sites, often with an “m” subdomain.

Post-RWD layouts

Ethan Marcotte’s pivotal 2010 article titled “Responsive Web Design” explained how CSS media queries could be used to create a single site layout that adapted to the viewport dimensions. Various techniques emerged, and for the past decade developers have been encouraged to omit img width and height attributes and use CSS sizing instead. For example, you could size an image to the width of its container.

img {
  width: 100%;
  height: auto;
}

You could also ensure an image was no wider than its actual pixel width.

img {
  max-width: 100%;
  height: auto;
}

This technique had an unfortunate consequence: page space could only be allocated once the image started to download and the browser could determine its dimensions. Content had to be reflowed as each image started to appear. The text you were reading could suddenly move or disappear off-screen, especially on mobile devices.

Sizing an Image to the Width of a Container

The loading issue is also evident with videos, iframes, and advertising.

There is no easy way to solve this problem within web code. Determining whether an asset has started to load and unintentionally reflowed content is currently impossible. Browsers have implemented scroll anchoring in an attempt to prevent content jumping, but the annoyance still occurs.

Aspect ratio resolution

An aspect ratio defines the relationship between an element’s width and height. Consider a typical 16:9 screen: for every 16 units across, the screen is nine units deep.

Aspect Ratio Resolution
If we know one dimension, we can calculate the other.

  • Given a 800px width, the height is 800 x 9 / 16 = 450px
  • Given a 450px height, the width is 450 x 16 / 9 = 800px

Note: televisions and monitors are normally advertised using the diagonal measurement. Using a little Pythagorous, an 85-inch, 16:9 screen would therefore be 74.1×41.7 inches. The screen size calculator does the hard work for you.

In the case of browser rendering, the width of an HTML element is often known almost immediately because it can be determined during the initial layout. If an aspect ratio can be provided, the height and corresponding area can be calculated and reserved.

Firefox 71 aspect ratio determination

Firefox 71 and above calculates aspect ratios when the width and height attributes are defined in an img tag.

<!-- set a 400:300 - or 4:3 - aspect ratio -->
<img src="myimage.jpg" width="400" height="300" alt="my image" />

CSS can be used to size the image to the width of its containing element.

img {
  width: 100%;
  height: auto;
}

height: auto is essential to ensure the image height is not fixed at 300px.

Therefore, if a 4:3 image is shown in a 200px-width parent container, its corresponding height is calculated and a 200px-by-150px area can be reserved before the image starts to download. The result is a jank-free loading experience.

Firefox 71 Aspect Ratio Determination
Any appropriate img width and height can be used since the image is resized by CSS (e.g., width="4" height="3". However, it is best to set a reasonable size to ensure the image remains visible in very old browsers, if CSS fails to load, or when a user style sheet is defined.

Firefox 71 is set to be released in December 2019, and Chrome will support aspect ratio calculations shortly after. However, you can add img width and height attributes now. Older browsers and those without support will not reserve space, but the image will be sized as before.

Responsive images (srcset and sizes attributes)

Responsive images use the srcset and sizes attributes to define a set of bandwidth-efficient alternatives. This is typically used on high native resolution screens (HiDPI or Retina) to ensure the best quality image is downloaded. For example, only use the highest-resolution img-400.jpg on 4x DPI devices.

 <img width="100" height="100"
      alt="responsive image"
        src="img-100.jpg"
     srcset="img-100.jpg 1x,
             img-200.jpg 2x,
             img-300.jpg 3x,
             img-400.jpg 4x" />

It can also be used to load appropriate images for the space available. For example, use img-400.jpg when the browser viewport approaches a 400px width.

<img width="100" height="100"
       alt="responsive image"
       src="img-100.jpg"
    srcset="img-100.jpg 100w,
            img-200.jpg 200w,
            img-300.jpg 300w,
            img-400.jpg 400w" />

Each image should use the same aspect ratio so the img width and height can be set.

Art direction (picture element)

The HTML picture element requests one of its child elements according to browser support and conditions. Different images can be used according to the dimensions and orientation of a device. For example, download landscape.jpg when the viewport width is greater than the height, or fall back to portrait.jpg otherwise.

<picture>
  <source srcset="landscape.jpg"
           media="(min-aspect-ratio:1/1)" />
  <img src="portrait.jpg" alt="portrait image" />
</picture>

Each image could have a different aspect ratio. At the time of writing, browser vendors are considering the best solution. The most likely option is width and height attributes on all source and img child elements.

<picture>
  <source srcset="landscape.jpg"
           media="(min-aspect-ratio:1/1)"
           width="800" height="450" />
  <img src="portrait.jpg" alt="portrait image"
           width="450" height="800" />
</picture>

Aspect ratios on other elements

The aspect ratio is not calculated when a width and height attribute is defined for an iframe, video, or any other element. This would be useful, so two proposals have been put forward, but neither is implemented in a browser at the time of writing:

  1. The CSS aspect-ratio property, e.g. aspect-ratio: 16/9
  2. The HTML intrinsicsize attribute (this seems less likely to gain support given that the width and height attributes effectively do the same thing)

Let’s go over some alternative aspect ratio options that work in most browsers.

Stretching and cropping

Our puffin image is 1,600×1,200, so it has a 4:3 ratio.

<img src="puffin.jpg" width="1600" height="1200" alt="a puffin" />

If we attempt to place it in an area with a different aspect ratio, such as 200×300, the browser will stretch or squash the image to fit.

img {
  width: 200px;
  height: 300px;
  border: 2px solid #000;
}

Stretching and Cropping an Image With Aspect Ratios
The CSS object-fit property provides further options. object-fit: fill is the default and is identical to the image shown above.

object-fit: contain ensures the whole image is scaled to fit the available space. The aspect ratio is maintained, so borders will appear on either the horizontal or vertical edges.

img {
  width: 200px;
  height: 300px;
  object-fit: contain;
  border: 2px solid #000;
}

Image Scaled to Fit the Available Space With Aspect Ratios
object-fit: cover resizes the image to fit the area without showing borders. Its aspect ratio is maintained so cropping can occur.

Image Resized to Fit the Area Without Showing Borders
This may not be ideal, so object-position can be used to specify the horizontal and vertical alignment, e.g., object-position: 20% 50%.

Image Aligned With Object Position
object-fit: none does not resize the image, so in this case, only a small portion will be shown.

Portion of an Image Not Resized
Finally, object-fit: scale-down resizes the image as if none or contain were specified and chooses whichever results in the smallest size. Since our image is larger than the containing space, contain is effectively chosen.

The CSS background-size property provides similar options for background images.

While these methods may be appropriate for some images, they force the content into a sized container. If that container does not match the aspect ratio, parts of that content could be hidden, e.g., the playback controls on a video element.

The aspect ratio padding trick

Fortunately, there is an unusual way to define the aspect ratio of any element using the CSS padding-top or padding-bottom property. When either is expressed as a percentage, the resulting padding is proportional to the width of the element, not the height.

The following element will be a perfect 16:9 ratio since padding-top is set to 56.25 percent of the width (9 / 16 x 100 percent). The actual height is set to zero because it would normally be added to the padding.

.container-16-9 {
  position: relative;
  width: 100%;
  height: 0;
  padding-top: calc(9 / 16 * 100%);
}

If the width is 400px, the vertical padding is 56.25 percent of 400px which is 225px. The height will change as the width increases or decreases.

We can therefore place any element inside that container, such as a 16:9 video.

<div class="container-16-9">
  <video controls preload="none">
    <source src="video.mp4" type='video/mp4' />
  </video>
</div>

We can also absolutely position it to retain the aspect ratio.

.container-16-9 {
  position: relative;
  width: 100%;
  height: 0;
  padding-top: calc(9 / 16 * 100%);
}

.container-16-9 > :first-child {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

Video Placed Inside a Container
It would be great if we could extract the container’s width and height attributes as CSS attr() values, then use them in a calc(), but that is not possible because the attr() function only returns a string for use in content properties. However, browser vendors are considering casting.

Therefore, we can define a series of known ratios.

.aspect {
  position: relative;
  width: 100%;
  height: 0;
  padding-top: 100%; /* default 1:1 */
}

.aspect.ratio-16-9 {
  padding-top: calc(9 / 16 * 100%);
}

.aspect.ratio-9-16 {
  padding-top: calc(16 / 9 * 100%);
}

.aspect.ratio-4-3 {
  padding-top: calc(3 / 4 * 100%);
}

.aspect.ratio-3-4 {
  padding-top: calc(4 / 3 * 100%);
}

.aspect > :first-child {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

Then, set an aspect and appropriate ratio class on the container.

<div class="aspect ratio-16-9">
  <video controls preload="none">
    <source src="video.mp4" type='video/mp4' />
  </video>
</div>

A CSS preprocessor such as Sass may be an easier option since all aspect ratios can be defined in a list and calculated at build time.

// define all known ratios
$ratio:
  (w: 16, h:  9 ),
  (w:  9, h: 16 ),
  (w:  4, h:  3 ),
  (w:  3, h:  4)
;

.aspect {
  position: relative;
  width: 100%;
  height: 0;
  padding-top: 100%; /* default 1:1 */
}

.aspect > :first-child {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}

@each $r in $ratio {
  $w: map-get($r, w);
  $h: map-get($r, h);
  .aspect.ratio-#{$w}-#{$h} {
    padding-top: $h / $w * 100%;
  }
}

The main drawback of predefining these classes is that you cannot set any aspect ratio in the HTML like you can with width and height attributes. However, custom properties (CSS variables) provide an option since an aspect ratio can be defined in a style attribute.

<!-- define a 21:9 container -->
<div style="--aspect-ratio: 21/9;">
  <video...></video>
</div>

Its value is available in CSS and can be used accordingly:

@supports (--custom:property) {

  [style*="--aspect-ratio"] {
    position: relative;
    width: 100%;
    height: 0;
    padding-top: calc( 100% / (var(--aspect-ratio)) );
  }

  [style*="--aspect-ratio"] > :first-child {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
  }

}

This is shorter and more flexible, but will fail in older browsers without custom property support (the @supports rule ensures those browsers don’t even try).

The primary flaw of the padding trick is that the sized element must be wrapped in a container where the aspect ratio is defined. JavaScript options such as fitvids can handle this automatically, but there can still be some page reflowing as aspect ratios are calculated and containers are added.

Conclusion

Text content being moved by reflowing pages as they load has been a user frustration for several years, but a little light has appeared at the end of the tunnel.

For the immediate future, remember to add width and height attributes to all img tags. Consider using the padding percentage trick on video and iframe containers if they are likely to cause a reflow.

As browsers evolve, the CSS aspect-ratio property may become an essential way to reserve page space, and the era of janky page loading will be over.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Craig Buckler Freelance UK IT consultant specialising in HTML5 webby stuff.

Leave a Reply