Vai al contenuto
Spedizione in 24/48h in tutta Italia
Vai al contenuto
Navigazione documentazione

Questa pagina non è ancora disponibile nella lingua scelta. È mostrata la versione EN.

Admin sidebar startup performance

Reference document for the P3 backlog row "Al caricamento delle pagine in backoffice, la sidebar ha un ritardo notevole". It defines the measured scenarios, the metric contract, the reproducible harness, and the accepted evidence for the change from a client-constructed primary navigation to a server-rendered, hydrated one.

Related: doc/public/en/architecture/frontend-delivery.md (build policy and budgets), template/default/AI.theme.md and template/sober/AI.theme.md (layout contracts), doc/public/en/complete.md § admin shell.

What changed

Before the change both admin themes shipped an empty <ul> and built the whole primary navigation in the browser from window.__MENU_DATA__ through an Alpine x-for. The navigation therefore could not exist before the theme bundle had been downloaded, parsed and executed, and it could not exist at all when JavaScript was disabled or the bundle failed.

Now both layouts render the complete ACL-filtered navigation — links, labels, icons, active item, ancestor trail and the active expanded branch — in the HTML response, mark the desktop collapsed/expanded state before paint, and let Alpine hydrate the existing nodes by stable menu id. The critical entry no longer pulls Markdown/DOMPurify, Klaro, maps, masks or the editors; those load only on pages that declare them.

Metric contract

Browser marks published by the shell (template/shared/asset/js/admin/performance.js plus the two layouts):

Mark Meaning
k0smos:admin-response-start navigation responseStart
k0smos:admin-first-contentful-paint FCP replayed as a mark
k0smos:admin-dom-content-loaded domContentLoadedEventEnd
k0smos:admin-shell-bootstrap-start theme entry begins executing
k0smos:admin-sidebar-server-rendered server markup for the drawer is parsed
k0smos:admin-sidebar-first-visible first frame in which the primary navigation holds usable controls
k0smos:admin-sidebar-interactive the layout model finished hydrating
k0smos:admin-shell-stable two animation frames after bootstrap completed

Server samples are published as Server-Timing on authenticated admin responses: k0smos-route, k0smos-view-bootstrap, k0smos-menu-collect, k0smos-menu-acl, k0smos-menu-map, k0smos-template, k0smos-terminal.

The benchmark's two headline metrics are measured with one predicate that is valid for both the old and the new rendering model, so the comparison is not biased by instrumentation that only exists on one side:

  • sidebar first visible — the first animation frame in which the primary navigation contains at least one real menu control. A streaming parse can satisfy this while the list is still growing, which is what a user actually sees.
  • sidebar stable — the first animation frame in which the document has finished parsing and that navigation carries its resolved active item ([data-menu-active="true"]). The readiness condition is what stops a partially parsed list from counting as the final state. This is the gate metric.

p95 is the nearest-rank 95th percentile. At the default sample size per cell that resolves to the worst observed sample, which is the conservative reading for an improvement claim.

When script execution is disabled or the entry bundle is blocked, the same two values are taken from the protocol-side First Contentful Paint if the response markup contains a usable navigation, and are reported as unreachable if it does not.

Reproducing the measurement

tools/benchmarks/admin-sidebar-latency.sh --variant current --iterations 10 --warmup 2

The runner:

  1. creates an isolated SQLite database and two throwaway tenant descriptors (one per admin theme) through tools/benchmarks/admin-sidebar-fixture.php; the fixture refuses any database path outside var/db/benchmark-* or the system temp directory, so a configured runtime database is unreachable;
  2. seeds a restricted operator (roles.read only) next to the default administrator, which is what makes the minimal menu and full menu scenarios differ by authorization rather than by configuration;
  3. serves the application twice with php -S and the benchmark router shim, one process per admin theme, in APP_ENV=production;
  4. drives a local Chromium over the DevTools protocol (tools/benchmarks/admin-sidebar-latency.mjs) across the scenario matrix;
  5. removes the tenants, the database and the browser profile on exit.

A baseline run uses the same harness inside a disposable worktree of the pre-change commit:

git worktree add --detach /tmp/k0smos-p3-baseline <pre-change-commit>
cp -a vendor /tmp/k0smos-p3-baseline/vendor
ln -s "$PWD/template/default/node_modules" /tmp/k0smos-p3-baseline/template/default/node_modules
ln -s "$PWD/template/sober/node_modules" /tmp/k0smos-p3-baseline/template/sober/node_modules
( cd /tmp/k0smos-p3-baseline/template/default && npx vite build )
( cd /tmp/k0smos-p3-baseline/template/sober   && npx vite build )
mkdir -p /tmp/k0smos-p3-baseline/tools/benchmarks
cp -r tools/benchmarks/. /tmp/k0smos-p3-baseline/tools/benchmarks/
ROOT_OVERRIDE=/tmp/k0smos-p3-baseline tools/benchmarks/admin-sidebar-latency.sh --variant baseline ...

