Skip to content

Download each web font once on a first visit #104392

Closed

Description

@melvin-bot

Background

Three things load the 10 Expensify woff2 fonts on web:

  1. The fixed-name copy in /fonts/. The web build copies assets/fonts/web there. fonts.css and two inline @font-face rules in index.html point to it. The inline rules were added in Perf web windows fonts improv so the splash screen's fonts start downloading while the HTML is parsed. The browser draws all of the app's HTML text with these files.
  2. The hashed copy. Since June 2026, chartFontAssets.ts imports the same fonts, and the bundler emits them a second time under names that include a content hash. Charts are drawn by Skia on a canvas, which can't use CSS fonts, so Skia fetches these files itself for chart labels.
  3. The service worker. Workbox precaches every file for offline use. It fetches unhashed files past the HTTP cache and takes hashed ones from it.

The font preload filter matches both copies, so production index.html preloads each font under two URLs.

Problem

When a browser has no NewDot files cached yet, if the user opens any NewDot page on web, then the page downloads each Expensify woff2 font twice before the splash screen hides, and the service worker downloads it a third time. So 374 KB of fonts costs about 1.1 MB of traffic.

Solution

Give each of these fonts one hashed URL:

  1. Import fonts.css from the web entry so the bundler processes it. This points its url() values at the hashed files the charts already use. Stop copying the fonts to /fonts/.
  2. Write the hashed URLs into the two inline rules in index.html, which is already a build-time template, so the splash screen's two Expensify Neue faces still start downloading while the HTML is parsed.

Each woff2 font is then downloaded once, and the service worker takes it from the HTTP cache.

Impact: the sign-in page hides its splash screen about 160 ms (4%) sooner.

Reported in Slack.

Activity

  1. roryabraham commented on Oct 21, 2026

    @roryabraham
    Contributor

    @collectioneur when do you estimate you'll open a PR for this?

  2. collectioneur commented on Oct 22, 2026

    @collectioneur
    Contributor

    I was a little tied up with other tasks, I'll open the PR today 🫡

  3. collectioneur commented on Oct 22, 2026

    @collectioneur
    Contributor

    @roryabraham The PR is up 🙂

  4. melvin-bot commented on Oct 27, 2026

    @melvin-bot
    Author

    @roryabraham @collectioneur

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.23-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

  5. roryabraham commented on Oct 27, 2026

    @roryabraham
    Contributor

    @MelvinBot test this change in NewDot production - did it work?

  6. roryabraham commented on Oct 27, 2026

    @roryabraham
    Contributor

    I opted out of C+ review on the PR, so no payments will be due

  7. MelvinBot commented on Oct 27, 2026

    @MelvinBot
    Contributor

    🤖 Web test steps verification results: ⚠️ partial

    Yes, it worked. Production (9.5.23-3) now references each Expensify woff2 font under exactly one hashed URL, so a first visit downloads each font once. I checked the deployed production files directly. I couldn't capture a live browser network trace, because the web tester only drives the dev server and can't record HAR or service worker traffic.

    Step Status
    Production runs the fixnew.expensify.com/version.json returns 9.5.23-3, the release that includes Download each web font once on a first visit.
    ✅
    index.html preloads each font once10 font preloads, one per font, all hashed (for example, /ExpensifyNeue-Regular.bfe6e44ce4.woff2). No /fonts/ preloads, no inline @font-face rules, and no /css/fonts.css link.
    ✅
    Bundled CSS uses the same hashed URLs/main.c338bcf09f.css holds the @font-face rules. Every url() matches a preload URL exactly, so the preload and the CSS share one download.
    ✅
    Service worker reuses the HTTP cacheThe Workbox precache lists only the hashed fonts, each with revision:null. Workbox fetches those without cache-busting, so the browser serves them from the HTTP cache. No /fonts/ entries remain.
    ✅
    Live first-visit network traceNot run. The web tester can't open production in a fresh profile or record network traffic.
    ⚠️

    One note: /fonts/*.woff2 and /css/fonts.css still return 200 on production. They look like leftover files from earlier deploys. Nothing in the current HTML, CSS, JS, or service worker requests them, so they don't add traffic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions