I Added Analytics and Doubled the Homepage's JavaScript. I Caught It Before Anyone Else Did.

I Added Analytics and Doubled the Homepage's JavaScript. I Caught It Before Anyone Else Did.

September 15, 2026 2 min read
Build in Public MockEvalio Performance Bundle Size Web Vitals

MockEvalio's homepage JS+CSS had been deliberately kept small through route-based code splitting — every page loads only the JavaScript it needs, not the whole app up front. Adding PostHog for funnel tracking (trial starts, completions, signup clicks — the free ATS checker and mock-interview trial had no measurement at all) took one import posthog from "posthog-js" at the top of a new analytics.ts, called unconditionally from main.tsx before the app even renders.

The number that gave it away

A production build before the change: the main index chunk at 353.19 kB, 113.39 kB gzipped. The same build after: 624.31 kB, 202.28 kB gzipped. Roughly 90 KB of gzipped JavaScript added to every single page load — including pages that never call trackEvent at all — because a static import at the top of an eagerly-loaded file pulls its entire dependency graph into that file's bundle, whether or not the code path that uses it ever runs.

Why it happened specifically here

main.tsx is not lazy — it's the entry point, loaded on every route before anything else. A static import of a library that size, placed there, has exactly one outcome: that library ships on every page load, unconditionally. This is the same category of mistake the route-splitting work had already been built to prevent, reintroduced by a dependency that had nothing to do with routing at all.

The fix and how it was verified

initAnalytics() now loads posthog-js through a dynamic import(), only inside the function that runs it, and only once — trackEvent calls go through a nullable instance that's null until that import resolves. Verified two ways, not just asserted: a production build with no VITE_POSTHOG_KEY set showed zero PostHog-related strings anywhere in dist/ — Vite's dead-code elimination removing the entire unreachable branch when the env var compiles to an empty string. A second build, this time with a real test key set, showed posthog-js split cleanly into its own ~270 KB chunk, loaded separately, while the main index-*.js chunk returned to its exact pre-analytics size. Both builds also ran the full test suite clean — 46 of 46 passing — before either was treated as done.

What I still don't know

Whether this specific class of regression — a library statically imported into an eagerly-loaded entry file — has happened before or since in this codebase isn't something a single build diff can answer; it would take auditing every top-level import in main.tsx and everything it pulls in directly, not just the one that was being added this time. What this event does show concretely: verifying a feature and verifying its footprint are two different checks, and a passing test suite proves neither of them.