Appearance
Performance
Agisse should feel instant, also on a mid-range phone on 4G. This page has the targets, how to measure them, and the latest numbers. The raw numbers are in eval/results/perf-baseline.json.
Targets
| What | Target | Baseline (2026-10-14) | Latest (2026-10-15) |
|---|---|---|---|
| Landing page LCP, Lighthouse mobile | under 1.5 s | 6.5 s on agisse.xyz | 1.7 s on agisse.xyz (1.4 s with devtools throttling) |
| Jobs LCP, Lighthouse mobile | under 2.5 s | about 5 s (estimated, see below) | 1.4 s locally, compressed (see below) |
| Initial JS, gzipped | under 150 kB | 782 kB | 129 kB |
| API round trips | no more than a route needs | /api/me 10, facets 7 | see API below |
Measure it yourself
Bundle. After npm run build:
sh
npx tsx scripts/bundle-size.tsIt prints the gzipped JS and CSS the first page load downloads, and the biggest packages in that JS.
CI runs it with --check and fails when the initial JS grows more than 10% over scripts/bundle-budget.json. A PR that makes the bundle smaller lowers that number; one that needs more raises it on purpose and says why.
API. Every /api, /mcp and /.well-known response carries a Server-Timing header with the number of database round trips and their time. Open DevTools → Network → Timing, or:
sh
curl -s -o /dev/null -D - http://localhost:5173/api/me -b 'better-auth.session_token=…' | grep -i server-timing
# server-timing: db;dur=148;desc="10 queries", total;dur=50db is the sum of all queries, so queries that run in parallel can add up to more than total.
Lighthouse. On the built app (npm run build, then npx vite preview):
sh
npx lighthouse@12 http://localhost:4173/ --only-categories=performance --chrome-flags="--headless=new"The preview server sends assets uncompressed, while Cloudflare compresses them in production, so local times come out about three times slower. Compare local runs with each other, not with the targets. For signed-in pages, put a session cookie in a Chromium profile and pass --disable-storage-reset --chrome-flags="--user-data-dir=…". Lighthouse's --extra-headers cookie only reaches the HTML, not the app's API calls.
Baseline, 2026-10-14
Bundle. The whole app is one 2.4 MB script (782 kB gzipped), plus 46 kB of CSS. Only the maps and the PDF reader load later. The biggest parts, minified:
| Package | kB |
|---|---|
posthog-js | 306 |
react-dom | 208 |
@assistant-ui/core + react | 260 |
@base-ui/react | 140 |
motion-dom + framer-motion | 125 |
src/routes/Jobs.tsx | 59 |
Lighthouse, agisse.xyz signed out. Landing: score 72, FCP 1.1 s, LCP 6.5 s, TBT 260 ms, CLS 0. The hero heading only paints after the full app script has loaded and run. PostHog also loads its session recorder and surveys on the landing page. The docs: score 100, LCP 1.5 s.
Lighthouse, agisse.xyz signed out, 2026-10-15 (after the code splitting in #91–#95 and #98). Landing: score 98, FCP 1.7 s, LCP 2.3 s, TBT 27 ms, CLS 0. The LCP element is still the hero heading; 73% of its time is render delay, the wait for the app script to draw it, and the 41 kB stylesheet blocks the first paint for about 0.3 s. The docs: score 99, LCP 1.8 s.
Landing heading in index.html, 2026-10-15. The crawler copy in index.html now carries Landing's header and hero classes and text, so the first paint already shows the final heading at its final size, and React's copy of it is not a larger LCP candidate. Local preview with devtools throttling (150 ms, 1.6 Mbit/s, 4× CPU), three runs: LCP 3.66 s → 3.06 s, now equal to FCP. The next lever is FCP itself: the stylesheet.
Lighthouse, agisse.xyz signed out, 2026-10-15 evening (after #202–#205: the static heading and a 12 kB entry stylesheet). Landing, three runs: score 99, FCP 1.7 s, LCP 1.7 s, TBT 20–30 ms, CLS 0. LCP now equals FCP. With devtools throttling instead of the simulated default: FCP 1.4 s, LCP 1.4 s, under the target. The simulated number stays higher because Lighthouse's model counts the high-priority module scripts (132 kB entry) as blocking the first paint, though the browser runs them after it. Marking them fetchpriority="low" cut the simulated FCP from 1.8 to 0.8 s locally, but not the LCP, so it isn't shipped.
The app's styles out of the first stylesheet, 2026-10-15. routes.css styles only signed-in screens, so it now loads with the signed-in app and its shell instead of with every page. The landing's stylesheet: 219 → 148 kB, 37 → 25 kB gzipped. Same throttled preview, seven runs: FCP and LCP 3.07 s → 2.34 s. Screenshots of the landing, sign-in and six signed-in pages are pixel-identical before and after.
Only the first paint's utilities in the first stylesheet, 2026-10-15. Tailwind used to build one sheet of utilities from every file. base.css now builds them only from the files in the initial script (its @source list; scripts/bundle-size.ts --check fails when one is missing), and app.css builds the full set with the signed-in shell. The landing's stylesheet: 148 → 70 kB, 25 → 14 kB gzipped. Same throttled preview, seven runs: FCP and LCP 2.35 s → 1.58 s. The same eight pages are pixel-identical before and after; the signed-in app downloads about 7 kB gzipped more, once.
The legacy primitives out of the first stylesheet, 2026-10-15. components.css (buttons, fields, chips, badges of the older screens) is used only after sign-in; the first paint needed one rule from it, the logo's size, which moved to base.css. It now loads with the signed-in shell. The landing's stylesheet: 70 → 54 kB, 14.3 → 10.9 kB gzipped. Same throttled preview, seven runs: FCP and LCP 1.58 s → 1.41 s. The same eight pages are pixel-identical before and after.
Signed-in pages one by one, 2026-10-15. The signed-in app loaded every page at once; now Home, Jobs, onboarding and Prepare's frame come with it and every other page is fetched on its first visit, behind the same skeleton as the first load. What Jobs downloads before it draws: 679 → 571 kB gzipped. Lazy pages made the bundler split modules the landing shares with them into seven small extra files, which cost the landing 160 ms of first paint; one chunk group in vite.config.ts keeps everything src/main.tsx imports in one file. Same throttled preview, seven runs: landing FCP and LCP 1.42 s → 0.95 s (the stylesheet now shares the line with two downloads instead of four); signed-in Jobs, five runs, heading drawn 13.7 s → 11.8 s. The landing's script is 8 kB gzipped larger (icons and menu parts of later pages that reach the group through lucide-react's and @base-ui/react's index files) and finishes about 150 ms later.
Signed-in Jobs, compressed, 2026-10-15. The preview behind a small gzip proxy, so the transfer sizes match production, with a synthetic returning account (session cookie and signed-in hint in the Chromium profile), Lighthouse 12 mobile on /jobs?view=feed, three runs: score 100, FCP 0.6–0.8 s, LCP 1.4 s, CLS 0.009. With devtools throttling instead of the simulated default: FCP 1.1–2.0 s, LCP 2.5–2.9 s. The LCP element is the first role's title, so the time includes /api/vacancies against the dev database. The page is bound by bandwidth: about 300 kB gzipped of script before the feed draws (entry 127, signed-in app 97, shell 72). Preloading the signed-in files from index.html was tried and dropped: on a throttled link they shared the line with the entry, which then finished 3.8 s later, and the feed drew no sooner.
Lighthouse, local preview (uncompressed, so relative only). Landing, sign-in, Home and Jobs all land at FCP about 14.3 s, because all four wait for the same script. Home has a CLS of 0.086, Jobs 0.012. Scaled to compressed transfer, Jobs would be around 5 s.
Requests before Home shows data. Four steps, one after the other: the HTML, the script, /api/auth/get-session, then /api/me, and only then /api/referral, /api/contacted, /api/jobs and /api/due in parallel. The Jobs feed waits for /api/vacancies (about 400 ms locally) and /api/vacancies/facets (about 600 ms).
API, warm, against the dev database, median of five. Every signed-in route pays two round trips for the session check.
| Route | Round trips | Time |
|---|---|---|
/api/auth/get-session | 1 | 19 ms |
/api/me | 10 | 53 ms |
/api/due, /api/jobs | 4 | 45 ms |
/api/contacted | 3 | 43 ms |
/api/vacancies | 5 | 80–100 ms |
/api/vacancies/facets | 7 | 544 ms |
/api/public/stats | 1, then 0 | edge cache |
Sentry tracing, measured 2026-10-14 in npm run dev against a local stub collector, median of 30. Tracing every request adds about 6 ms to a cached /api/public/stats (18 → 24 ms) and 11 ms to /api/me (68 → 79 ms), and sends one envelope of 5 kB (stats) to 20 kB (/api/me) per request. Errors are reported either way, so Agisse traces one request in ten.
What comes next
- Load the landing page without the app, and the app's pages one by one (
React.lazy). PostHog, motion and the assistant load late. - Draw the page layout at once and fill it in, instead of waiting on
get-sessionand then/api/me. /api/meloads the user twice (once in the session check). The feed facets are the same for everyone, so they can come from the edge cache.- Hashed assets skip the Worker and are cached for a year.