# Font and image optimization

> How font-display and size-adjust control text during a font load, how loading, fetchpriority, and picture control image fetches, and AVIF and WebP support.

Fonts and images are the bytes that decide Largest Contentful Paint and Cumulative Layout
Shift on most pages: a web font that blocks text or swaps late moves every line, and an image
that is lazy-loaded above the fold or served as a JPEG where AVIF would do delays the largest
paint. The controls are declarative, `font-display` and `size-adjust` in `@font-face`, and
`loading`, `fetchpriority`, `decoding`, `width`, `height`, and `<picture>` on images.

## How it works

Fonts and images are handled by different mechanisms, so they are described separately.

### Fonts

`font-display` sets two timers for a `@font-face`: a block period, during which text is drawn
invisible while the font loads, and a swap period, during which a late-arriving font may still
replace the fallback. `swap` makes the block period near zero so text appears in the fallback
at once and reflows when the font arrives; `optional` makes the block period about 100 ms and
the swap period zero, so a font that misses the deadline is used only on the next page view and
never causes a reflow; `fallback` sits between them; `block` hides text for up to 3 s
([font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display),
developer.mozilla.org). The descriptor is in Chrome 60, Firefox 58, and Safari 11.1 (BCD
`css.at-rules.font-face.font-display`).

The reflow that `swap` causes is removed with a size-matched fallback: a second `@font-face`
that names a local system font and uses `size-adjust`, `ascent-override`, and `descent-override`
(Chrome 92, Firefox 92, Safari 17) so its lines occupy the same height as the web font. With
matching metrics the swap changes glyph shapes but not line breaks, and the layout shift score
stays at zero.

### Images

`loading="lazy"` defers an image's fetch until it approaches the viewport (Chrome 77, Firefox 75,
Safari 15.4) and must not be used on the LCP image, which the browser would then discover late.
`fetchpriority="high"` raises an image's fetch priority from the default `Low` to `High` in the
preload scanner, which is what the LCP image needs when it competes with scripts (Chrome 101,
Firefox 132, Safari 17.2). `decoding="async"` lets the decode happen off the main thread.
`width` and `height` give the image a layout box before it loads; without them a lazy image has
no size, and the browser may decide it is never near the viewport.

Format decides transfer size. AVIF is around half the size of a JPEG at comparable quality and
WebP about a quarter smaller; AVIF decodes in Chrome 85, Firefox 93, and Safari 16.1 (16.4 on all
platforms), WebP in every engine since Safari 14 ([Image file type and format
guide](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types),
developer.mozilla.org). `<picture>` with `<source type="image/avif">` lets the browser pick the
first format it can decode without a script.

## Examples

The first example is CSS, the second markup, and only the third needs script.

### A web font that cannot shift layout

The first rule loads the web font with `swap`; the second declares a metric-matched fallback
and the `font-family` list names both. The override values come from the font's metrics (tools
such as Fontaine or Capsize compute them).

```css
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Brand Sans Fallback";
  src: local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: "Brand Sans", "Brand Sans Fallback", sans-serif;
}
```

On a slow connection the page renders in Arial scaled to the same metrics; when `brand-sans.woff2`
arrives the glyphs change and nothing moves. Where the brand font is decorative rather than
essential, `font-display: optional` skips the swap entirely on slow visits.

### The hero image and a gallery

The hero is eager and high priority with an AVIF source and a JPEG fallback; gallery images
are lazy with explicit dimensions so they reserve space before they load.

```html
<picture>
  <source srcset="/images/hero.avif" type="image/avif" />
  <source srcset="/images/hero.webp" type="image/webp" />
  <img src="/images/hero.jpg" width="1200" height="630" fetchpriority="high" decoding="async" alt="Product hero shot" />
</picture>

<img src="/images/gallery-3.avif" width="400" height="300" loading="lazy" decoding="async" alt="Gallery photo 3" />
```

The `<img>` inside `<picture>` must still carry `width` and `height`; the `<source>` elements
only choose the URL.

### Detecting AVIF support for script-generated images

Markup needs no detection because `<picture>` negotiates. Script that builds URLs (a canvas
export, an API that returns one format) can probe once with a one-pixel AVIF and cache the
answer.

```js
let avifSupport;
function supportsAvif() {
  if (avifSupport) return avifSupport;
  if (!('Image' in window) || !('decode' in HTMLImageElement.prototype)) {
    avifSupport = Promise.resolve(false); // no decode(): request JPEG
    return avifSupport;
  }
  const probe = new Image();
  probe.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgABogQEDQgMgkxIABJSElBQkJB';
  avifSupport = probe.decode().then(() => true).catch(() => false); // decode failure: fall back to JPEG
  return avifSupport;
}
```

`decode()` rejects with `EncodingError` when the format is unknown, which the catch maps to
`false`; the same rejection also covers a corrupt file, so the probe is only run on the known
sample.

:::observed
Chrome DevTools, Network panel, with the **Priority** column enabled (right-click a column
header), shows the image marked `fetchpriority="high"` above as `High` while the lazy gallery
images show `Low`, the default for images the preload scanner finds; the column's values
(`Highest`, `High`, `Medium`, `Low`, `Lowest`) map one-to-one onto Blink's internal priorities
([Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority),
web.dev). Removing the attribute and reloading drops the hero to `Low` until layout promotes it,
which is the delay the attribute exists to remove.
:::

## See also

- [font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display) (developer.mozilla.org)
- [Optimize resource loading with the Fetch Priority API](https://web.dev/articles/fetch-priority) (web.dev)
- [Image file type and format guide](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types) (developer.mozilla.org)
- [Resource hints (preload and preconnect)](/reference/performance/resource-hints/)
- [Core Web Vitals (LCP, INP, and CLS)](/reference/performance/core-web-vitals/)
- [Precaching strategies](/reference/performance/precaching/)