Skip to content

perf: lazy-load posthog-js (#406) and date-fns locales (#408) - #46

Merged
PMQ9 merged 5 commits into
wiki-v2from
feat/bundle-trim-posthog-datefns
Sep 21, 2026
Merged

PMQ9 merged 5 commits into
wiki-v2from
feat/bundle-trim-posthog-datefns

Conversation

@PMQ9

@PMQ9 PMQ9 commented Sep 21, 2026

Copy link
Copy Markdown

Fork half of the paired PR for cccvu/wiki-v2#406 and cccvu/wiki-v2#408 — two bundle-trim follow-ups from docmost#309 / ADR 0020. Super-repo PR (gitlink bump + UPSTREAM_MODIFICATIONS.md + ADR) is separate.

docmost#406 — posthog-js off the eager graph

posthog-js (~209 KB raw / ~48 KB br) sat on the eager import graph via two paths — main.tsx (init + PostHogProvider) and layout.tsx → ee/posthog-user.tsx (usePostHog) — so every self-hosted visitor downloaded and evaluated it although telemetry is never sent. CLOUD is a runtime value (window.CONFIG), so the same bundle serves cloud + self-hosted; posthog is now gated behind a runtime dynamic import, not a build-time drop (cloud telemetry is preserved).

  • New fork-owned features/analytics/: posthog-init.ts (lazy-only, the sole static import posthog) + posthog-analytics.tsx (PostHogAnalytics passthrough that dynamically inits posthog only when isCloud() && isPostHogEnabled(), and a lazyWithReload PosthogUser wrapper).
  • No PostHogProvider: usePostHog() falls back to the global singleton initPostHog() initializes, and posthog-user.tsx reads it with ?. — so the wrapper always renders <>{children}</> (stable element type → <App/> never remounts).
  • main.tsx drops the eager imports/init/provider (also fixes the latent isPostHogEnabled un-called guard bug); layout.tsx repoints its PosthogUser import.

docmost#408 — only the active date-fns locale

date-locale.ts statically imported all 12 locales (~97 KB raw / ~19 KB br). Now en-US is the static default/fallback and the other 11 are lazy chunks (one per locale). Consumers need a Locale synchronously in render (incl. non-hook utils), so the locale is preloaded before the language becomes active: preloadDateFnsLocale() is awaited before i18n.changeLanguage() at the two (and only two) app-language set points. getDateFnsLocale() stays a pure sync getter — zero flash, no store, no re-emit. Public API + en-US output unchanged.

Result & verification

  • Initial JS 789,788 → 722,673 B brotli (−67,115, −8.5%); CSS unchanged (48,435). posthog-js added to mustStayLazy; budget tightened 860,000 → 790,000.
  • pnpm --filter client test green (46 files, 342 tests, incl. new posthog-analytics.test.tsx + date-locale.test.ts); strict build (BUNDLE_BUDGET_STRICT=1) passes; bundle report confirms posthog-js + non-active locales are lazy chunks (off the initial set); touched files typecheck clean.

🤖 Generated with Claude Code

PMQ9 and others added 5 commits September 21, 2026 17:35
posthog-js (~209 KB raw / ~48 KB brotli) was on the eager import graph via two
paths — main.tsx (init + PostHogProvider) and layout.tsx -> ee/posthog-user.tsx
(usePostHog) — so every self-hosted visitor downloaded and evaluated it though
telemetry is never sent. CLOUD is a runtime value (window.CONFIG), so the same
bundle serves cloud and self-hosted; the fix gates posthog behind a runtime
dynamic import, not a build-time drop.

New fork-owned features/analytics/ owns all posthog knowledge:
- posthog-init.ts (lazy-only): the sole static `import posthog from "posthog-js"`;
  idempotent initPostHog() with the exact upstream init options.
- posthog-analytics.tsx (eager, no static posthog import): PostHogAnalytics always
  renders <>{children}</> (stable element type -> <App/> never remounts) and, only
  when isCloud() && isPostHogEnabled(), dynamically imports posthog-init and calls
  initPostHog() in an effect (best-effort). PosthogUser is a lazyWithReload wrapper
  around ee/posthog-user.tsx. No PostHogProvider: usePostHog() falls back to the
  global singleton initPostHog() initializes, and posthog-user reads it with `?.`.

main.tsx (seam docmost#120) drops the eager posthog imports/init/provider and wraps
<App/> in <PostHogAnalytics>; this also fixes the latent guard bug (isPostHogEnabled
was referenced un-called). layout.tsx repoints its PosthogUser import at the lazy
wrapper. lazy-pages.test.ts registers the new lazy source so the lazyWithReload
convention is enforced.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
date-locale.ts statically imported all 12 date-fns locales from the barrel
(~97 KB raw / ~19 KB brotli), so every visitor downloaded every locale though
only one is ever used. Now en-US is the static default/fallback and the other 11
are lazy chunks (one per locale, literal import() specifiers) loaded on demand.

Consumers need a Locale synchronously in render (incl. non-hook utils time.ts and
format-label-date.ts), so the locale is preloaded BEFORE the language becomes
active: preloadDateFnsLocale() is awaited immediately before i18n.changeLanguage()
at the two (and only two) app-language set points — user-provider.tsx and
account-language.tsx. getDateFnsLocale() therefore stays a pure synchronous getter
that is always correct, with zero flash and no subscription store / event re-emit.
Loaders are best-effort (a failed chunk keeps the en-US fallback and never
rejects). Public API (getDateFnsLocale/useDateFnsLocale/formatLocalized) and en-US
output are unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…al-JS budget

Add "posthog-js" to mustStayLazy (matches the core and its /react subpath under
node_modules/) so a future eager re-import reds the strict build. With posthog-js
(docmost#406) and 11 date-fns locales (docmost#408) off the eager graph, the initial JS set
dropped 789,788 -> 722,673 B brotli; tighten the budget ceiling 860,000 -> 790,000
(~9% headroom) to lock in the win. CSS unchanged (48,435 B).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…t the locale-switch invariant

Extract changeAppLanguage() in date-locale.ts (preload the date-fns locale, then
i18n.changeLanguage) and route both app-language switch sites (user-provider,
account-language) through it. This turns the previously-unenforced "every
changeLanguage must be preceded by preloadDateFnsLocale" invariant into an
enforceable, testable one (Architecture + Testing L3):

- date-locale.test.ts: source-scan fitness function — no client file calls
  i18n.changeLanguage() outside date-locale.ts, and both switch sites use the
  helper (mirrors lazy-pages.test.ts); an ordering test proving the locale is
  cached BEFORE the switch fires (a change-then-load reorder fails it); a
  formatLocalized non-English test (the localized-pattern branch was unguarded);
  order-independent fallback assertions (reserved ko-KR/it-IT locales).

Also: reword the overstated posthog-init.ts / posthog-analytics.test.tsx comments
(posthog-js has two static importers, both lazy-reached; the off-eager-graph
guarantee is the budget lane's, not the effect test's); reconcile the 2026-09-18
baseline to 789,788 B across vite.config.ts.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@PMQ9
PMQ9 merged commit 8d1f7fd into wiki-v2 Sep 21, 2026
@PMQ9
PMQ9 deleted the feat/bundle-trim-posthog-datefns branch September 21, 2026 23:36
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