The Standard Way Fonts Get Loaded

In most Angular projects, fonts and icon sets are pulled in as static assets—often through a <link> element placed directly inside the index.html file.

<link
      href="https://fonts.googleapis.com/css?family=Lato:300,400,700,900"
      rel="stylesheet"
    />

While the font file is being fetched and initialized, the browser withholds text rendering entirely. This behavior is commonly referred to as FOIT, or Flash of Invisible Text.

From a user's perspective, this leads to:

  • a degraded experience, since the interface appears empty for a noticeable stretch,
  • measurable accessibility shortcomings,
  • wasted network requests for font data that might never actually be used in the active view.

To be fair, the static approach isn't fundamentally flawed. But for our particular scenario, switching to lazy loading makes more sense.

Lazy Font Loading Paired with font-display: swap

Rather than placing a <link> for the font inside index.html—which means Angular hasn't even bootstrapped yet—the smarter move is to defer the font request until the moment a particular view is about to be rendered.

Here are a couple of practical illustrations:

  • If a specific font is only relevant to the dashboard module, trigger its loading when the dashboard component initializes.
  • If one global font covers the entire application, call the dynamic injection within the ngOnInit of the root AppComponent.

Essentially, the font <link> still ends up in the document's <head>, just that it now gets inserted there programmatically and well after the initial document parse.

This strategy brings a number of advantages:

  • The font download only kicks in when there's an actual rendering need,
  • the initial payload of the application is lighter,
  • and render-blocking is greatly reduced, since the browser no longer stalls user-visible content on font availability.

This addresses a chunk of the problem—but definitely not all of it, because at the very start of the render cycle, our custom font is simply unavailable in the browser.

That's exactly where font-display: swap comes into play. This CSS property dictates the browser's fallback strategy while the font face is still downloading.

In the absence of this setting, browsers hold back text rendering until the web font is completely loaded, effectively producing the blank-space effect of FOIT.

Enabling font-display: swap changes the behavior to:

  1. Instantly paint text with a locally available fallback, like a system font,
  2. fetch the web font in the background,
  3. and—once that fetch completes—swap in the intended font with minimal visual disruption for the user.

What's the real consequence of this approach?

  • The visitor sees content immediately instead of a void; even if the typeface isn't the custom one yet, it's readable text.
  • From a performance-metric standpoint—especially FCP and LCP—any gain that gets visible text on screen sooner is a win, and these metrics form the backbone of Core Web Vitals reporting.

A Concrete Code Walkthrough

The snippet below illustrates how a target font begins its loading lifecycle from a system font baseline.

Notice the display=swap parameter appended to the font URL—it's an instruction to Google Fonts to insert font-display: swap into the CSS it serves as part of the various @font-face rules.

import { Component, OnInit, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `<h1>Przykład lazy loaded font</h1>`
})
export class AppComponent implements OnInit {
  private document = inject(DOCUMENT);

  ngOnInit() {
    const link = this.document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Roboto&display=swap';
    this.document.head.appendChild(link);
  }
}

Should you prefer a locally saved font to act as the placeholder—while the web font downloads—you'll need to add some configuration in your theming layer, like in the root style.scss file.

@font-face {
  font-family: 'MyLocalFont';
  src: url('/assets/fonts/MyLocalFont.woff2') format('woff2');
  font-display: swap; 
}

body {
  font-family: 'MyLocalFont', system-ui, sans-serif;
}

Decoding the declaration font-family: 'MyLocalFont', system-ui, sans-serif; is straightforward:

  1. First, it looks for that local font (referenced as MyLocalFont),
  2. if that isn't there, it falls back to the operating system's interface font (system-ui),
  3. and in the worst case, it ends up on a generic sans-serif family.

Post-Optimization Core Web Vitals Snapshot

Metric Before Lazy Loading Fonts After applying Lazy Loading Fonts / font-display: swap Improvement (%)
Largest Contentful Paint (LCP) 3.2 s 2.1 s -34%
Cumulative Layout Shift (CLS) 0.15 0.06 -60%
First Input Delay (FID) 120 ms 80 ms -33%

Some context on the listed metrics:

  • LCP (Largest Contentful Paint) gauges the timing for the most significant visual element to appear. Because faster font handling accelerates textual render times, we lock in a better LCP.
  • CLS (Cumulative Layout Shift) quantifies instabilities in the layout. The combination of lazy font retrieval and web font swapping prevents the layout from jumping when the new font face is applied.
  • FID (First Input Delay) marks the time gap between a user action and the page's response. It's perhaps the least directly impacted metric, but releasing the main thread from font-blocking overhead does contribute to a healthier FID.

These figures reflect comparative measurements taken before and after deploying this lazy-loading pattern and the swap behavior. For corroboration, you can see similar patterns and outcomes described over at Google Web.dev and within the data published by HTTP Archive.

Merging It All Together

We gain nothing when web fonts become gatekeepers to complete page rendering. The more modern, performant path is deferred font loading, where a system font manages the earliest paint, preserving the user experience while the custom typeface loads. The outcome is a buttery-smooth text transition, quicker view hydration, improved ranking on Core Web Vitals tests, and smarter bandwidth usage.

For such a small change in code structure, the returns are disproportionately large—you get a tangible boost for both end-user experience and overall application speed.