vendor/ is copied rather than symlinked because Composer resolves its autoload base directory relative to the vendor directory; the copy makes the worktree load its own src/ and modules/.

Reports are written to reports/ (ignored by git). Numbers are hardware- and load-dependent: compare a baseline and a current run taken back to back on the same machine, never a stored number against a fresh run.

Scenario matrix

Dimension Values
Admin theme default, sober
Device desktop 1440×900, mobile 390×844 (DPR 3, touch)
Cache cold (cleared, cache disabled), warm
JavaScript enabled, disabled, entry bundle blocked ("failed")
Menu full authorized (administrator), minimal (restricted role)
CPU/network normal, throttled (4× CPU, ~1.6 Mbit/s, 150 ms latency)

48 cells. The scripted cells keep the full cross product — 2 themes × 2 devices × 2 cache states × 2 menu sizes × 2 load profiles = 32 — because the p95 gate is decided on them. The two no-JavaScript resilience states run cold-cache with the full menu across both themes, both devices and both load profiles (16 cells): they answer whether the navigation is reachable at all, which cache state and menu size do not change. The harness prints every cell it runs, so a reduced run is never silently reported as full coverage.

Results

Measured on 2026-08-21 on one machine, both variants back to back, 10 iterations after 2 warm-ups per cell. Regenerate the tables with:

node tools/benchmarks/admin-sidebar-report.mjs \
  --baseline reports/admin-sidebar-latency-baseline.json \
  --current  reports/admin-sidebar-latency-current.json \
  --write doc/public/en/quality/admin-sidebar-performance.md

Verdict against the P3 gate

The gate was "improve sidebar-stable p95 by at least 30%, with no regression above 10% in server menu time or the Sober theme."

  • Default — met. 15 of 16 scripted cells improve by 31–75%; the mean is −54%. The single cell below the target is default/desktop/warm/minimal-menu/normal at −24%: a three-item menu on a warm cache, where there was almost no client-side work left to remove. Where the reported problem actually lives — the full authorized menu — the improvement is 37–43% on a normal link and 51–63% on a throttled one; the largest gains overall (74–75%) are the throttled minimal-menu cells, where the old build still had to download and execute the whole entry before rendering three links.
  • Sober — no regression, and the 30% target does not apply. Sober's mean is −9% (best −22%, worst +3%). Its pre-change entry was already 71 KB, so it never carried the Default bundle's startup cost; the change adds hydration and the shared drawer helpers on top of a bundle that was close to optimal. The gate's own wording constrains Sober to "no regression above 10%", which holds: the worst cell is +3% and no cell regresses beyond that.
  • Reachability — changed categorically. With JavaScript disabled or the entry bundle blocked, the baseline exposed zero navigation controls in both themes; the current build exposes the full 104-item authorized navigation. This is the part of the row no percentage captures.
  • Server response — unchanged within noise. TTFB p95 moves by −22 ms to +40 ms across cells with no consistent direction. The menu collection, authorization and mapping code was not touched by this work, and its Server-Timing instrumentation ships with the change, so no pre-change server-menu sample exists to compare against; the absolute figures below are the new reference.

One caveat on the transferred-bytes column: php -S serves responses uncompressed, so the server-rendered navigation looks far more expensive on the wire than it is. The same admin document is 338 060 bytes raw and 22 031 bytes gzipped; a deployment behind Caddy/FrankenPHP negotiates encoding normally.

p95 of the gate metric. Lower is better; the delta is current versus baseline.

