# The Font Arrives Late and the Text Jumps: Swap Does That by Design

> Turning on font optimization trades invisible text for shifted text, by specification. Two variables tell you which symptom you have, and the fix differs for each.

- Published: 2026-10-02
- Updated: 2026-10-04
- Author: xSpeed Cache Team
- Tags: Performance, Core Web Vitals, WordPress, PageSpeed, fix:font-display, platform:wordpress
- Canonical: https://xspeedcache.com/blog/font-optimization-text-flash/

---

Updated October 2026

`font-display: swap` appears on 50.1% of mobile pages that use web fonts, per the [HTTP Archive Web Almanac's 2025 fonts chapter](https://almanac.httparchive.org/en/2025/fonts). The two values that put any limit on that swap, `fallback` and `optional`, together reach 5.6%.

So the setting almost everyone turns on is the one that promises to replace your fallback face whenever the real font turns up, however late. Invisible text and shifted text look like one problem with one switch. They come from different halves of the font-loading path, and the display value governs only one of them.

## Quick Summary: Which Symptom You Actually Have

| If you see this | Do this | Why |
|---|---|---|
| Blank space where text should be, then text appears | Check whether the display value reached that font at all | A blocking value, or a rewrite that never touched your `@font-face` |
| Text at once, typeface changes later, nothing moves | Nothing | `swap` behaving exactly as specified |
| Text at once, typeface changes later, lines move | Match the fallback's metrics, not the display value | The two faces measure differently, so the swap reflows the paragraph |
| Text at once, typeface never changes | Confirm the font actually loaded | `optional` declined it, or the request failed |

## Two Variables Tell You Which One It Is

Throttle the network to Slow 3G in your browser's dev tools, reload, and watch only the first screen. Record two things: whether the text is there on the first paint, and whether it moves afterwards.

![A two-by-two grid: text present at first paint against whether the line moves later, giving four verdicts.](https://xspeedcache.com/images/blog/fdread.webp)

## What Each font-display Value Actually Promises

| Value | Block period | Swap period | What the visitor gets | Share of mobile pages, 2025 |
|---|---|---|---|---|
| `auto` | the browser's choice | the browser's choice | invisible text first | 9.1% |
| `block` | about 3s | infinite | invisible, then swaps late | 24.9% |
| `swap` | about 100ms | infinite | fallback, then swaps late | 50.1% |
| `fallback` | about 100ms | about 3s | fallback, swaps only if quick | 5.1% |
| `optional` | about 100ms | zero | fallback, and it keeps it | 0.5% |

Those durations are the recommendations written into CSS Fonts Module Level 4, which also notes that user agents may use slightly different ones. The adoption column is the Web Almanac's 2025 crawl.

![Block and swap periods for the five font-display values on one time axis, with infinite swap periods running off the edge.](https://xspeedcache.com/images/blog/fdtime.webp)

**An infinite swap period is the entire story behind a late reflow.** `block` and `swap` both carry one, so both will replace your fallback at second nine if second nine is when the file arrives. `fallback` stops asking after about three seconds. `optional` never swaps at all unless the font was already cached, which makes it the only value that cannot move a paragraph after the reader started reading it.

## Why the Toggle Did Nothing to Half Your Fonts

Our `FontsModule.php` in the plugin trunk appends `display=swap` to Google Fonts stylesheet URLs, rewrites a blocking `display=auto` or `display=block`, and leaves a deliberate `fallback` or `optional` alone. (xSpeed Cache is ours, built by WPDeveloper, a Startise company.) It does that through a filter on `style_loader_tag`, the hook WordPress runs over stylesheets it enqueued itself. That last detail is the whole problem, and it is true of every plugin in this genre.

| Where the font comes from | Reached by a display-value toggle |
|:---|:---:|
| A Google Fonts stylesheet enqueued through WordPress | ✅ |
| A `<link>` the theme prints straight into `header.php` | ❌ |
| A font pulled in by an `@import` inside the theme's CSS | ❌ |
| A self-hosted `@font-face` rule in the theme's stylesheet | ❌ |

Self-hosted faces declare their own `font-display` inside the `@font-face` block, so that is where you set it for those, and no URL rewrite can do it for you. Our [fonts documentation](https://xspeedcache.com/docs/fonts/) states this directly. Competitors' release notes carry the same shape: LiteSpeed Cache 7.9 records resolving an HTML attribute parse problem in its Font Display setting, and WP-Optimize's changelog added an option in 3.5.0 to switch Google Fonts processing off entirely when it conflicts with another plugin.

A Fonts panel also governs only how the font renders, never whether the stylesheet referencing it blocks the page, which is the separate job of [CSS and JavaScript loading](https://xspeedcache.com/docs/minify/) and [critical CSS](https://xspeedcache.com/docs/critical-css/).

## The Shift Is a Metrics Problem, So Fix the Metrics

If the verdict was "text at once, then it moves", the display value is doing its job and the fallback is the wrong size. A swap from a fallback with a taller x-height or wider advances re-wraps the line, and every line below it moves.

CSS has four descriptors for exactly this, documented in Chrome's "Improved font fallbacks" by Katie Hempenius: `size-adjust`, `ascent-override`, `descent-override` and `line-gap-override`. You declare a second `@font-face` pointing at a local system font, override its metrics to match your web font, then name that face as the fallback. The swap changes the shapes without changing the measurements, and the shift goes to zero rather than getting smaller.

People skip it because a display value is a checkbox and a metric override is four lines of CSS. Fonts also sit inside the first-paint window, which is why [caching alone never moved your LCP](https://xspeedcache.com/blog/caching-did-not-fix-slow-wordpress/) and why [page weight and LCP come apart](https://xspeedcache.com/blog/page-weight-vs-lcp-wordpress/) on font-heavy themes.

## Preload Buys One Face and Charges You for the Rest

- **`crossorigin` is mandatory.** MDN's `rel=preload` reference states that font preloading requires it. Without it the file is fetched in the wrong mode, the CSS-driven request cannot reuse it, and the font downloads twice.
- **Preload only what paints above the fold.** A preloaded font is fetched at high priority whether or not the first screen needs it, so a full stack competes with your hero image for the same bandwidth. One or two faces is usually right.

## Checking This on a Site You Did Not Build

Read the rendered page, not the settings screen. A toggle reporting "on" tells you what the plugin intends, not what reached the markup, and the three unreachable rows above are where they differ.

Run the free scan on your own site: [xspeedcache.com/scan/](https://xspeedcache.com/scan/)

Our [font and resource-hint controls](https://xspeedcache.com/features/) are free, and the self-hosting and subsetting that remove the third-party connection are what the [paid tier](https://xspeedcache.com/free-vs-pro/) adds. Autoptimize and WP-Optimize both handle Google Fonts well if that one piece is all you need.

## Frequently Asked Questions

### I turned on font-display swap and my text still goes invisible. Why?

The rewrite only reaches fonts WordPress enqueued as stylesheets. A `<link>` hardcoded in `header.php`, an `@import`, or a self-hosted `@font-face` never passes through it.

### I fixed the invisible text and now the page jumps instead. Did I make it worse?

You traded one symptom for the other, which is what the specification says those values do. The jump is a metric mismatch between your fallback and your web font, so `size-adjust` on a fallback `@font-face` is the fix, not reverting the display value.

### My CLS score got worse after enabling font optimization. Is that expected?

It is consistent: `swap` converts a paint delay into a swap that can land at any moment, and a swap between differently measured faces is a layout shift. Override the fallback metrics and the score recovers.

### Should I just use font-display: optional?

It is the only value that cannot shift text after first paint, and it is on 0.5% of pages because visitors on slow connections may then never see your typeface. For body text, `fallback` with matched metrics is the usual compromise.

### My preloaded font downloads twice. What did I get wrong?

Almost certainly a missing `crossorigin` attribute on the `<link rel="preload">`. Fonts are fetched over CORS, so a preload without it is a different request from the one the CSS makes.

## The Order to Fix It In

**What to do this week:**

1. Throttle to Slow 3G and record the two variables, so you know which verdict you have.
2. Fetch the rendered HTML and check which font sources carry a display value.
3. Set `font-display` inside your self-hosted `@font-face` blocks, where no toggle reaches.
4. Add a metric-matched fallback face with `size-adjust` if anything moved.
5. Cut your preload list to the faces that paint above the fold, each with `crossorigin`.

For the toggles rather than the CSS, xSpeed Cache does the Google Fonts rewrite, the preload emission and the preconnect free, with self-hosting and subsetting on the $29/yr founding price and a 14-day money-back guarantee: [pricing](https://xspeedcache.com/pricing/). Stripping [unused CSS](https://xspeedcache.com/blog/remove-unused-css-wordpress/) changes which fonts the first screen asks for, so run the two-variable read again afterwards. The font arriving late is not the bug. The paragraph that moves when it arrives is.
