Self-hosting fonts fixed a 7-second first paint
A Percher app had fast server responses but a blank page for seven seconds. The culprit was a blocked Google Fonts request, not the app server.
The bug report was one line: "the app is slow."
First visit took about seven seconds before anything painted. After that the page felt fine. Reloads were instant.
That shape, slow first paint, fast everything else, usually points away from the server. So I started with the obvious suspects, and they were all clean. TTFB was under 100 ms. The bundle was small. Every asset Percher served came back quickly.
One request did not. fonts.googleapis.com/css2?family=... sat in (pending) for several seconds and then timed out. The browser was holding the page hostage, waiting on a stylesheet hosted somewhere else.
The actual problem
The app loaded fonts directly from Google. That works until the user's network cannot reach Google's font CDN. The reason can be boring: a corporate firewall, a Pi-hole, regional routing, a CDN hiccup. It does not matter much. The moment your render path depends on a domain you do not control, you have signed up for outages you cannot debug.
Percher was serving the app fast. The browser was stuck waiting somewhere else.
Why generated apps hit this
Generated React and Vite starters often drop a Google Fonts <link> into the document head. It is convenient. It is free. It makes the default UI look less bare.
The cost is hidden. First paint depends on a third-party request, and if that request stalls, the app looks broken even though your origin is fine.
Service workers can make it worse. Some starters cache aggressively. A bad font request can get cached, or the worker can keep serving a stale state, and the user keeps seeing slow loads after the network problem is gone. Unregistering the service worker and hard reloading is a useful sanity check.
How to spot it
Open DevTools, Network tab. If your HTML, JS, CSS, and images from your own origin all return fast, look for one long pending request to:
fonts.googleapis.comfonts.gstatic.com- another hosted font or icon CDN
Then check Application, Service Workers. If unregistering the worker makes the next reload fast, the worker was part of the problem.
The fix
Self-host the font files. Ship them from your own origin.
For Next.js, use next/font/google:
```ts // app/layout.tsx import { DM_Sans, JetBrains_Mono } from "next/font/google";
const dmSans = DM_Sans({ subsets: ["latin"], display: "swap" }); const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], display: "swap" });
export default function RootLayout({ children }) { return ( <html className={dmSans.className}> <body>{children}</body> </html> ); } ```
Next downloads the font at build time and serves it from your app. No runtime request to Google.
For Vite, SvelteKit, Astro, or Solid, use @fontsource:
``bash bun add @fontsource/dm-sans @fontsource/jetbrains-mono ``
``css @import "@fontsource/dm-sans/400.css"; @import "@fontsource/dm-sans/500.css"; @import "@fontsource/jetbrains-mono/400.css"; ``
For plain HTML, download the WOFF2 files, drop them under public/fonts/, and define @font-face:
``css @font-face { font-family: "DM Sans"; src: url("/fonts/dm-sans-400.woff2") format("woff2"); font-weight: 400; font-display: swap; } ``
Redeploy after the change. If a service worker cached the bad path, unregister it or hard reload twice while testing.
The rule
Anything needed for first paint should come from your origin. Fonts are the common offender. The same rule applies to icon CDNs, blocking analytics scripts, avatar services, and hosted vendor scripts.
Once the critical path is yours, slow pages are easier to debug. If the browser has to wait on someone else's domain before it can paint, you have less control than you think.
Percher can serve your app quickly. It cannot make the browser's third-party requests fast.
Start with fonts.