Scenario Baseline p95 Current p95 Delta Baseline p50 Current p50
default / desktop / cold / full / normal 633 ms 300 ms -53% 474 ms 261 ms
default / desktop / cold / full / throttled 8695 ms 3306 ms -62% 8401 ms 3143 ms
default / desktop / cold / minimal / normal 355 ms 245 ms -31% 304 ms 211 ms
default / desktop / cold / minimal / throttled 7425 ms 1870 ms -75% 7271 ms 1811 ms
default / desktop / warm / full / normal 608 ms 297 ms -51% 493 ms 264 ms
default / desktop / warm / full / throttled 8453 ms 3202 ms -62% 8259 ms 3121 ms
default / desktop / warm / minimal / normal 372 ms 281 ms -24% 321 ms 192 ms
default / desktop / warm / minimal / throttled 7341 ms 1860 ms -75% 7264 ms 1829 ms
default / mobile / cold / full / normal 490 ms 309 ms -37% 460 ms 255 ms
default / mobile / cold / full / throttled 8662 ms 3225 ms -63% 8149 ms 3140 ms
default / mobile / cold / minimal / normal 370 ms 223 ms -40% 296 ms 176 ms
default / mobile / cold / minimal / throttled 7327 ms 1843 ms -75% 7254 ms 1792 ms
default / mobile / warm / full / normal 556 ms 319 ms -43% 460 ms 261 ms
default / mobile / warm / full / throttled 8448 ms 3190 ms -62% 8217 ms 3138 ms
default / mobile / warm / minimal / normal 390 ms 221 ms -43% 336 ms 189 ms
default / mobile / warm / minimal / throttled 7371 ms 1936 ms -74% 7219 ms 1806 ms
sober / desktop / cold / full / normal 476 ms 378 ms -20% 400 ms 305 ms
sober / desktop / cold / full / throttled 2184 ms 1843 ms -16% 2027 ms 1802 ms
sober / desktop / cold / minimal / normal 256 ms 253 ms -1% 211 ms 200 ms
sober / desktop / cold / minimal / throttled 1328 ms 1168 ms -12% 1256 ms 1102 ms
sober / desktop / warm / full / normal 414 ms 426 ms +3% 358 ms 342 ms
sober / desktop / warm / full / throttled 2286 ms 1833 ms -20% 2035 ms 1797 ms
sober / desktop / warm / minimal / normal 243 ms 246 ms +1% 205 ms 218 ms
sober / desktop / warm / minimal / throttled 1285 ms 1155 ms -10% 1234 ms 1107 ms
sober / mobile / cold / full / normal 396 ms 360 ms -9% 368 ms 329 ms
sober / mobile / cold / full / throttled 2072 ms 1854 ms -11% 1985 ms 1796 ms
sober / mobile / cold / minimal / normal 253 ms 261 ms +3% 208 ms 195 ms
sober / mobile / cold / minimal / throttled 1345 ms 1239 ms -8% 1232 ms 1094 ms
sober / mobile / warm / full / normal 405 ms 410 ms +1% 342 ms 302 ms
sober / mobile / warm / full / throttled 2367 ms 1835 ms -22% 2022 ms 1791 ms
sober / mobile / warm / minimal / normal 269 ms 262 ms -3% 216 ms 198 ms
sober / mobile / warm / minimal / throttled 1286 ms 1109 ms -14% 1237 ms 1094 ms
Theme Cells Mean p95 change Best Worst Cells below −30%
default 16 -54% -75% -24% 1
sober 16 -9% -22% 3% 16
  • default cells below the −30% target: default/desktop/warm/js-enabled/minimal-menu/normal (-24%).
  • sober cells below the −30% target: sober/desktop/cold/js-enabled/full-menu/normal (-20%), sober/desktop/cold/js-enabled/full-menu/throttled (-16%), sober/desktop/cold/js-enabled/minimal-menu/normal (-1%), sober/desktop/cold/js-enabled/minimal-menu/throttled (-12%), sober/desktop/warm/js-enabled/full-menu/normal (3%), sober/desktop/warm/js-enabled/full-menu/throttled (-20%), sober/desktop/warm/js-enabled/minimal-menu/normal (1%), sober/desktop/warm/js-enabled/minimal-menu/throttled (-10%), sober/mobile/cold/js-enabled/full-menu/normal (-9%), sober/mobile/cold/js-enabled/full-menu/throttled (-11%), sober/mobile/cold/js-enabled/minimal-menu/normal (3%), sober/mobile/cold/js-enabled/minimal-menu/throttled (-8%), sober/mobile/warm/js-enabled/full-menu/normal (1%), sober/mobile/warm/js-enabled/full-menu/throttled (-22%), sober/mobile/warm/js-enabled/minimal-menu/normal (-3%), sober/mobile/warm/js-enabled/minimal-menu/throttled (-14%).
  • No compared cell regressed by more than 10%.

Median number of usable menu controls in the primary navigation.

Theme JavaScript Baseline links Current links
default disabled 0–0 104–104
default failed 0–0 104–104
sober disabled 0–0 104–104
sober failed 0–0 104–104

Server response and transferred bytes

Time to first byte is the only server-side figure measurable in both variants: the Server-Timing instrumentation ships with the change, so the baseline publishes none. Transferred bytes are uncompressed — the benchmark server does not negotiate content encoding.

Scenario TTFB p95 (baseline → current) Server menu p95 Server template p95 Transferred p50 (baseline → current)
default / desktop / cold / full / normal 203 ms → 217 ms 76 ms 15 ms 1434 KB → 727 KB
default / desktop / cold / minimal / normal 171 ms → 160 ms 54 ms 7 ms 1375 KB → 442 KB
default / mobile / cold / full / normal 159 ms → 199 ms 67 ms 13 ms 1434 KB → 727 KB
default / mobile / cold / minimal / normal 161 ms → 139 ms 42 ms 6 ms 1375 KB → 442 KB
sober / desktop / cold / full / normal 212 ms → 219 ms 78 ms 15 ms 270 KB → 442 KB
sober / desktop / cold / minimal / normal 169 ms → 176 ms 57 ms 7 ms 217 KB → 228 KB
sober / mobile / cold / full / normal 173 ms → 211 ms 70 ms 12 ms 270 KB → 442 KB
sober / mobile / cold / minimal / normal 171 ms → 173 ms 60 ms 7 ms 217 KB → 228 KB

