This one’s for the frontend typography nerds. Fontsource is a convenient way to add high-performance variable fonts to your site, and Fontaine can often dramatically reduce cumulative layout shift, improving user experience and performance metrics when using custom fonts. This article points out some common configuration pitfalls that can prevent the two packages from working together, especially when you add Tailwind to the mix.
Fontsource
Fontsource bundles Google Fonts into NPM packages that streamline the process of using custom fonts in your project. Tutorials on using Fontsource abound. Astro even includes one in its official docs; I’m not going to repeat it here. Custom fonts are generally a bad idea from a performance perspective, but the allure of control over your site’s typeface can be irresistible to certain personality types.
Fontaine
Fontaine helps mitigate an issue called Cumulative Layout Shift (CLS) that occurs when you use custom fonts on a web page. You typically want site visitors’ browsers to begin rendering content as soon as possible, which invovles using the font-display: swap
CSS rule. This rule allows the browser to show content before your custom font is loaded, but once that font does become available, text might shift around as characters change size and aspect ratio.
CLS matters because Google considers it a “core web vital.” Apparently users find the rearrangement of page content jarring, especially if buttons or links move right before they’re clicked. More importantly, reducing CLS gives your site a better Lighthouse score, which can improve your site’s search rankings (all else equal). As with Fontsource, tutorials on using Fontaine are plentiful; I found this SvelteKit + Fontaine Tutorial by Rodney Lab particularly helpful.
fontaine and tailwind
Fontaine doesn’t look for fonts in your tailwind.config.ts
file. Several tutorials I’ve come across assume that you can just list the custom font in your tailwind config file like this:
Even if you include the correct fallback name, however, Fontaine isn’t going to actually generate the fallback @font-face
declaration unless it finds a font-family
declaration in a CSS file. This is a subtle bug because fallback fonts are only ever displayed for a few milliseconds before the custom font becomes available, so you might not notice whether Fontaine is working or not. Here’s how to work around this in a way that allows you to use the font-sans
utility in your tailwind.config.ts
file:
Fontsource + Fontaine
So now we get to the point of this article. While tutorials on either Fontsource or Fontaine are plentiful, you can run into some unexpected issues when using them together under a Vite-based framework like Astro or SvelteKit.
hashing it out with Vite
Vite is a build tool that bundles and versions your code and “static assets”, e.g. the parts of your site that aren’t code, like images, fonts, and CSS. It’s at the heart of both Astro and SvelteKit, my two favorite frontend frameworks.
Vite handles static assets gracefully: at build time, it adds a hash to the filename of each asset, which makes it possible to cache certain assets indefinitely. In this case: if a font file changes, your CSS style sheet will be updated to point to a new version. The user’s browser will download the new version linked from the updated CSS file and can store it until it changes again. No need to worry about the font file becoming stale since every change creates a distinct filename. This elegant technique is known as “cache busting”.
busted
Fontaine tutorials usually assume that you’re placing your font files somewhere under the public
directory. Files in public
are static, in the sense that this directory’s contents are treated as if they’re being served by a traditional web server like Apache or Nginx, but they’re not handled as “static assets” by Vite. This is convenient for tutorials that use Fontaine because the files in public
have stable URLs that aren’t affected by cache busting.
If you install a font with a @fontsource
NPM package, however, the font files are subject to Vite’s static asset handling. This means that if you have a @font-face
declaration like this in a file like src/styles/global.css
:
Then at build time, your CSS file will be inlined and Vite will bundle the font file to a new path that looks something like this:
The package-relative import url in global.css
relies on Vite’s static asset handling to become a valid URL at build time. Fontaine doesn’t understand package-relative file import URls because they’re not really valid CSS syntax, they only work because Vite transforms them. This means that Fontaine will not be able to find font files at build time based on the import urls it finds in the CSS files in your project.
As a result Fontaine can’t upload your font files to the API it uses to retrieve font metrics, so it can’t calculate an optimized fallback font, so ultimately it doesn’t do anything. It doesn’t even throw an error, it just silently fails, which makes this a frustrating bug to debug.
the fix
Fontaine’s Vite plugin has a resolvePath
option that gets us out of this mess. Tutorials usually use the resolvePath
option to turn relative paths into absolute paths under the public
directory. In this case, we can use the same option to point package-relative paths to the correct file locations under node_modules
:
conclusion
There you have it— a working example of using Fontsource and Fontaine together in an Astro project. The same approach has worked for me in SvelteKit projects and it should work for other Vite-based frameworks. The key is to use the resolvePath
option to tell Fontaine how to find the font files it needs at build time by pointing it to the node_modules
directory.
By applying Fontaine to this blog, I was able to reduce CLS for this very post (as measured by Lighthouse) from 0.454 to 0.064. This still takes a point off my lighthouse performance score, but 99 is way better than the 85 I had before using Fontaine. Don’t forget the important note above about the order of @font-face
declarations; this is one of many ways you can trip yourself up while using Fontaine.
This approach minimizes asset loading waterfalls by allowing font definitions to be bundled with your global CSS file, while still allowing you to use the @fontsource
package to retrieve optimized .woff2
font files. And it minimizes the need for aggressive font preloading, which can be a drag on performance. It’s a level of performance optimization that’s sure to appeal to a certain personality type.