Skip to content

Font Performance Optimization - From Basic to Enhanced Fallbacks

Font Performance Migration: Enhanced Fallback Fonts

Section titled “Font Performance Migration: Enhanced Fallback Fonts”

In the past, we used to implement fonts in a basic way, without considering the impact on layout shift and performance:

/* ================================================= Font-Face */
@font-face {
font-family: "Syne";
src: url('/fonts/Syne/Syne-VariableFont_wght.woff2') format('woff2'),
url('/fonts/Syne/Syne-VariableFont_wght.woff') format('woff');
font-style: normal;
font-weight: 400 800;
font-display: fallback or swap;
}
@font-face {
font-family: "Hubot Sans";
src: url('/fonts/HubotSans/HubotSans-VariableFont_wdth,wght.woff2') format('woff2'),
url('/fonts/HubotSans/HubotSans-VariableFont_wdth,wght.woff') format('woff');
font-style: normal;
font-weight: 200 900;
font-display: fallback or swap;
}
@font-face {
font-family: "Hubot Sans";
src: url('/fonts/HubotSans/HubotSans-Italic-VariableFont_wdth,wght.woff2') format('woff2'),
url('/fonts/HubotSans/HubotSans-Italic-VariableFont_wdth,wght.woff') format('woff');
font-style: italic;
font-weight: 200 900;
font-display: fallback or swap;
}

We discovered a better approach using enhanced fallback fonts with metric overrides to eliminate layout shift:

/* ================================================= Font-Face */
/* fallback fonts */
@font-face {
font-family: 'Syne-fallback';
src: local('Trebuchet MS');
ascent-override: calc((925 / 1000) * 100%);
descent-override: calc((275 / 1000) * 100%);
line-gap-override: calc((0 / 1000) * 100%);
}
@font-face {
font-family: 'HubotSans-fallback';
src: local('Courier New');
ascent-override: calc((109 / 1000) * 100%);
descent-override: calc((32 / 1000) * 100%);
line-gap-override: calc((0 / 1000) * 100%);
}
/* project fonts */
@font-face {
font-family: "Syne";
src: url('/fonts/Syne/Syne-VariableFont_wght.woff2') format('woff2'),
url('/fonts/Syne/Syne-VariableFont_wght.woff') format('woff');
font-style: normal;
font-weight: 400 800;
font-display: swap;
}
@font-face {
font-family: "Hubot Sans";
src: url('/fonts/HubotSans/HubotSans-VariableFont_wdth,wght.woff2') format('woff2'),
url('/fonts/HubotSans/HubotSans-VariableFont_wdth,wght.woff') format('woff');
font-style: normal;
font-weight: 200 900;
font-display: swap;
}
@font-face {
font-family: "Hubot Sans";
src: url('/fonts/HubotSans/HubotSans-Italic-VariableFont_wdth,wght.woff2') format('woff2'),
url('/fonts/HubotSans/HubotSans-Italic-VariableFont_wdth,wght.woff') format('woff');
font-style: italic;
font-weight: 200 900;
font-display: swap;
}

The main problem with the old approach was layout shift. When custom fonts loaded, the text would jump and reposition because system fonts have different dimensions than our custom fonts. This created a poor user experience and hurt our Core Web Vitals scores.

  1. Upload your custom font to FontDrop.info.
    If FontDrop doesn’t work, you can also try FontForge or Capsize.

  2. Find the metrics in the ‘data’ panel: Data panel

  3. Look for “Ascent”, “Descent, “Line Gap” and “unitsPerEm” values Values unitsPerEm

    • Example for Syne: Ascent = 1036, Descent = 335, Line Gap = 0
    • Note: Even if the value comes out negative, always use it as a positive.

Add these fallback font definitions before your existing @font-face declarations:

  1. Bring the values you found in the previous step. Add the correct value into de calc:
ascent-override = (Custom Font Ascent / unitsPerEm) × 100
descent-override = (Custom Font Descent / unitsPerEm) × 100
line-gap-override = (Custom Font Line Gap / unitsPerEm) × 100

Example:

ascent-override: calc((1036 / 1000) * 100%);
descent-override: calc((335 / 1000) * 100%);
line-gap-override: calc((0 / 1000) * 100%);
  1. Upload the system font you want to use as fallback, see more info here:
    • For Syne, we use Trebuchet MS
    • For Hubot Sans, we use Courier New
/* Add these fallback fonts */
@font-face {
font-family: 'Syne-fallback';
src: local('Trebuchet MS');
ascent-override: calc((1036 / 1000) * 100%);
descent-override: calc((335 / 1000) * 100%);
line-gap-override: calc((0 / 1000) * 100%);
}
@font-face {
font-family: 'HubotSans-fallback';
src: local('Courier New');
ascent-override: calc((109 / 1000) * 100%);
descent-override: calc((32 / 1000) * 100%);
line-gap-override: calc((0 / 1000) * 100%);
}

Change your CSS font declarations from:

/* Old way */
font-family: "Syne", sans-serif;
font-family: "Hubot Sans", sans-serif;

To:

/* New way */
font-family: "Syne", "Syne-fallback", sans-serif;
font-family: "Hubot Sans", "HubotSans-fallback", sans-serif;
  1. Open your site in Chrome DevTools
  2. Disable cache (right-click refresh → “Empty Cache and Hard Reload”)
  3. Throttle network to “Slow 3G” in Network tab
  4. Watch the text as the page loads - it should not jump or shift
  5. Compare before/after by temporarily removing the fallback fonts
  1. Use Lighthouse to measure CLS (Cumulative Layout Shift)

    • Run audit before and after implementing fallbacks
    • Target: CLS score should be < 0.1
  2. Use Chrome DevTools Performance tab:

    • Record page load
    • Look for “Layout Shift” events
    • Should see significant reduction

If you still see layout shift, adjust the override values:

/* Start with calculated values */
@font-face {
font-family: 'Syne-fallback';
src: local('Trebuchet MS');
ascent-override: 92.5%; /* Try 90% or 95% if needed */
descent-override: 27.5%; /* Try 25% or 30% if needed */
line-gap-override: 0%;
}

Testing tip: Use this CSS to temporarily highlight layout shifts:

/* Add this temporarily to see shifts */
* {
outline: 1px solid red;
}
  • ascent-override: Controls how tall letters appear (affects spacing above text)
  • descent-override: Controls how deep letters go below the line (affects spacing below text)
  • line-gap-override: Usually set to 0% for precise control

These percentages make the system font match your custom font’s dimensions, eliminating the jump when fonts load.

The following fonts are the best web safe fonts for HTML and CSS:

  • Arial (sans-serif)
  • Verdana (sans-serif)
  • Tahoma (sans-serif)
  • Trebuchet MS (sans-serif)
  • Times New Roman (serif)
  • Georgia (serif)
  • Garamond (serif)
  • Courier New (monospace)
  • Brush Script MT (cursive)

You can algo check it out here

If layout shift still occurs:

  1. Double-check your calculations
  2. Try adjusting values by ±2-5%
  3. Test on different devices and browsers
  4. Consider using a different system font as fallback

If fonts look too different:

  1. Choose a system font with similar character width
  2. Adjust the size-adjust property if needed
  3. Test with actual content, not just Lorem Ipsum

That’s it! Your fonts will now load smoothly without layout shift.

All of the information below is based on the original article available here