Baseline run: 2026-08-21T13:17:21.885Z, 10 iterations after 2 warm-ups. Current run: 2026-08-21T12:15:30.757Z, 10 iterations after 2 warm-ups.

Build budgets

Both themes are built by the maintainer with npm run build:<theme>, which runs the shared production policy and scripts/check-theme-asset-budget.mjs against config/quality/frontend-asset-budgets.json.

Critical entry, measured on the produced artifacts (gzip -9):

Theme Baseline raw Current raw Baseline gzip Current gzip Raw budget Gzip budget
default 639 694 B 91 270 B (−85.7%) 195 239 B 29 909 B (−84.7%) 360 000 B 120 000 B
sober 71 203 B 76 343 B (+7.2%) 24 299 B 25 732 B (+5.9%) 90 000 B 32 000 B

The Default reduction comes from moving Markdown/DOMPurify, Klaro, maps, masks, storefront search and the editors behind dynamic imports; the budget file lists them as forbidden_eager_sources and required_dynamic_sources, so the check fails if any of them returns to the critical entry.

Sober grows slightly because it now carries the hydration model and the shared drawer accessibility helpers. The increase is inside its budget and inside the 10% regression tolerance the P3 gate sets for the Sober theme.

Visual evidence

tools/benchmarks/admin-sidebar-screenshots.mjs captures five reviewable shell states per theme against the same benchmark servers — desktop expanded, desktop collapsed, mobile drawer closed, mobile drawer open, and mobile with script execution disabled — using the shell's own toggle rather than a synthetic state change. Output lands in reports/ (ignored by git). The captures support the maintainer's visual review; they do not replace it.

Behavioural regressions covered by tests

Behaviour Test
Server HTML carries the authorized navigation, no x-cloak on it tests/Integration/Theme/AdminSidebarMarkupTest.php
ACL filtering shrinks the navigation for a restricted role tests/Integration/Theme/AdminSidebarMarkupTest.php
Deep route marks the active node and expands its branch server-side tests/Integration/Theme/AdminSidebarMarkupTest.php
Sober renders the same contract tests/Integration/Theme/AdminSidebarMarkupTest.php
Server-Timing samples reach the client tests/Integration/Theme/AdminSidebarMarkupTest.php
Markup/model/CSS accessibility contract tests/Unit/Theme/AdminSidebarServerRenderingContractTest.php
Mobile drawer hidden state, focus move/restore, Escape, Tab containment, breakpoint changes, active/expanded state, reduced motion template/default/test/admin-sidebar-interaction.test.js
Same for the sober drawer template/sober/test/sober-sidebar-interaction.test.js
Tooltip hover intent, pinning, keyboard focus template/default/test/tooltip.test.js, template/sober/test/sober-admin.test.js

Rollback position

A temporary compatibility flag was considered, in case markup hydration broke deployed custom themes. It is deliberately not implemented: default and sober are the only themes that own an admin layout (template/*/tpl/admin.tpl.php), and the admin template resolver falls back to default for every other theme, so a public custom theme cannot inherit the changed drawer markup. Adding a second rendering path would have meant keeping a client-only navigation alive — exactly the code this row removed. If a future theme does claim an admin layout, the contract to satisfy is the one in this document, not a flag.

Suite status at delivery

vendor/bin/phpunit --testsuite Application reports 2642 tests with 15 failures and 1 error. Every one of them reproduces with this work stashed and belongs to areas it never touches — Psapi/Wpapi admin routes, Mail and Payment settings APIs, the Robo/Tracy evaluation contracts, an Ecommerce read-model DTO, the cc1 Klaro script order, and a SoberAdminThemeCompatibilityTest assertion still expecting the pre-policy entryFileNames: 'js/[name].js' literal that the frontend delivery policy replaced. They are recorded here so a later reader does not attribute them to the sidebar work; fixing them belongs to their own owners.

Rules for future changes

  • Never rebuild the primary navigation client-side, and never hide the whole sidebar behind x-cloak; the first response must be usable on its own.
  • inert / aria-hidden on the drawer are runtime state only. Server markup that shipped them would strand the navigation when the bundle fails.
  • Keep the CSS visibility guard on the closed mobile drawer: it removes the off-canvas navigation from the tab order before hydration, and the <noscript> block must keep overriding it.
  • Menu authorization output is per user and per tenant and must never be cached across either.
  • Budgets move only with a measured bundle-composition explanation, recorded with the change review.