> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# The Web Font That Made Your Page Invisible for Two Seconds
- URL: https://debugly.dev/font-loading-invisible-text-flash/
- Published: 2026-09-23T07:30:00.000Z
- Updated: 2026-10-17T05:57:35.000Z
- Description: With font display block, the browser renders nothing where the text should be until the font arrives, and on a slow network the page is a blank skeleton…
- Author: Rohit Bhadani
- Tags: Performance, Web Development, JavaScript

The page loaded, the layout appeared, and the words were not there. The header was a blank space, the body a column of nothing, for two full seconds on a throttled connection, until the font file arrived and the text popped in. Nothing was broken. The browser was doing exactly what the CSS asked, which is the unsettling part: the page had been asked to be invisible.

I have lost on-call weeks to this exact shape of slow burn, so the order below is the order I fix it in.

The property is `font-display`, and its values are a set of trades between invisible text, wrong text and shifting text, none of which is free. This is the mechanics of those trades, and the reasoning for choosing deliberately instead of inheriting a default.

This was Chrome 133 and Safari 17, with a self hosted woff2 and a third party font service, which conveniently fail in different ways.

## What the browser does while the font is missing

When text needs a font that has not arrived, the browser cannot render the final glyphs, so it follows the font-display policy. The default for a plain `@font-face`, when the property is omitted, is auto, which browsers treat as block in practice, and block is the invisible one.

**block** gives the font a short invisible period, commonly up to three seconds, during which text in that face renders as nothing. If the font arrives within the period, the text appears in the right face and nobody saw a fallback. If it arrives late, the fallback shows briefly and then swaps. On a fast network this is invisible in both senses. On a slow one, the page is a blank where the words should be, which is the incident.

**swap** skips the invisible period entirely. Text renders immediately in a fallback face and swaps to the web font when it arrives. Nothing is ever invisible, which is why swap became the recommended default, but the swap changes metrics widths, so the layout shifts when the font lands, and the shift is real user visible movement, the thing CLS measures.

**optional** gives the font a tiny window and then lets the browser skip it entirely on slow connections, caching the decision. The text is never invisible and often never shifts, at the cost of sometimes not getting the brand font, which is a trade some sites should make and almost none consider.

**fallback** is a middle of block and swap, a short invisible window then a short swap window. It exists and is rarely chosen deliberately.

## Why the invisible default ships

The blank page ships because the default is block-ish and because the failure is network dependent. On the developer's fast connection the font arrives inside the invisible window and the page looks perfect, so the review sees perfect. The blank only appears on the connections the team does not test, which is the same sampling blindness as [serving a 2400px image to a 390px phone](https://debugly.dev/shopify-image-srcset-oversized-mobile/), where the developer's device is not the user's device.

Third party font services compound it. A stylesheet from a font vendor that omits font-display, or sets block, makes the vendor's latency your invisibility window, and the vendor's slow day is your blank header, on a dependency you do not monitor.

## The layout shift half

Choosing swap removes the invisibility and introduces the shift, and the shift has its own cost. A heading rendered in the fallback face occupies different space than in the web font, so when the swap happens the text below it moves, and users mid read lose their place. This is not cosmetic. It is the page moving under the reader, and it is measured, and it is why swap is not a free win but a different bill.

The mitigation is to make the fallback occupy nearly the same space, by choosing a fallback face with similar metrics and adjusting its size and ascent so the swap moves little. Size adjust and ascent override on the fallback face, or a curated local fallback stack, shrink the shift to near nothing, which converts swap from a shift into a subtle repaint. This is the craft part, and it is the difference between swap as a default and swap as a decision.

## The preload interaction

A font that is critical to the first paint, the header face, can be preloaded so it arrives inside any window, which removes both the invisibility and the shift for that face by making the font not late. The preload must point at the exact file, with the crossorigin attribute fonts require, and it should be reserved for the one or two faces that matter early, because preloading every face is a bandwidth tax that makes the slow connection worse.

This is the same deprioritisation lesson as the lazy loaded hero in [your hero image is lazy loaded](https://debugly.dev/shopify-lcp-hero-image-lazy-loading/), where the browser's politeness about non critical resources is exactly what hurts the critical one. Fonts and the hero are both victims of not telling the browser what matters first.

## The decision, per face

The honest posture is per face, because faces have different importance.

The brand face on the first heading gets preload plus swap with a metric matched fallback, so it arrives early and shifts little. Body text, which is many faces worth of reading, gets swap with a close fallback, accepting a subtle repaint over any invisibility. Decorative faces get optional, letting slow connections skip them without ceremony.

What nobody should inherit is the blank, which is what block on an unpreloaded critical face produces on the slow network, and what the default hands you if you never decide.

## If it happens again

font-display is a three way trade between invisible text, shifted text and absent text, and the default chooses invisibility on the networks you do not test. Decide per face: preload the critical face so it is never late, swap with a metric matched fallback so the swap is a repaint, and optional for the decorative, and test on the throttled connection where the blank lives.

The page that renders its skeleton and hides its words is not loading slowly. It is loading on policy, and the policy was a default. The measurement side, seeing the font in the critical path, is the curl and devtools discipline in [the devtools performance panel mistakes](https://debugly.dev/devtools-performance-panel-mistakes/).

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)