Home / Blog / Custom font loading: the three pitfalls that cost me 200ms

Custom font loading: the three pitfalls that cost me 200ms

Adding a custom font pushed launch time up by 200ms. How to optimize font loading.

Custom fonts give an iOS app character. Brand consistency, a differentiated UX. Used poorly, they’re a performance cost. When I added a custom font to Dentii, launch time jumped 200ms, with visible jank.

I found and fixed three main pitfalls, and restored launch time. Here’s the experience.

Pitfall 1: too many font files

A typical font ships as multiple files: Regular, Bold, Italic, Black, Light, Medium, Black Italic, Light Italic. Eight files, 500KB to 2MB each.

All of them get bundled into the binary. App size grows, and at launch each one is registered with the font system.

Fix: only ship the weights you actually use.

Say your project uses two weights (Regular, Bold). Don’t include the others in the binary. Don’t drop them in the asset catalog, don’t list them under UIAppFonts in Info.plist.

I saved 3 to 4 MB of binary size that way. Launch time dropped by 80ms.

Alternative: variable fonts. One file, programmatic weight. GeistVF.ttf (Geist Variable) packs every weight into a single file.

Text("Hello")
    .font(.custom("GeistVF", size: 16))
    .fontWeight(.bold)  // Automatic variable axis adjustment

Most modern typefaces have a variable version. Prefer it.

Pitfall 2: font registration bottleneck

At iOS app launch, custom fonts go through a registration pass. With 5+ variants, the delay is noticeable.

Automatic registration at startup:

// Info.plist UIAppFonts array
<key>UIAppFonts</key>
<array>
    <string>GeistRegular.otf</string>
    <string>GeistBold.otf</string>
</array>

This automatic registration is synchronous. It adds to launch time.

Alternative: async registration.

Leave fonts out of Info.plist and register them programmatically:

func registerFonts() {
    let fontURLs = Bundle.main.urls(forResourcesWithExtension: "otf", subdirectory: nil) ?? []
    
    fontURLs.forEach { url in
        var error: Unmanaged<CFError>?
        CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error)
    }
}

// At app launch, async
Task.detached(priority: .background) {
    registerFonts()
}

Register on a background thread. Main thread stays fast.

Careful though: fonts must be ready before the first UI render. Race conditions are possible.

Safer path: register synchronously for the first screen’s fonts, async for the rest.

Pitfall 3: font metric mismatches

The default system font (SF Pro, San Francisco) has very well-tuned metrics. Switching to a custom font:

  • Line height changes
  • Leading (space between lines) differs
  • Cap height lands somewhere else
  • x-height doesn’t match

Result: text looks misaligned in the UI. Each SwiftUI Text component can end up at a different height.

Fix: custom UIFont extension.

extension UIFont {
    static func geist(size: CGFloat, weight: UIFont.Weight = .regular) -> UIFont {
        let fontName: String
        switch weight {
        case .bold: fontName = "GeistBold"
        case .semibold: fontName = "GeistSemiBold"
        case .medium: fontName = "GeistMedium"
        default: fontName = "GeistRegular"
        }
        
        let font = UIFont(name: fontName, size: size) ?? UIFont.systemFont(ofSize: size, weight: weight)
        
        // Metric override to match the system font
        let metrics = UIFontMetrics.default
        return metrics.scaledFont(for: font)
    }
}

Fallback: system font if the custom font didn’t load.

CSS-like approach with text styles

Custom font plus Dynamic Type integration in SwiftUI:

extension Font {
    static func geistTitle() -> Font {
        .custom("GeistBold", size: 28, relativeTo: .title)
    }
    
    static func geistBody() -> Font {
        .custom("GeistRegular", size: 16, relativeTo: .body)
    }
    
    static func geistCaption() -> Font {
        .custom("GeistMedium", size: 12, relativeTo: .caption)
    }
}

The relativeTo: parameter respects the user’s text size preference. Custom font plus accessibility.

Font subset generation

On larger projects, you can optimize custom fonts further.

A font file contains every Unicode glyph (Chinese, Arabic, symbols). Strip out the characters you won’t use and the file shrinks.

Tools:
pyftsubset (Python fonttools)
glyphhanger (Node.js)

pyftsubset GeistRegular.otf --unicodes=U+0020-007F,U+00A0-00FF --output-file=GeistRegular-subset.otf

Subset for Basic Latin (English plus European). 60 to 70% size reduction.

For a localized app, be careful: include the Turkish characters in the unicode range.

FOUT vs FOIT

A web problem that applies to iOS too:

  • FOIT (Flash of Invisible Text): text stays invisible until the font loads
  • FOUT (Flash of Unstyled Text): text shows in the system font first, then swaps to the custom one

iOS default is FOUT. Text appears, then swaps when the font loads. Minor flicker.

If the flicker bothers you:

// Load font async, then show UI
func loadFontThenShowUI() {
    CTFontManagerRegisterFontURLs([url], .process, true) { _, _ in
        DispatchQueue.main.async {
            self.showMainUI()  // Font ready, now show UI
        }
    }
}

Delays launch a bit, but kills the flicker.

Licensing implications

Watch out for custom font licensing:

  • Google Fonts: free, open source
  • Adobe Fonts: requires Creative Cloud subscription
  • Commercial fonts (Gotham, Proxima Nova): per-platform license
  • Custom/bespoke fonts: designer contract

Does the font’s license permit iOS app usage? Check. Some fonts allow web use but not native apps.

Benchmarking

Benchmark before and after font loading optimization:

  • App launch time (Instruments)
  • First frame render time
  • Text rendering perf (scroll through a list)

On Dentii:
– Before: 850ms launch (mid-range phone)
– After: 650ms launch (200ms recovered)

Wrap-up

Custom font performance impact is real, but manageable. Three pitfalls:
1. Too many font files: include only the weights you use
2. Font registration overhead: go async when possible
3. Font metric mismatch: scale with UIFontMetrics

Variable fonts are the modern answer. Subset generation for size reduction. Verify licensing.

Custom fonts add brand value. The performance cost can be minimized with careful management.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch