From c067f7a28b84e36182c9d82ce21b36e03824a845 Mon Sep 17 00:00:00 2001 From: Ronja Date: Tue, 10 Nov 2026 21:45:35 +0100 Subject: [PATCH] perf(elements): show the drawings at most at their real size Closes #174 --- docs/AI_USAGE.md | 8 ++++++++ docs/SUSTAINABILITY.md | 17 +++++++++++++++++ src/components/elements/ElementDiagram.tsx | 6 ++++-- src/components/elements/ResultsTable.tsx | 6 ++++-- src/lib/imageUrl.ts | 10 ++++++++++ .../components/elements/ResultsTable.test.tsx | 3 +++ src/tests/pages/ElementDetailPage.test.tsx | 3 +++ 7 files changed, 49 insertions(+), 4 deletions(-) diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 3e7719c..76d3893 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -589,3 +589,11 @@ - Changed outside the Gymnast page: the element page (colors, the badge, "Try in a routine", "Add to gymnast"), "Your gymnasts" (the wrapping of the title and the list), `variables.css` (`--card-hover`), and two shared parts that other pages use too: `Button` (the `icon` prop, which changes nothing when it is not given) and `DifficultyBadge` (its colors now follow the theme, also in the results of the Elements page). - Not done on purpose: moving the error messages of the other pages (the elements results, the element page, the login, "Your gymnasts") to the three layers, because those files belong to other pull requests and the change would conflict with them. The menus of the Elements page still have their own copies of the menu classes that `menuStyles.ts` now holds. - **Limits:** The end-to-end tests need the database and the backend, and are run by hand: the course git server has no runners. A screen reader was not used: that has to be done by a person. Lighthouse was started on a production build in the last round, but the run did not finish (the computer was low on memory), so there are no Lighthouse scores from that round. "Your gymnasts" was fixed after the last check in a browser and has not been measured again, and the results table of the Elements page, which uses the changed badge, has not been looked at in dark mode. The test gymnast was made with the test login in the local database and deleted again. + +## The size of the drawings (`src/components/elements/ElementDiagram.tsx`, `src/components/elements/ResultsTable.tsx`, `src/lib/imageUrl.ts`, `docs/SUSTAINABILITY.md`) + +- **Tool:** Claude Code +- **What the team did:** Took the peer feedback that the drawings on the element page are shown far larger than their real size (issue #174), and asked for a check of every page that shows a drawing, with the numbers written down. +- **What Claude did:** Measured first, with a real browser at widths from 375 to 1920 px, and the files themselves (836 files, at most 400 px wide, 5.64 MB in total, 12.7 kB at most). The element page showed the drawing up to 1812 px wide. Then put a maximum width of 400 px on the two `` elements that exist (the element page and the cards of the Elements page below 768 px), gave both a `width` and a `height`, added a test for the two attributes, and wrote the pages, sizes and bytes in `docs/SUSTAINABILITY.md`. +- **What was decided:** No `srcset` and no smaller copies: the files are already small and only one size exists. The `height` is a typical one (200), because the drawings differ in height. +- **Limits:** The numbers are from one machine and one element page, not from a repeated measurement. The height is not exact, so the page can still move a little when a drawing loads. diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 3900b4c..b066b60 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -28,6 +28,23 @@ Catalog drawings use WebP. Result-card images are lazy-loaded and decoded asynch **Where:** image files in [data/images/](../data/images/); `` in [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx); HTTP cache headers in [app.ts](../backend/src/server/app.ts); the image endpoint is also described in [API.md](./API.md#images). +### Show the drawings at their real size + +The drawings are at most **400 px wide** (652 of the 836 files are exactly 400; the height is between 71 and 499 px) and **5.64 MB in total** (6.7 kB on average, 12.7 kB at most; the 7.4 MB in the peer feedback is the size on disk, counted in blocks). Only two places show a drawing, and neither shows it larger than the file: + +| Page | What shows a drawing | Shown size | Transferred | +| ------------------------------------------ | --------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------- | +| Element page | One drawing | At most 400 px wide (before: up to 1812 px on a 1920 px screen, 4.5 times the file) | One file, 6.3 kB in the example measured | +| Elements, below 768 px (cards) | One drawing per card, lazy-loaded | At most 400 px wide, and at most 224 px high | 20 files per page of results, about 27 to 54 kB | +| Elements, 768 px and wider (table) | No drawing | None | None | +| Variants list, Your gymnasts, Gymnast page | No drawing | None | None | + +A smaller file per screen width (`srcset`) was considered and not done: the files are already small (6.7 kB on average), there is only one size of each drawing, and a smaller copy would save a few kB per page and would need new drawings. Both `` elements have `width` and `height`, so the space is reserved before the file has loaded. The height is a typical one (200), because it differs between the drawings, so the page can move a little when a drawing is much taller or lower than that. + +**Measured** with a real browser on the running app, at widths from 375 to 1920 px (the size shown, and the bytes of the files that were loaded). + +**Where:** `DRAWING_WIDTH` and `DRAWING_HEIGHT` in [imageUrl.ts](../src/lib/imageUrl.ts); [ElementDiagram.tsx](../src/components/elements/ElementDiagram.tsx) and [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx). + ### Keep catalog data out of the frontend bundle The catalog JSON is no longer imported into the frontend to build the element detail view; the page fetches the selected item from the GraphQL API instead. The project history records the JavaScript bundle changing from **665 kB to 522 kB** after this move. On the current version, `npm run build` reports a JavaScript bundle of **548.06 kB (170.79 kB gzip)**. These are build-output sizes, not per-visit measurements; the historical before/after comparison and current build can differ because the code and build tooling have changed. diff --git a/src/components/elements/ElementDiagram.tsx b/src/components/elements/ElementDiagram.tsx index 5bd9bc3..9b92635 100644 --- a/src/components/elements/ElementDiagram.tsx +++ b/src/components/elements/ElementDiagram.tsx @@ -1,6 +1,6 @@ import { Card, CardContent } from '@/components/ui/card' import { displayCode, type ElementDetail } from '@/lib/catalog' -import { imageUrlForApp } from '@/lib/imageUrl' +import { DRAWING_HEIGHT, DRAWING_WIDTH, imageUrlForApp } from '@/lib/imageUrl' export default function ElementDiagram({ element, @@ -10,11 +10,13 @@ export default function ElementDiagram({ return (
{/* The drawings are black lines, so they keep a white ground in both themes. */} - + {`Illustration 0)}${element.name ? `: ${element.name}` : ''}`} + width={DRAWING_WIDTH} + height={DRAWING_HEIGHT} className="block h-auto max-h-[75svh] w-full object-contain" /> diff --git a/src/components/elements/ResultsTable.tsx b/src/components/elements/ResultsTable.tsx index 8ff38d8..a38bda3 100644 --- a/src/components/elements/ResultsTable.tsx +++ b/src/components/elements/ResultsTable.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom' import DifficultyBadge from '@/components/ui/DifficultyBadge' import { getDifficultyLetter } from '@/lib/difficulty' import { groupLabel, type ElementListRow } from '@/lib/elementList' -import { imageUrlForApp } from '@/lib/imageUrl' +import { DRAWING_HEIGHT, DRAWING_WIDTH, imageUrlForApp } from '@/lib/imageUrl' // On a wide screen the results are a table. On a phone and a small screen (below // the "md" breakpoint) they are cards below each other instead, because four @@ -85,12 +85,14 @@ function ResultCards({ rows }: { rows: ElementListRow[] }) { {`Illustration { event.currentTarget.hidden = true }} - className="mt-4 block h-auto max-h-56 w-full rounded-md bg-white object-contain" + className="mt-4 block h-auto max-h-56 w-full max-w-[400px] rounded-md bg-white object-contain" /> ))} diff --git a/src/lib/imageUrl.ts b/src/lib/imageUrl.ts index 3071f05..b816bb9 100644 --- a/src/lib/imageUrl.ts +++ b/src/lib/imageUrl.ts @@ -1,5 +1,15 @@ const IMAGE_PATH = '/images/' +/** + * The drawings are at most 400 px wide (652 of the 836 files are exactly 400; measured + * on all of them) and between 71 and 499 px high, 196 on average. Nothing shows them + * wider than 400 px, because a larger size is only a blurry enlargement. The height is a + * typical one: with the width it only reserves the space of the drawing before it has + * loaded, so the page does not jump. + */ +export const DRAWING_WIDTH = 400 +export const DRAWING_HEIGHT = 200 + /** * Route backend image URLs through the frontend's base path. The backend URL * still supplies the apparatus and filename, while the app base may differ diff --git a/src/tests/components/elements/ResultsTable.test.tsx b/src/tests/components/elements/ResultsTable.test.tsx index 2e71d7a..c52b245 100644 --- a/src/tests/components/elements/ResultsTable.test.tsx +++ b/src/tests/components/elements/ResultsTable.test.tsx @@ -234,6 +234,9 @@ describe('ResultsTable: the cards (a phone and a small screen)', () => { const image = /]*>/.exec(plainCard)?.[0] ?? '' expect(image).toContain(`src="${plain.imageUrl}"`) expect(image).toContain('loading="lazy"') + // The size of the file, so the card does not grow when the drawing has loaded. + expect(image).toContain('width="400"') + expect(image).toContain('height="200"') expect(image.split(/[ "]/)).not.toContain('hidden') expect(plainCard).toContain(`alt="Illustration for ${plain.code}"`) expect(namedCard).toContain( diff --git a/src/tests/pages/ElementDetailPage.test.tsx b/src/tests/pages/ElementDetailPage.test.tsx index 7432d55..09eb5c9 100644 --- a/src/tests/pages/ElementDetailPage.test.tsx +++ b/src/tests/pages/ElementDetailPage.test.tsx @@ -149,6 +149,9 @@ describe('ElementDetailPage', () => { expect(screen.getByRole('img').getAttribute('src')).toBe( 'http://images.test/UB/UB-2.305-a.webp', ) + // The size of the file, so the page does not jump when the drawing has loaded. + expect(screen.getByRole('img').getAttribute('width')).toBe('400') + expect(screen.getByRole('img').getAttribute('height')).toBe('200') expect( screen.getByRole('link', { name: /2\.305-b/ }).getAttribute('href'), ).toBe('/elements/UB-2.305-b')