From 68e23367deb8ea4813f1b73244fb26c412533823 Mon Sep 17 00:00:00 2001 From: Ronja Date: Tue, 10 Nov 2026 21:45:40 +0100 Subject: [PATCH 1/2] perf(fonts): serve the fonts from the app as variable fonts Closes #175 --- docs/AI_USAGE.md | 8 ++++++++ docs/SUSTAINABILITY.md | 19 +++++++++++++++++++ docs/VM_SETUP.md | 15 +++++++++++++++ index.html | 6 ------ package-lock.json | 20 ++++++++++++++++++++ package.json | 2 ++ src/main.tsx | 3 +++ src/styles/variables.css | 4 ++-- 8 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 44e324f..ee36630 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -596,3 +596,11 @@ - **What the team did:** Took the peer feedback that the README (about 59 kB) is too long to find your way in, wrote issue #182, and asked for the reasoning to be kept. - **What Claude did:** Moved four long sections unchanged to their own documents (`ELEMENTS_PAGE.md`, `DATABASE_SETUP.md`, `CATALOG_SEARCH.md`, `E2E_TESTS.md`) with a short summary and a link in the README (now about half the size). Added a "Where is what" table that points from each feature to the file where it starts, and a list of contents at the top of `docs/API.md`. A script checked that every relative link and anchor in the changed files works. - **Limits:** The text of the moved sections was not shortened or checked again, so what was outdated in them is still outdated (that is issue #148). A first run of the move emptied `README.md` because a formatting command was cut off in a pipe; it was restored from git and run again. + +## The fonts (`index.html`, `src/main.tsx`, `src/styles/variables.css`, `docs/SUSTAINABILITY.md`, `docs/VM_SETUP.md`) + +- **Tool:** Claude Code +- **What the team did:** Took the peer feedback that the fonts are many and not cached (issue #175), and asked for the work to be done with measurements. +- **What Claude did:** Counted first which weights the pages really use (the computed style of every text on six pages in both themes) and what the browser loads (a real browser, before and after). Found that Google sends one variable file for each family, so the list of weights does not change what is transferred, and that the only avoidable cost was the style sheet of Google and the two other servers. Moved the fonts into the app (`@fontsource-variable/inter`, `@fontsource-variable/faustina`), removed the links to Google from `index.html`, and wrote the Apache header for the built files in `docs/VM_SETUP.md`. +- **What was decided:** Keep the two families. Using one family would save 26.7 kB, but it changes the look of every page, so it was left to the team. The README was not changed here, because the README is being shortened in another pull request. +- **Limits:** 91.1 kB before and 75.0 kB after, on the About page, measured once on one machine. The cache header was not tried on the VM, and `vite preview` does not set one, so the long cache is written down and not measured. The new packages add the font files of all scripts to the built folder (about 200 kB on the disk of the VM, not transferred to visitors). diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 3900b4c..9904330 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -46,6 +46,25 @@ The app offers a dark theme. On OLED displays, darker pixels can use less power **Where:** the theme toggle in [Navbar.tsx](../src/components/navbar/Navbar.tsx), theme state in [App.tsx](../src/App.tsx), and dark color variables in [variables.css](../src/styles/variables.css). +### Serve the fonts ourselves, as variable fonts + +The app used two families from Google Fonts (Faustina for titles, Inter for text), asked for with nine weights in one address in `index.html`. A page needed **three requests to two other servers** (the style sheet of Google, 16.1 kB, and one font file for each family) and **91.1 kB**. The weights that the pages really use are Faustina 500, 700 and 800 and Inter 400, 500, 600 and 700 (measured with the computed style of every text on the Elements page, the element page, About, Calculator, Login and the page for an unknown address, in the light and in the dark theme). Faustina 400 and 600 were asked for and never used. + +Both fonts are now packages ([`@fontsource-variable/inter`](https://fontsource.org/fonts/inter) and [`@fontsource-variable/faustina`](https://fontsource.org/fonts/faustina)) and are built into the app. They are **variable fonts**: one file holds all the weights, so the weights are not a cost any more and the list of weights does not need to be kept short by hand. The files are the same ones that Google sent (48.3 kB for Inter and 26.7 kB for Faustina, the Latin part; the browser fetches the parts for other scripts only if a text needs them). + +| | Requests | Servers | Transferred | +| --------------------- | -------- | -------------------------------------------------------- | ----------- | +| Before (Google Fonts) | 3 | 3 (the app, `fonts.googleapis.com`, `fonts.gstatic.com`) | 91.1 kB | +| After (in the app) | 2 | 1 | 75.0 kB | + +The style sheet of Google (16.1 kB, a request that blocks the page, with a cache of one day) is gone, and no visitor sends a request to a third party. The cache header of the font files is now ours: they are in `dist/assets/` with a hash in the name, so they can be kept for a year (the setting is in [VM_SETUP.md](VM_SETUP.md), step 5, and has not been tried on the VM yet). + +**Not changed:** the two families. Using Inter for the titles as well would save the 26.7 kB of Faustina and one request, but it changes how every page looks, so it is a decision for the whole team. The built folder holds the font files of all scripts (Greek, Cyrillic, Vietnamese and so on, about 200 kB in all); they are only on the disk of the VM, and no visitor downloads them unless a text needs them. + +**Measured** with a real browser on a production build (`vite build`, `vite preview`), on the About page, before and after. The cache header of the built files was not measured: `vite preview` does not set one. + +**Where:** `src/main.tsx` (the imports), `--font-family-title` and `--font-family-text` in [variables.css](../src/styles/variables.css), and `index.html` (no links to Google). + ## What we have not measured and could improve - We have not measured energy use or carbon emissions per visit, real-world data transferred on a typical user journey, or the energy mix of the hosting environment. A next step would be to establish repeatable measurements for transferred bytes, load performance, and browser processing. diff --git a/docs/VM_SETUP.md b/docs/VM_SETUP.md index ee60346..b39a5df 100644 --- a/docs/VM_SETUP.md +++ b/docs/VM_SETUP.md @@ -143,6 +143,21 @@ Check the result with the `curl` command below before deploying the frontend. The answer should be `{"data":{"__typename":"Query"}}`. +5. Let the browser keep the built files for a long time (recommended). Vite puts a hash of the content in the name of every file in `dist/assets/` (the JavaScript, the CSS and the fonts), so a changed file always gets a new name and the old one can be kept for a year. Without a header the browser asks again every time. Enable `mod_headers` and add this to the same site configuration as the proxy lines: + + ```bash + sudo a2enmod headers + sudo systemctl restart apache2 + ``` + + ```apache + + Header set Cache-Control "public, max-age=31536000, immutable" + + ``` + + Check it with `curl -sI http://it2810-XX.idi.ntnu.no/project2/assets/`: the answer should have `Cache-Control: public, max-age=31536000, immutable`. Do not set this for `index.html`, which has no hash in the name. The fonts are in the same folder, because they are part of the app and not loaded from Google (see "Serve the fonts ourselves" in [SUSTAINABILITY.md](SUSTAINABILITY.md)). This step has not been tried on the VM yet. + The VM serves plain http, so Sign in with Google does not work there (Google only accepts http for `localhost`), and the tokens would travel without encryption. See "Login" in the README. --- diff --git a/index.html b/index.html index 4676c48..ddf0a6b 100644 --- a/index.html +++ b/index.html @@ -4,12 +4,6 @@ - - - WAG CoP Planner diff --git a/package-lock.json b/package-lock.json index f73e602..715f43c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@apollo/client": "^4.3.4", "@apollo/server": "^5.5.1", "@base-ui/react": "^1.9.0", + "@fontsource-variable/faustina": "^5.3.0", + "@fontsource-variable/inter": "^5.3.0", "@prisma/client": "^7.10.1", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", @@ -1703,6 +1705,24 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@fontsource-variable/faustina": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/faustina/-/faustina-5.3.0.tgz", + "integrity": "sha512-C0OuS3x2cKErndfoX3mGIGnVsrkjvMuujj4b0b1FVBNw/scN1BLxZKCSxy++8FXJjhNwJcWfDwwurE4s0/rXjw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@graphql-codegen/add": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-7.1.1.tgz", diff --git a/package.json b/package.json index 331bb02..6b23bb9 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "@apollo/client": "^4.3.4", "@apollo/server": "^5.5.1", "@base-ui/react": "^1.9.0", + "@fontsource-variable/faustina": "^5.3.0", + "@fontsource-variable/inter": "^5.3.0", "@prisma/client": "^7.10.1", "class-variance-authority": "^0.7.1", "cn": "^0.4.0", diff --git a/src/main.tsx b/src/main.tsx index 9ba3546..8183d91 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,9 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { ApolloProvider } from '@apollo/client/react' import { BrowserRouter } from 'react-router-dom' +// The fonts are in the app (and not loaded from Google): variable fonts, one file for every weight. +import '@fontsource-variable/faustina/wght.css' +import '@fontsource-variable/inter/wght.css' import App from './App.tsx' import { apolloClient, restoreSession } from './lib/apollo/client' diff --git a/src/styles/variables.css b/src/styles/variables.css index 0f78d26..96105de 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -13,8 +13,8 @@ --text-color-light: #eae0cf; /* Font settings */ - --font-family-title: 'Faustina', serif; - --font-family-text: 'Inter'; + --font-family-title: 'Faustina Variable', serif; + --font-family-text: 'Inter Variable'; --font-size-page-title: 42px; --font-size-element-title: 19px; --font-size-row-title: 15px; From 0a2558e6cf06bb5d137b6f22fa88dd2f753b959f Mon Sep 17 00:00:00 2001 From: Ronja Date: Wed, 11 Nov 2026 18:01:01 +0100 Subject: [PATCH 2/2] docs(fonts): explain the weights and the number of fonts, leave the cache header to #204 Refs #175 --- docs/AI_USAGE.md | 6 +++--- docs/SUSTAINABILITY.md | 25 +++++++++++++++++++------ docs/VM_SETUP.md | 15 --------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index ee36630..aa981d3 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -597,10 +597,10 @@ - **What Claude did:** Moved four long sections unchanged to their own documents (`ELEMENTS_PAGE.md`, `DATABASE_SETUP.md`, `CATALOG_SEARCH.md`, `E2E_TESTS.md`) with a short summary and a link in the README (now about half the size). Added a "Where is what" table that points from each feature to the file where it starts, and a list of contents at the top of `docs/API.md`. A script checked that every relative link and anchor in the changed files works. - **Limits:** The text of the moved sections was not shortened or checked again, so what was outdated in them is still outdated (that is issue #148). A first run of the move emptied `README.md` because a formatting command was cut off in a pipe; it was restored from git and run again. -## The fonts (`index.html`, `src/main.tsx`, `src/styles/variables.css`, `docs/SUSTAINABILITY.md`, `docs/VM_SETUP.md`) +## The fonts (`index.html`, `src/main.tsx`, `src/styles/variables.css`, `docs/SUSTAINABILITY.md`) - **Tool:** Claude Code - **What the team did:** Took the peer feedback that the fonts are many and not cached (issue #175), and asked for the work to be done with measurements. -- **What Claude did:** Counted first which weights the pages really use (the computed style of every text on six pages in both themes) and what the browser loads (a real browser, before and after). Found that Google sends one variable file for each family, so the list of weights does not change what is transferred, and that the only avoidable cost was the style sheet of Google and the two other servers. Moved the fonts into the app (`@fontsource-variable/inter`, `@fontsource-variable/faustina`), removed the links to Google from `index.html`, and wrote the Apache header for the built files in `docs/VM_SETUP.md`. +- **What Claude did:** Counted first which weights the pages really use (the computed style of every text on six pages in both themes) and what the browser loads (a real browser, before and after). Found that Google sends one variable file for each family, so the list of weights does not change what is transferred, and that the only avoidable cost was the style sheet of Google and the two other servers. Moved the fonts into the app (`@fontsource-variable/inter`, `@fontsource-variable/faustina`), removed the links to Google from `index.html`. The Apache header for the built files was left to a separate issue (#204), because it has to be set and tried on the VM. - **What was decided:** Keep the two families. Using one family would save 26.7 kB, but it changes the look of every page, so it was left to the team. The README was not changed here, because the README is being shortened in another pull request. -- **Limits:** 91.1 kB before and 75.0 kB after, on the About page, measured once on one machine. The cache header was not tried on the VM, and `vite preview` does not set one, so the long cache is written down and not measured. The new packages add the font files of all scripts to the built folder (about 200 kB on the disk of the VM, not transferred to visitors). +- **Limits:** 91.1 kB before and 75.0 kB after, on the About page, measured once on one machine. The cache header was not set up (issue #204), and `vite preview` does not set one, so the long cache is not measured. The new packages add the font files of all scripts to the built folder (about 200 kB on the disk of the VM, not transferred to visitors). diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 9904330..ab4b7b6 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -48,22 +48,35 @@ The app offers a dark theme. On OLED displays, darker pixels can use less power ### Serve the fonts ourselves, as variable fonts -The app used two families from Google Fonts (Faustina for titles, Inter for text), asked for with nine weights in one address in `index.html`. A page needed **three requests to two other servers** (the style sheet of Google, 16.1 kB, and one font file for each family) and **91.1 kB**. The weights that the pages really use are Faustina 500, 700 and 800 and Inter 400, 500, 600 and 700 (measured with the computed style of every text on the Elements page, the element page, About, Calculator, Login and the page for an unknown address, in the light and in the dark theme). Faustina 400 and 600 were asked for and never used. +**Before.** The app got its two fonts (Faustina for titles, Inter for text) from Google Fonts. A page needed three requests to two other servers: the style sheet of Google (16.1 kB) and one font file for each family. In all that was 91.1 kB. -Both fonts are now packages ([`@fontsource-variable/inter`](https://fontsource.org/fonts/inter) and [`@fontsource-variable/faustina`](https://fontsource.org/fonts/faustina)) and are built into the app. They are **variable fonts**: one file holds all the weights, so the weights are not a cost any more and the list of weights does not need to be kept short by hand. The files are the same ones that Google sent (48.3 kB for Inter and 26.7 kB for Faustina, the Latin part; the browser fetches the parts for other scripts only if a text needs them). +**Now.** Both fonts are packages, [`@fontsource-variable/inter`](https://fontsource.org/fonts/inter) and [`@fontsource-variable/faustina`](https://fontsource.org/fonts/faustina), and are built into the app. A page needs two requests to one server (the app itself): 75.0 kB. | | Requests | Servers | Transferred | | --------------------- | -------- | -------------------------------------------------------- | ----------- | | Before (Google Fonts) | 3 | 3 (the app, `fonts.googleapis.com`, `fonts.gstatic.com`) | 91.1 kB | | After (in the app) | 2 | 1 | 75.0 kB | -The style sheet of Google (16.1 kB, a request that blocks the page, with a cache of one day) is gone, and no visitor sends a request to a third party. The cache header of the font files is now ours: they are in `dist/assets/` with a hash in the name, so they can be kept for a year (the setting is in [VM_SETUP.md](VM_SETUP.md), step 5, and has not been tried on the VM yet). +**What changed for the visitor.** -**Not changed:** the two families. Using Inter for the titles as well would save the 26.7 kB of Faustina and one request, but it changes how every page looks, so it is a decision for the whole team. The built folder holds the font files of all scripts (Greek, Cyrillic, Vietnamese and so on, about 200 kB in all); they are only on the disk of the VM, and no visitor downloads them unless a text needs them. +- The style sheet of Google (16.1 kB) is gone. It blocked the page and was kept for only one day. +- No visitor sends a request to a third party any more. +- The font files are in `dist/assets/` with a hash in the name, so they can be kept for a year. The header that does this has to be set on the VM; it is not done yet and is issue #204. -**Measured** with a real browser on a production build (`vite build`, `vite preview`), on the About page, before and after. The cache header of the built files was not measured: `vite preview` does not set one. +**About the weights.** The peer review counted nine weights (Faustina 400, 500, 600, 700, 800 and Inter 400, 500, 600, 700). That was right, but the weights were not where the cost was. Google already sent one file for each family, so nine weights never meant nine downloads. Both fonts are now **variable fonts**: one file holds all the weights, so there is no list of weights to keep short. The files are the same ones Google sent (Inter 48.3 kB, Faustina 26.7 kB, the Latin part). The browser fetches the parts for other scripts only if a text needs them. -**Where:** `src/main.tsx` (the imports), `--font-family-title` and `--font-family-text` in [variables.css](../src/styles/variables.css), and `index.html` (no links to Google). +We measured which weights the pages really use: Faustina 500, 700 and 800, and Inter 400, 500, 600 and 700. We read the computed style of every text on the Elements page, the element page, About, Calculator, Login and the page for an unknown address, in the light and the dark theme. Faustina 400 and 600 were asked for and never used, but that did not cost a download of its own. + +**How many fonts.** The app loads **two fonts**, Faustina and Inter. The only other family is the system monospace stack (`--mono`). It is on the visitor's machine and costs no download. + +**Not changed.** + +- The two families. Using Inter for the titles too would save the 26.7 kB of Faustina and one request, but it changes how every page looks, so the whole team has to decide it. +- The built folder holds the font files of all scripts (Greek, Cyrillic, Vietnamese and so on, about 200 kB in all). They are only on the disk of the VM, and no visitor downloads them unless a text needs them. + +**How we measured.** With a real browser on a production build (`vite build`, `vite preview`), on the About page, before and after. The cache header was not measured, because `vite preview` does not set one. + +**Where.** `src/main.tsx` (the imports), `--font-family-title` and `--font-family-text` in [variables.css](../src/styles/variables.css), and `index.html` (no links to Google). ## What we have not measured and could improve diff --git a/docs/VM_SETUP.md b/docs/VM_SETUP.md index b39a5df..ee60346 100644 --- a/docs/VM_SETUP.md +++ b/docs/VM_SETUP.md @@ -143,21 +143,6 @@ Check the result with the `curl` command below before deploying the frontend. The answer should be `{"data":{"__typename":"Query"}}`. -5. Let the browser keep the built files for a long time (recommended). Vite puts a hash of the content in the name of every file in `dist/assets/` (the JavaScript, the CSS and the fonts), so a changed file always gets a new name and the old one can be kept for a year. Without a header the browser asks again every time. Enable `mod_headers` and add this to the same site configuration as the proxy lines: - - ```bash - sudo a2enmod headers - sudo systemctl restart apache2 - ``` - - ```apache - - Header set Cache-Control "public, max-age=31536000, immutable" - - ``` - - Check it with `curl -sI http://it2810-XX.idi.ntnu.no/project2/assets/`: the answer should have `Cache-Control: public, max-age=31536000, immutable`. Do not set this for `index.html`, which has no hash in the name. The fonts are in the same folder, because they are part of the app and not loaded from Google (see "Serve the fonts ourselves" in [SUSTAINABILITY.md](SUSTAINABILITY.md)). This step has not been tried on the VM yet. - The VM serves plain http, so Sign in with Google does not work there (Google only accepts http for `localhost`), and the tokens would travel without encryption. See "Login" in the README. ---