# Measure Startup Requests

> Use when measuring what the Trilium client loads at startup — "what loads at boot?", "did this change reduce the startup bundle?", "is <dependency> lazy?", or any before/after comparison for lazy-loading / code-splitting work. Drives a headless browser through login against the running dev server, records every request, and analyzes captures (summary, heavy-dependency probe, before/after diff). Don't write a new throwaway Playwright script or inline node analyzers — both already live here.

- Skill: `triliumnext/measure-startup-requests` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add triliumnext/measure-startup-requests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/triliumnext/measure-startup-requests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: triliumnext (https://skillmd.com/u/triliumnext)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/triliumnext/measure-startup-requests

---


# Measuring Trilium startup requests

Two scripts in this folder do everything; don't reinvent them:

```bash
# 1. Capture a full startup (login → network quiet) into a JSON file:
TRILIUM_PASSWORD=<password> node .claude/skills/measure-startup-requests/capture-requests.mjs <out.json> [baseUrl]

# 2. Analyze captures:
node .claude/skills/measure-startup-requests/analyze-requests.mjs summary <capture.json> [--top N]
node .claude/skills/measure-startup-requests/analyze-requests.mjs probe <capture.json> [name ...]
node .claude/skills/measure-startup-requests/analyze-requests.mjs diff <before.json> <after.json> [--filter <regex>]
```

## Prerequisites

- The dev server must already be running (`pnpm server:start`, http://localhost:8080 by default).
  Note which checkout it serves: the capture reflects the tree the *server* runs from, not your cwd
  (verify with `curl` on a file that only exists in one tree if unsure).
- `TRILIUM_PASSWORD` env var if the instance has a password. **Without it the capture still
  "succeeds"** — it just never gets past the login page, yielding a plausible-looking ~178-request
  file that measures nothing. Validate every capture before drawing conclusions: a real one
  contains `/api/` requests (`/api/tree`, `/api/options`, ...). Zero `/api/` hits means you
  captured the login screen.
- Playwright is resolved from `packages/trilium-e2e`; the script prefers system Edge/Chrome, so no
  `playwright install` is needed.

## Workflow for lazy-loading work

1. Capture a **baseline** before changing anything: `capture-requests.mjs baseline.json`.
2. Make the change (Vite dev picks it up automatically; a fresh headless session has no HMR state).
3. Capture again and compare: `analyze-requests.mjs diff baseline.json after.json`.
4. `probe` confirms specific heavy deps stayed off the boot path.

## Timing one module graph, without logging in

The capture above needs a session. To weigh a *single* entry point instead — "what does reaching
CKEditor actually cost?" — skip the login entirely: Vite serves module URLs unauthenticated.

- URL form: `http://localhost:8080/assets/v<version>/@fs/<absolute repo path>/…/file.ts`.
- Navigate a blank page to that origin, `setContent("<html><body>")`, then take `performance.now()`
  around `await import(url).catch(() => {})`.
- **The `.catch` is what makes this work.** An ES module graph is fully fetched and instantiated
  before any of it is evaluated, so a module whose *evaluation* throws without a session ("Logged in
  session not found") still yields a valid download-and-instantiate measurement.
- Constructing an editor in that page needs `{ licenseKey: "GPL" }`, or `create()` throws
  `license-key-missing`.

This is how the "first text note is slow" delay was attributed to the module graph rather than to
the editor (dev server, 2026-08): `packages/ckeditor5/src/index.ts` ~290 ms / 216 modules and
`EditableText.tsx` ~355 ms / 328 modules, against `PopupEditor.create()` at 83 ms first and ~25 ms
after. That is why the idle preload (`preloadCommonNoteTypes` in `note_types.tsx`) is the lever —
and why the 428 KB emoji `definitionsUrl` fetch is not a suspect: `EmojiRepository.init()` fires it
without returning the promise, so it never blocks `create()`.

## Interpreting results

- **Dev-mode numbers, not production.** The dev server serves unbundled ES modules (~500+ script
  requests is normal), so sizes are uncompressed and per-module. The *module sets* and import
  chains are what matter; production chunk sizes differ.
- **Request order ≈ import discovery order.** To find what triggers a heavy load, look at the
  `seq` of the first module of that package and at the `/src/...` modules requested just before it,
  then confirm the chain by grepping for static importers.
- **Sessions are stateful.** Open tabs / the active note change what loads (e.g. a text note pulls
  CKEditor legitimately). Totals between two captures are only comparable for the same session
  state; prefer the `diff` of targeted module sets, and treat full-MB totals as indicative.
- **Never filter raw URLs.** Dev URLs embed the absolute checkout path via `/@fs/...`, so a
  worktree named e.g. `lazy-ribbon` makes every request match `/ribbon/`. The analyzer normalizes
  paths (strips host, `?v=`/`?t=` params, `/assets/vX.Y.Z`, and the `/@fs/<checkout>` prefix) —
  rely on that.
- Vite's hash-named shared chunks (`dist-XXXX.js`) are identified by their `.js.map` in
  `.cache/vite-<port>/deps/` (each dev instance has its own): `grep -o '"[^"]*node_modules/[^"]*"'
  <chunk>.js.map | ...` and count by
  package. (The 800 KB `es-toolkit`+`mdast`/`hast` chunk is CKEditor's internals, for example.)

## Reference

The default `probe` list is the set of heavy deps that were deliberately made lazy (CKEditor,
highlight.js, KaTeX, codemirror-vim, snapdom, force-graph, the LLM chat graph, ...) — if one of
them reports `LOADED` on a plain board/empty note startup, a regression sneaked in. After the
2026-06 lazy-loading work the new-layout baseline was ~557 requests / 3.75 MB / 500 scripts
(down from 810 / 8.02 MB / 745).

Both eager-load offenders this file used to list have since been fixed (re-verified 2026-08):
`applyModals` in `layout_commons.tsx` now wraps every dialog in `LazyDialog` (a dynamic `import()`
on first summons), keeping only `PopupEditor`, `CallToAction`, `Toast` and `ShortcutHintsPanel`
eager — each with a comment saying why. The Inter font is served as woff2. Don't re-report either
as a finding; measure first.

