Skip to content

Split bundle into lazy chunks; compositor-only sticky-header collapse#99

Merged
pzverkov merged 2 commits intomainfrom
feature/split-bundle-and-scroll-perf
Apr 22, 2026
Merged

Split bundle into lazy chunks; compositor-only sticky-header collapse#99
pzverkov merged 2 commits intomainfrom
feature/split-bundle-and-scroll-perf

Conversation

@pzverkov
Copy link
Copy Markdown
Owner

Load time

Vite manualChunks groups integrity + scoreboard into one lazy chunk and challenges + scenarios into another. Achievements is left for Rollup to auto-chunk (manually assigning it pulled shared constants from types.ts into the chunk, forcing the entry to statically re-import them). Each non-en locale is its own chunk via import.meta.glob. target=es2022, cssCodeSplit=true, modulePreload=false (we hand-pick what to warm).

main.ts switches to dynamic import() for achievements (via AchStub that queues events and flushes on attach), scoreboard, integrity, challenges, scenarios. afterFirstPaint() subscribes to PerformanceObserver on paint so the chained import only fires once FCP has actually landed. loadLanguage() is async; top-level await gets first paint on the selected locale.

Small Vite plugin injects rel=prefetch for the integrity and meta chunks, plus an inline <head> script that adds rel=modulepreload for the user`s locale so non-en paint fetches it in parallel with the entry.

Entry: 117 KB raw / 37 KB gzip. Lazy: achievements 13 KB raw / 3.5 KB gzip, meta 4.3 KB / 1.8 KB gzip, integrity 2 KB / 0.9 KB gzip, per-locale 0.3-6.4 KB, shared i18n 18 KB / 6.4 KB gzip.

Scroll

.sideHeader h1 collapse moves from max-height+margin+padding to transform+opacity so scroll frames no longer trigger layout. .sideBody mask-image replaced with a sticky ::before gradient. backdrop-filter kept only on .canvasHud--actions (behind prefers-reduced-motion and min-resolution gates); place/platform/backlog HUDs use a more opaque color-mix surface.

#ticketList uses one delegated click listener; renderTickets no longer re-attaches per-button handlers on every signature change. Non-header sparklines skip push() when Overview is inactive. contain: content on .ticket/.card/.regionRow/.scenarioRow/.achItem.

Test plan

  • npm run test:unit (143/143 pass; new achievements-lazy queue-then-flush coverage)
  • npm run build (clean)
  • npm run test:e2e:ci (25/25 pass on desktop)
  • Perf invariants (tests/e2e/perf-invariants.spec.ts):
    • No afterFirstPaint chunk (ach / scoreboard / scenarios / challenges / achievements barrel) starts before FCP
    • Language switch to ru / ja / hi fetches the target locale chunk and repaints the Start label
    • Sticky header collapse uses transform+opacity only; h1.offsetHeight is invariant across scroll

Load time:
- Vite manualChunks separates achievements, scoreboard/integrity/postmortem, and
  challenges/scenarios; each non-en locale emits its own chunk via
  import.meta.glob. target=es2022, cssCodeSplit=true, modulePreload=false.
- main.ts switches to dynamic import() for achievements (via AchStub that
  queues events until the real tracker attaches), scoreboard, integrity,
  challenges, and scenarios. loadLanguage is async; top-level await keeps
  first paint on the selected locale.
- Small Vite plugin injects link rel=prefetch for integrity+meta chunks and
  an inline head script that adds link rel=modulepreload for the user's
  locale so non-en paint fetches it in parallel with the entry.

Scroll:
- .sideHeader h1 collapse moves from max-height/margin/padding to
  transform+opacity (no layout work per scroll frame).
- .sideBody mask-image replaced with a sticky ::before gradient.
- backdrop-filter kept only on .canvasHud--actions (behind prefers-reduced-
  motion and min-resolution gates); place/platform/backlog HUDs use a more
  opaque color-mix surface instead.
- #ticketList uses one delegated click listener; renderTickets no longer
  re-attaches per-button handlers on every signature change.
- Non-header sparklines skip push() when the Overview tab is inactive.
- contain: content on .ticket/.card/.regionRow/.scenarioRow/.achItem.

New unit: achievements-lazy queue-then-flush behaviour.
- New tests/e2e/perf-invariants.spec.ts: no afterFirstPaint chunk starts
  before first-contentful-paint (via PerformanceObserver); language switch
  fetches the target locale chunk and repaints labels; sticky-header
  collapse runs on transform+opacity, not layout-driven properties.
- afterFirstPaint() now subscribes to the paint PerformanceObserver and
  schedules its idle callback only after FCP fires, with rAF+setTimeout
  fallbacks. rAF x2 alone was racing the actual paint event, letting the
  lazy ach chunk start before FCP in headless runs.
- manualChunks: drop postmortem from the integrity chunk (sim.ts imports
  it statically, which was dragging integrity onto the critical path);
  drop the ach assignment entirely (let Rollup auto-chunk achievements,
  otherwise shared constants from types.ts got co-packed into ach and
  forced the entry to statically import it back).
- Updated the existing parallax test to match the compositor-only contract
  (h1.offsetHeight stays constant across scroll; transform + opacity are
  what animate).
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
app-survival-android 943d741 Commit Preview URL

Branch Preview URL
Apr 22 2026, 11:06 PM

@pzverkov pzverkov self-assigned this Apr 22, 2026
@pzverkov pzverkov merged commit a40080d into main Apr 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant