From c5b37f4edc1d5146f66cf048a46a46cc9a72d94f Mon Sep 17 00:00:00 2001 From: Ronja Date: Wed, 11 Nov 2026 18:08:19 +0100 Subject: [PATCH 1/4] docs: measure Brotli and the Lucide icons and explain why we change nothing The peer feedback said that Lucide icons cannot be tree-shaken and that Brotli could help. Both were measured before changing anything: - 13 icons are imported and 13 icon modules are in the bundle (12.1 kB before minification), so the icon imports stay as they are. - Brotli at the default quality is 6% smaller than gzip, and quality 11 is 15% smaller but takes about a second per request. The VM sends gzip and no Brotli. SUSTAINABILITY.md has the numbers and the decision, VM_SETUP.md has the optional steps for Apache (not tried on the VM), and AI_USAGE.md has the entry. Refs #203 --- docs/AI_USAGE.md | 7 +++++++ docs/SUSTAINABILITY.md | 21 +++++++++++++++++++++ docs/VM_SETUP.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 44e324f..b78e0ea 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -596,3 +596,10 @@ - **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. + +## Brotli and the size of the icons (`docs/VM_SETUP.md`, `docs/SUSTAINABILITY.md`) + +- **Tool:** Claude Code +- **What the team did:** Took the peer feedback that Lucide icons cannot be tree-shaken and that Brotli could help, and wrote issue #203 with the order: measure first, change only if the claim is true. +- **What Claude did:** Measured the Lucide modules in the build with a temporary bundler plugin (not committed): the 13 icons the app imports are in the bundle, with 12 helper modules, 12.1 kB before minification, so nothing was changed. Measured gzip and Brotli (qualities 5 and 11, with the time each takes) on the built files with Node's `zlib`, and checked what the VM sends with `curl` (gzip, and no Brotli). Read the `mod_brotli` documentation and wrote the steps for the VM in `docs/VM_SETUP.md` and the numbers in `docs/SUSTAINABILITY.md`. +- **Limits:** The Apache steps have not been tried on the VM, and the documentation of `mod_brotli` does not say which of gzip and Brotli wins when both are set up (the text says so, with a check). The sizes are of the built files and not of a visit. The Lucide size is before minification, so the real share is a little smaller. The numbers are for the build of 2026-11-11, before the split per route (#176). diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 3900b4c..395665d 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -34,6 +34,27 @@ The catalog JSON is no longer imported into the frontend to build the element de **Where:** the API-backed detail query in [catalogOperations.ts](../src/lib/catalogOperations.ts) and its use in [ElementDetailPage.tsx](../src/pages/ElementDetailPage.tsx); the historical comparison is recorded in [AI_USAGE.md](./AI_USAGE.md). +### Compress what is sent, and check what the icons add + +The peer feedback (collected in #167, translated from Norwegian) said: + +> Lucide icons cannot be tree-shaken by default, but it is possible with Babel, a custom Rollup plugin or direct imports. Brotli compression can also help. + +We checked both before changing anything. **Result: we change no code. We document the Brotli steps and why we do not set them up now, and we keep the icon imports as they are.** + +**Compression.** The VM compresses the JavaScript and CSS with gzip, but not with Brotli: a request with `Accept-Encoding: br` got the 720 kB file as it was, and one with `gzip` got 225 kB (checked with `curl` on 2026-11-11, Apache 2.4.66). Brotli is supported by every current browser. We measured what it would save on the build of 2026-11-11 (one JavaScript file of 766.9 kB and one CSS file of 70.8 kB, before the split per route in #176), with Node's `zlib` on the files in `dist/assets/`: + +| | JavaScript | CSS | +| ------------------------------------------ | ------------------------------ | ------------------- | +| Uncompressed | 766,907 B | 70,762 B | +| gzip, level 6 (Apache's default) | 239,781 B | 13,183 B | +| Brotli, quality 5 (`mod_brotli`'s default) | 225,354 B (6% less than gzip) | 12,382 B (6% less) | +| Brotli, quality 11 | 203,138 B (15% less than gzip) | 11,265 B (15% less) | + +The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. They have not been tried on the VM, so the numbers above are measured on the built files and not on the wire. **Why we do not set it up now:** the default quality gives about 6% on a file that gzip already compresses by 69%; the larger gain needs a compression step in the deployment that every person who deploys would have to remember; and we cannot try it on the VM from the repository. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. + +**Icons.** The peer feedback said that the Lucide icons cannot be tree-shaken. We measured it with a small temporary plugin that listed the modules of `lucide-react` in the build (`chunk.modules` of the bundler, not committed). The app imports 13 icons by name, and exactly 13 icon modules are in the bundle, together with 12 small helper modules that every icon needs; the other 4,300 or so icons of the package are not. All 25 modules are **12.1 kB before minification** (the icons are 270–560 B each, 4.7 kB together, and the helpers 7.4 kB), out of 767 kB for the file. Importing from `lucide-react` does tree-shake with the current Vite, so nothing was changed in how the icons are imported. The package is built as ES modules and is meant to be used with named imports, as we do (`import { Plus } from 'lucide-react'`); what does not tree-shake is importing the whole `icons` object, which we do not do ([the guide of `lucide-react`](https://lucide.dev/guide/packages/lucide-react) says the same). A Babel or Rollup plugin for the icons would save at most those 12.1 kB and add a build step. + ### Reuse cached search results Apollo Client's in-memory cache stores search pages by search text, filters, and sort order. When a user returns to a search already loaded during the same app session, Apollo can display the cached results without requesting them again. Loading another page adds it to that search's cached result list. diff --git a/docs/VM_SETUP.md b/docs/VM_SETUP.md index ee60346..c63b266 100644 --- a/docs/VM_SETUP.md +++ b/docs/VM_SETUP.md @@ -145,6 +145,38 @@ Check the result with the `curl` command below before deploying the frontend. 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. +### Send the files compressed with Brotli (optional) + +Apache on the VM already compresses with gzip (`mod_deflate`). Brotli makes the JavaScript and CSS about 6% smaller than gzip when Apache compresses them on each request, and about 15% smaller when they are compressed once at build time (measured in [SUSTAINABILITY.md](SUSTAINABILITY.md)). **This has not been tried on the VM.** On 2026-11-11 the VM answered a request with `Accept-Encoding: br` without compression and one with `gzip` with `Content-Encoding: gzip`, so Brotli is not on yet. + +1. Check that the module is there, and enable it. If `brotli.load` is missing, stay on gzip: + + ```bash + ls /etc/apache2/mods-available/brotli.load + sudo a2enmod brotli + sudo apache2ctl configtest + sudo systemctl restart apache2 + ``` + +2. In the same `VirtualHost` as the proxy lines, add the types to compress (the fonts are WOFF2 and the drawings are images, which are compressed already, so they are left out): + + ```apache + AddOutputFilterByType BROTLI_COMPRESS text/html text/css text/javascript application/javascript image/svg+xml + ``` + + `BrotliCompressionQuality` is 5 by default (0–11). Do not turn it up to 11 here: Apache would compress with it on every request, and it took about one second for the JavaScript on a development machine, against 15 ms at 5. + +3. Check it, once for each kind of browser: + + ```bash + curl -sI -H 'Accept-Encoding: br' http://it2810-XX.idi.ntnu.no/project2/assets/ + curl -sI -H 'Accept-Encoding: gzip' http://it2810-XX.idi.ntnu.no/project2/assets/ + ``` + + The first should have `Content-Encoding: br` and the second `Content-Encoding: gzip`. Both should have `Vary: Accept-Encoding` (`mod_brotli` adds it). The documentation of `mod_brotli` does not say which one is used when gzip and Brotli are both set up. Other guides say it follows the order of the lines in the configuration, so if gzip is chosen for `br`, move the Brotli line above the gzip ones. A real browser sends `Accept-Encoding: gzip, deflate, br`; test with `-H 'Accept-Encoding: gzip, deflate, br'` too. + +For the smallest files, the JavaScript and CSS can be compressed at quality 11 once after `npm run build` and served as `.br` files, with the `RewriteRule` example under "Serving pre-compressed content" in the [`mod_brotli` documentation](https://httpd.apache.org/docs/2.4/mod/mod_brotli.html). That is not set up here: it is one more step for the person who deploys, for about 10% smaller files than the default. + --- ## 6. Test login on the virtual machine From 8b851e28d231617aff0ff6423fc43fce954135c8 Mon Sep 17 00:00:00 2001 From: Ronja Date: Wed, 11 Nov 2026 18:19:17 +0100 Subject: [PATCH 2/4] docs: use SetOutputFilter for Brotli, found by testing on the VM's Ubuntu and Apache A container test with Ubuntu 26.04.1 and Apache 2.4.66 (the VM's versions) showed that AddOutputFilterByType BROTLI_COMPRESS, the example in the mod_brotli documentation, is not enough on Ubuntu: deflate.conf adds the gzip filter for the same types, and a browser then gets gzip. Brotli is only sent to a client that asks for nothing but br. SetOutputFilter BROTLI_COMPRESS;DEFLATE in a LocationMatch for /project2/assets/ gives Brotli to a browser, gzip to a gzip-only client, and leaves the WOFF2 fonts alone. The module comes with the apache2 package on Ubuntu, so nothing has to be installed. Refs #203 --- docs/AI_USAGE.md | 3 ++- docs/SUSTAINABILITY.md | 2 +- docs/VM_SETUP.md | 24 +++++++++++++++--------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index b78e0ea..8c6507e 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -602,4 +602,5 @@ - **Tool:** Claude Code - **What the team did:** Took the peer feedback that Lucide icons cannot be tree-shaken and that Brotli could help, and wrote issue #203 with the order: measure first, change only if the claim is true. - **What Claude did:** Measured the Lucide modules in the build with a temporary bundler plugin (not committed): the 13 icons the app imports are in the bundle, with 12 helper modules, 12.1 kB before minification, so nothing was changed. Measured gzip and Brotli (qualities 5 and 11, with the time each takes) on the built files with Node's `zlib`, and checked what the VM sends with `curl` (gzip, and no Brotli). Read the `mod_brotli` documentation and wrote the steps for the VM in `docs/VM_SETUP.md` and the numbers in `docs/SUSTAINABILITY.md`. -- **Limits:** The Apache steps have not been tried on the VM, and the documentation of `mod_brotli` does not say which of gzip and Brotli wins when both are set up (the text says so, with a check). The sizes are of the built files and not of a visit. The Lucide size is before minification, so the real share is a little smaller. The numbers are for the build of 2026-11-11, before the split per route (#176). +- **Found by testing:** The example in the `mod_brotli` documentation (`AddOutputFilterByType BROTLI_COMPRESS …`) is not enough on Ubuntu: a first version of the steps used it, and a container test with the same Ubuntu and Apache versions as the VM showed that a browser (`Accept-Encoding: gzip, deflate, br`) still got gzip, because `deflate.conf` adds the gzip filter first. Four ways to set it up were compared in the container, and `SetOutputFilter BROTLI_COMPRESS;DEFLATE` in a `LocationMatch` gave Brotli to browsers, gzip to gzip-only clients and nothing to the fonts. The documentation does not say which filter wins, so the text says that it was found by testing. +- **Limits:** The Apache steps were tested in a container, not on the VM itself. The sizes are of the built files and not of a visit. The Lucide size is before minification, so the real share is a little smaller. The numbers are for the build of 2026-11-11, before the split per route (#176). diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 395665d..1074375 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -51,7 +51,7 @@ We checked both before changing anything. **Result: we change no code. We docume | Brotli, quality 5 (`mod_brotli`'s default) | 225,354 B (6% less than gzip) | 12,382 B (6% less) | | Brotli, quality 11 | 203,138 B (15% less than gzip) | 11,265 B (15% less) | -The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. They have not been tried on the VM, so the numbers above are measured on the built files and not on the wire. **Why we do not set it up now:** the default quality gives about 6% on a file that gzip already compresses by 69%; the larger gain needs a compression step in the deployment that every person who deploys would have to remember; and we cannot try it on the VM from the repository. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. +The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. The steps were tested in a container with the same Ubuntu and Apache versions as the VM (Brotli for a browser, gzip for a client that only accepts gzip, fonts left alone), but not on the VM itself, so the numbers above are measured on the built files and not on the wire. **Why we do not set it up now:** the default quality gives about 6% on a file that gzip already compresses by 69%; the larger gain needs a compression step in the deployment that every person who deploys would have to remember; and we cannot try it on the VM from the repository. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. **Icons.** The peer feedback said that the Lucide icons cannot be tree-shaken. We measured it with a small temporary plugin that listed the modules of `lucide-react` in the build (`chunk.modules` of the bundler, not committed). The app imports 13 icons by name, and exactly 13 icon modules are in the bundle, together with 12 small helper modules that every icon needs; the other 4,300 or so icons of the package are not. All 25 modules are **12.1 kB before minification** (the icons are 270–560 B each, 4.7 kB together, and the helpers 7.4 kB), out of 767 kB for the file. Importing from `lucide-react` does tree-shake with the current Vite, so nothing was changed in how the icons are imported. The package is built as ES modules and is meant to be used with named imports, as we do (`import { Plus } from 'lucide-react'`); what does not tree-shake is importing the whole `icons` object, which we do not do ([the guide of `lucide-react`](https://lucide.dev/guide/packages/lucide-react) says the same). A Babel or Rollup plugin for the icons would save at most those 12.1 kB and add a build step. diff --git a/docs/VM_SETUP.md b/docs/VM_SETUP.md index c63b266..9c5e06e 100644 --- a/docs/VM_SETUP.md +++ b/docs/VM_SETUP.md @@ -147,33 +147,39 @@ The VM serves plain http, so Sign in with Google does not work there (Google onl ### Send the files compressed with Brotli (optional) -Apache on the VM already compresses with gzip (`mod_deflate`). Brotli makes the JavaScript and CSS about 6% smaller than gzip when Apache compresses them on each request, and about 15% smaller when they are compressed once at build time (measured in [SUSTAINABILITY.md](SUSTAINABILITY.md)). **This has not been tried on the VM.** On 2026-11-11 the VM answered a request with `Accept-Encoding: br` without compression and one with `gzip` with `Content-Encoding: gzip`, so Brotli is not on yet. +Apache on the VM already compresses with gzip (`mod_deflate`). Brotli makes the JavaScript and CSS about 6% smaller than gzip when Apache compresses them on each request, and about 15% smaller when they are compressed once at build time (measured in [SUSTAINABILITY.md](SUSTAINABILITY.md)). **This has not been tried on the VM itself.** On 2026-11-11 the VM answered a request with `Accept-Encoding: br` without compression and one with `gzip` with `Content-Encoding: gzip`, so Brotli is not on yet. The steps below were tested on 2026-11-11 in a throwaway Docker container with Ubuntu 26.04.1 and Apache 2.4.66, the same versions as the VM, serving the built JavaScript from `/project2/assets/`. -1. Check that the module is there, and enable it. If `brotli.load` is missing, stay on gzip: +1. Enable the module. On Ubuntu it comes with the `apache2` package (`/etc/apache2/mods-available/brotli.load` and `mod_brotli.so`), so nothing has to be installed. It has no `brotli.conf`, so the settings go in the site configuration: ```bash - ls /etc/apache2/mods-available/brotli.load sudo a2enmod brotli sudo apache2ctl configtest sudo systemctl restart apache2 ``` -2. In the same `VirtualHost` as the proxy lines, add the types to compress (the fonts are WOFF2 and the drawings are images, which are compressed already, so they are left out): +2. In the same `VirtualHost` as the proxy lines (and the cache header, if it is set up), add: ```apache - AddOutputFilterByType BROTLI_COMPRESS text/html text/css text/javascript application/javascript image/svg+xml + + SetOutputFilter BROTLI_COMPRESS;DEFLATE + SetEnvIfNoCase Request_URI "\.(?:woff2?|png|jpe?g|webp|gif)$" no-gzip no-brotli + ``` - `BrotliCompressionQuality` is 5 by default (0–11). Do not turn it up to 11 here: Apache would compress with it on every request, and it took about one second for the JavaScript on a development machine, against 15 ms at 5. + The two filters are tried in this order, so a browser that accepts Brotli gets Brotli, and a client that only accepts gzip gets gzip. The second line leaves the fonts (WOFF2) and images alone, because they are compressed already. `BrotliCompressionQuality` is 5 by default (0–11). Do not turn it up to 11 here: Apache would compress with it on every request, and it took about one second for the JavaScript on a development machine, against 15 ms at 5. -3. Check it, once for each kind of browser: + **Do not use `AddOutputFilterByType BROTLI_COMPRESS …` on its own** (the example in the `mod_brotli` documentation). On Ubuntu `deflate.conf` already adds the gzip filter for the same types, and the container test showed that gzip then wins for a browser (`Accept-Encoding: gzip, deflate, br`), so Brotli is only sent to a client that asks for nothing but `br`. The documentation of `mod_brotli` does not say which filter wins, so this was found by testing. + +3. Check it: ```bash - curl -sI -H 'Accept-Encoding: br' http://it2810-XX.idi.ntnu.no/project2/assets/ + curl -sI -H 'Accept-Encoding: gzip, deflate, br' http://it2810-XX.idi.ntnu.no/project2/assets/ curl -sI -H 'Accept-Encoding: gzip' http://it2810-XX.idi.ntnu.no/project2/assets/ ``` - The first should have `Content-Encoding: br` and the second `Content-Encoding: gzip`. Both should have `Vary: Accept-Encoding` (`mod_brotli` adds it). The documentation of `mod_brotli` does not say which one is used when gzip and Brotli are both set up. Other guides say it follows the order of the lines in the configuration, so if gzip is chosen for `br`, move the Brotli line above the gzip ones. A real browser sends `Accept-Encoding: gzip, deflate, br`; test with `-H 'Accept-Encoding: gzip, deflate, br'` too. + The first (what a browser sends) should have `Content-Encoding: br` and the second `Content-Encoding: gzip`. Both should have `Vary: Accept-Encoding`. A `.woff2` file should have no `Content-Encoding`. In the container test the JavaScript of 522,290 B became 157,407 B with Brotli. + +`index.html` and the rest keep the gzip of `deflate.conf`. They are small, and they are not under `/project2/assets/`. For the smallest files, the JavaScript and CSS can be compressed at quality 11 once after `npm run build` and served as `.br` files, with the `RewriteRule` example under "Serving pre-compressed content" in the [`mod_brotli` documentation](https://httpd.apache.org/docs/2.4/mod/mod_brotli.html). That is not set up here: it is one more step for the person who deploys, for about 10% smaller files than the default. From 82d2997fe1f7df3df49922193f15584d64b99f3e Mon Sep 17 00:00:00 2001 From: Ronja Date: Wed, 11 Nov 2026 18:22:09 +0100 Subject: [PATCH 3/4] docs: say why Brotli was not set up: no time and a small gain Refs #203 --- docs/SUSTAINABILITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 1074375..7d20e24 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -40,7 +40,7 @@ The peer feedback (collected in #167, translated from Norwegian) said: > Lucide icons cannot be tree-shaken by default, but it is possible with Babel, a custom Rollup plugin or direct imports. Brotli compression can also help. -We checked both before changing anything. **Result: we change no code. We document the Brotli steps and why we do not set them up now, and we keep the icon imports as they are.** +We checked both before changing anything. **Result: we change no code, and Brotli is not set up on the VM.** The icon imports stay as they are because the check showed nothing to fix. Brotli was considered, but the team did not have the time and capacity for it and had to prioritise other things, and the gain is small (see below). The measurements and the steps are written down so that it can be done later. **Compression.** The VM compresses the JavaScript and CSS with gzip, but not with Brotli: a request with `Accept-Encoding: br` got the 720 kB file as it was, and one with `gzip` got 225 kB (checked with `curl` on 2026-11-11, Apache 2.4.66). Brotli is supported by every current browser. We measured what it would save on the build of 2026-11-11 (one JavaScript file of 766.9 kB and one CSS file of 70.8 kB, before the split per route in #176), with Node's `zlib` on the files in `dist/assets/`: @@ -51,7 +51,7 @@ We checked both before changing anything. **Result: we change no code. We docume | Brotli, quality 5 (`mod_brotli`'s default) | 225,354 B (6% less than gzip) | 12,382 B (6% less) | | Brotli, quality 11 | 203,138 B (15% less than gzip) | 11,265 B (15% less) | -The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. The steps were tested in a container with the same Ubuntu and Apache versions as the VM (Brotli for a browser, gzip for a client that only accepts gzip, fonts left alone), but not on the VM itself, so the numbers above are measured on the built files and not on the wire. **Why we do not set it up now:** the default quality gives about 6% on a file that gzip already compresses by 69%; the larger gain needs a compression step in the deployment that every person who deploys would have to remember; and we cannot try it on the VM from the repository. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. +The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. The steps were tested in a container with the same Ubuntu and Apache versions as the VM (Brotli for a browser, gzip for a client that only accepts gzip, fonts left alone), but not on the VM itself, so the numbers above are measured on the built files and not on the wire. **Why it was not done:** there were two reasons. The team did not have the time and capacity, and had to prioritise other work. And the gain is small: the default quality gives about 6% on a file that gzip already compresses by 69%, and the larger gain needs a compression step in the deployment that every person who deploys would have to remember. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. **Icons.** The peer feedback said that the Lucide icons cannot be tree-shaken. We measured it with a small temporary plugin that listed the modules of `lucide-react` in the build (`chunk.modules` of the bundler, not committed). The app imports 13 icons by name, and exactly 13 icon modules are in the bundle, together with 12 small helper modules that every icon needs; the other 4,300 or so icons of the package are not. All 25 modules are **12.1 kB before minification** (the icons are 270–560 B each, 4.7 kB together, and the helpers 7.4 kB), out of 767 kB for the file. Importing from `lucide-react` does tree-shake with the current Vite, so nothing was changed in how the icons are imported. The package is built as ES modules and is meant to be used with named imports, as we do (`import { Plus } from 'lucide-react'`); what does not tree-shake is importing the whole `icons` object, which we do not do ([the guide of `lucide-react`](https://lucide.dev/guide/packages/lucide-react) says the same). A Babel or Rollup plugin for the icons would save at most those 12.1 kB and add a build step. From 4d75ecf05829a4fe5441a251a3ee2369c87a74b5 Mon Sep 17 00:00:00 2001 From: Ronja Date: Wed, 11 Nov 2026 18:25:39 +0100 Subject: [PATCH 4/4] docs: remove the optional Brotli steps from VM_SETUP Brotli is not set up, so the steps do not belong in the VM setup guide. The decision and the finding about SetOutputFilter stay in SUSTAINABILITY.md. Refs #203 --- docs/AI_USAGE.md | 8 ++++---- docs/SUSTAINABILITY.md | 4 ++-- docs/VM_SETUP.md | 38 -------------------------------------- 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 8c6507e..42fefcb 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. -## Brotli and the size of the icons (`docs/VM_SETUP.md`, `docs/SUSTAINABILITY.md`) +## Brotli and the size of the icons (`docs/SUSTAINABILITY.md`) - **Tool:** Claude Code - **What the team did:** Took the peer feedback that Lucide icons cannot be tree-shaken and that Brotli could help, and wrote issue #203 with the order: measure first, change only if the claim is true. -- **What Claude did:** Measured the Lucide modules in the build with a temporary bundler plugin (not committed): the 13 icons the app imports are in the bundle, with 12 helper modules, 12.1 kB before minification, so nothing was changed. Measured gzip and Brotli (qualities 5 and 11, with the time each takes) on the built files with Node's `zlib`, and checked what the VM sends with `curl` (gzip, and no Brotli). Read the `mod_brotli` documentation and wrote the steps for the VM in `docs/VM_SETUP.md` and the numbers in `docs/SUSTAINABILITY.md`. -- **Found by testing:** The example in the `mod_brotli` documentation (`AddOutputFilterByType BROTLI_COMPRESS …`) is not enough on Ubuntu: a first version of the steps used it, and a container test with the same Ubuntu and Apache versions as the VM showed that a browser (`Accept-Encoding: gzip, deflate, br`) still got gzip, because `deflate.conf` adds the gzip filter first. Four ways to set it up were compared in the container, and `SetOutputFilter BROTLI_COMPRESS;DEFLATE` in a `LocationMatch` gave Brotli to browsers, gzip to gzip-only clients and nothing to the fonts. The documentation does not say which filter wins, so the text says that it was found by testing. -- **Limits:** The Apache steps were tested in a container, not on the VM itself. The sizes are of the built files and not of a visit. The Lucide size is before minification, so the real share is a little smaller. The numbers are for the build of 2026-11-11, before the split per route (#176). +- **What Claude did:** Measured the Lucide modules in the build with a temporary bundler plugin (not committed): the 13 icons the app imports are in the bundle, with 12 helper modules, 12.1 kB before minification, so nothing was changed. Measured gzip and Brotli (qualities 5 and 11, with the time each takes) on the built files with Node's `zlib`, and checked what the VM sends with `curl` (gzip, and no Brotli). Read the `mod_brotli` documentation and wrote the numbers and the decision in `docs/SUSTAINABILITY.md`. +- **Found by testing:** The example in the `mod_brotli` documentation (`AddOutputFilterByType BROTLI_COMPRESS …`) is not enough on Ubuntu: a container test with the same Ubuntu and Apache versions as the VM showed that a browser (`Accept-Encoding: gzip, deflate, br`) still got gzip, because `deflate.conf` adds the gzip filter first. Four ways to set it up were compared in the container, and `SetOutputFilter BROTLI_COMPRESS;DEFLATE` in a `LocationMatch` gave Brotli to browsers, gzip to gzip-only clients and nothing to the fonts. The documentation does not say which filter wins, so `docs/SUSTAINABILITY.md` says that it was found by testing. +- **Limits:** The Apache setup was tested in a container, not on the VM itself. The sizes are of the built files and not of a visit. The Lucide size is before minification, so the real share is a little smaller. The numbers are for the build of 2026-11-11, before the split per route (#176). diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 7d20e24..92d9a3e 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -40,7 +40,7 @@ The peer feedback (collected in #167, translated from Norwegian) said: > Lucide icons cannot be tree-shaken by default, but it is possible with Babel, a custom Rollup plugin or direct imports. Brotli compression can also help. -We checked both before changing anything. **Result: we change no code, and Brotli is not set up on the VM.** The icon imports stay as they are because the check showed nothing to fix. Brotli was considered, but the team did not have the time and capacity for it and had to prioritise other things, and the gain is small (see below). The measurements and the steps are written down so that it can be done later. +We checked both before changing anything. **Result: we change no code, and Brotli is not set up on the VM.** The icon imports stay as they are because the check showed nothing to fix. Brotli was considered, but the team did not have the time and capacity for it and had to prioritise other things, and the gain is small (see below). The measurements are written down so that it can be done later. **Compression.** The VM compresses the JavaScript and CSS with gzip, but not with Brotli: a request with `Accept-Encoding: br` got the 720 kB file as it was, and one with `gzip` got 225 kB (checked with `curl` on 2026-11-11, Apache 2.4.66). Brotli is supported by every current browser. We measured what it would save on the build of 2026-11-11 (one JavaScript file of 766.9 kB and one CSS file of 70.8 kB, before the split per route in #176), with Node's `zlib` on the files in `dist/assets/`: @@ -51,7 +51,7 @@ We checked both before changing anything. **Result: we change no code, and Brotl | Brotli, quality 5 (`mod_brotli`'s default) | 225,354 B (6% less than gzip) | 12,382 B (6% less) | | Brotli, quality 11 | 203,138 B (15% less than gzip) | 11,265 B (15% less) | -The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done; see [VM_SETUP.md](./VM_SETUP.md)). The steps for the VM are in VM_SETUP.md. The steps were tested in a container with the same Ubuntu and Apache versions as the VM (Brotli for a browser, gzip for a client that only accepts gzip, fonts left alone), but not on the VM itself, so the numbers above are measured on the built files and not on the wire. **Why it was not done:** there were two reasons. The team did not have the time and capacity, and had to prioritise other work. And the gain is small: the default quality gives about 6% on a file that gzip already compresses by 69%, and the larger gain needs a compression step in the deployment that every person who deploys would have to remember. The steps stay in VM_SETUP.md for a team that wants them. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. +The time it takes matters when the server compresses on every request: quality 5 took 15 ms for the JavaScript (the same as gzip), and quality 11 took 1,050 ms. So `mod_brotli` at its default quality saves about 14 kB of 240 kB on the first load, and the 15% of quality 11 needs the files to be compressed once when the app is built and not for each visitor (not done). The numbers above are measured on the built files and not on the wire, and nothing was changed on the VM. **Why it was not done:** there were two reasons. The team did not have the time and capacity, and had to prioritise other work. And the gain is small: the default quality gives about 6% on a file that gzip already compresses by 69%, and the larger gain needs a compression step in the deployment that every person who deploys would have to remember. If it is done later: use `SetOutputFilter BROTLI_COMPRESS;DEFLATE` in a `LocationMatch` for `/project2/assets/`, and not `AddOutputFilterByType BROTLI_COMPRESS` alone (the example in the `mod_brotli` documentation). In a container with the same Ubuntu and Apache versions as the VM, the second one gave gzip to a browser, because `deflate.conf` adds the gzip filter first. It was not tried on the VM itself. With the split per route (#176, a first file of 522 kB) the same measurement gives 164,654 B with gzip, 157,251 B with Brotli at quality 5 and 142,851 B at quality 11. **Icons.** The peer feedback said that the Lucide icons cannot be tree-shaken. We measured it with a small temporary plugin that listed the modules of `lucide-react` in the build (`chunk.modules` of the bundler, not committed). The app imports 13 icons by name, and exactly 13 icon modules are in the bundle, together with 12 small helper modules that every icon needs; the other 4,300 or so icons of the package are not. All 25 modules are **12.1 kB before minification** (the icons are 270–560 B each, 4.7 kB together, and the helpers 7.4 kB), out of 767 kB for the file. Importing from `lucide-react` does tree-shake with the current Vite, so nothing was changed in how the icons are imported. The package is built as ES modules and is meant to be used with named imports, as we do (`import { Plus } from 'lucide-react'`); what does not tree-shake is importing the whole `icons` object, which we do not do ([the guide of `lucide-react`](https://lucide.dev/guide/packages/lucide-react) says the same). A Babel or Rollup plugin for the icons would save at most those 12.1 kB and add a build step. diff --git a/docs/VM_SETUP.md b/docs/VM_SETUP.md index 9c5e06e..ee60346 100644 --- a/docs/VM_SETUP.md +++ b/docs/VM_SETUP.md @@ -145,44 +145,6 @@ Check the result with the `curl` command below before deploying the frontend. 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. -### Send the files compressed with Brotli (optional) - -Apache on the VM already compresses with gzip (`mod_deflate`). Brotli makes the JavaScript and CSS about 6% smaller than gzip when Apache compresses them on each request, and about 15% smaller when they are compressed once at build time (measured in [SUSTAINABILITY.md](SUSTAINABILITY.md)). **This has not been tried on the VM itself.** On 2026-11-11 the VM answered a request with `Accept-Encoding: br` without compression and one with `gzip` with `Content-Encoding: gzip`, so Brotli is not on yet. The steps below were tested on 2026-11-11 in a throwaway Docker container with Ubuntu 26.04.1 and Apache 2.4.66, the same versions as the VM, serving the built JavaScript from `/project2/assets/`. - -1. Enable the module. On Ubuntu it comes with the `apache2` package (`/etc/apache2/mods-available/brotli.load` and `mod_brotli.so`), so nothing has to be installed. It has no `brotli.conf`, so the settings go in the site configuration: - - ```bash - sudo a2enmod brotli - sudo apache2ctl configtest - sudo systemctl restart apache2 - ``` - -2. In the same `VirtualHost` as the proxy lines (and the cache header, if it is set up), add: - - ```apache - - SetOutputFilter BROTLI_COMPRESS;DEFLATE - SetEnvIfNoCase Request_URI "\.(?:woff2?|png|jpe?g|webp|gif)$" no-gzip no-brotli - - ``` - - The two filters are tried in this order, so a browser that accepts Brotli gets Brotli, and a client that only accepts gzip gets gzip. The second line leaves the fonts (WOFF2) and images alone, because they are compressed already. `BrotliCompressionQuality` is 5 by default (0–11). Do not turn it up to 11 here: Apache would compress with it on every request, and it took about one second for the JavaScript on a development machine, against 15 ms at 5. - - **Do not use `AddOutputFilterByType BROTLI_COMPRESS …` on its own** (the example in the `mod_brotli` documentation). On Ubuntu `deflate.conf` already adds the gzip filter for the same types, and the container test showed that gzip then wins for a browser (`Accept-Encoding: gzip, deflate, br`), so Brotli is only sent to a client that asks for nothing but `br`. The documentation of `mod_brotli` does not say which filter wins, so this was found by testing. - -3. Check it: - - ```bash - curl -sI -H 'Accept-Encoding: gzip, deflate, br' http://it2810-XX.idi.ntnu.no/project2/assets/ - curl -sI -H 'Accept-Encoding: gzip' http://it2810-XX.idi.ntnu.no/project2/assets/ - ``` - - The first (what a browser sends) should have `Content-Encoding: br` and the second `Content-Encoding: gzip`. Both should have `Vary: Accept-Encoding`. A `.woff2` file should have no `Content-Encoding`. In the container test the JavaScript of 522,290 B became 157,407 B with Brotli. - -`index.html` and the rest keep the gzip of `deflate.conf`. They are small, and they are not under `/project2/assets/`. - -For the smallest files, the JavaScript and CSS can be compressed at quality 11 once after `npm run build` and served as `.br` files, with the `RewriteRule` example under "Serving pre-compressed content" in the [`mod_brotli` documentation](https://httpd.apache.org/docs/2.4/mod/mod_brotli.html). That is not set up here: it is one more step for the person who deploys, for about 10% smaller files than the default. - --- ## 6. Test login on the virtual machine