Repository navigation
perf: PostHog and Google Analytics load once the page has drawn - #328
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Nov 2, 2026
…drawn posthog-js (~170 KB of JavaScript) was in Next's start-up chunk and initialised before the app ran (instrumentation-client.ts), and Google Analytics (next/script's default afterInteractive) downloaded and ran right after hydration: on the emulated phone both ran while the analysis page was building its game. PostHog now loads (a dynamic import) and initialises once the page has loaded and the browser is idle, or 5 s after the load event, with the settings instrumentation-client.ts used (_app's own init, with other settings, was ignored as a second init and is gone, as is PostHogProvider, whose hooks nothing used). Events tracked before then wait in src/lib/posthogClient and are sent once it has loaded, with the time they happened; its page view is captured then. Google Analytics uses next/script's lazyOnload. Visitors who leave within the first seconds may not be counted. whenPageIsIdle moves to src/lib for _app and the Stockfish provider. Waiting longer (idle, 1 s, idle again) was tried and dropped: it moved the analytics' start-up onto the clicks of visitors who choose on /play a second or so after the dialog appears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog's web-vitals code takes the LCP as final at the first click or key press after it has loaded, or when the page is hidden, and then holds it for 5 s, which a closing tab doesn't wait for. With PostHog loading once the page has drawn (387921b), a visitor who chose a side on /play before it had loaded sent no LCP. src/lib/largestContentfulPaint starts from instrumentation-client.ts, before the app, with no PostHog code. It sends the last largest-contentful-paint entry before the first click or key press (once that click has been handled) or before the page is hidden, as the $web_vitals event PostHog sends, with its $web_vitals_LCP_value and $web_vitals_LCP_event properties, through posthogClient's queue. PostHog itself now sends FCP, CLS and INP only. The event carries the URL of the page that loaded (main's carried the URL of whatever page the visitor was on 5 s after the tap). It leaves out web-vitals' attribution, and sends no new LCP after a back/forward-cache restore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lost LCP was measured with round 7 on top, whose dialog takes taps before PostHog starts; on round 6 alone LCP arrived. With src/lib/largestContentfulPaint, choosing a side at once on round 8's tip sends LCP in 3 of 3 runs (0 of 3 before), a 3 s pause sends exactly one, and the values match the browser's own LCP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ashtonanderson
force-pushed
the
claude/perf-round-5
branch
from
November 3, 2026 06:22
694083b to
c3d493e
Compare
ashtonanderson
force-pushed
the
claude/perf-round-6
branch
from
November 3, 2026 06:22
bcf27da to
26f66d5
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase on #330 and #332 gave round 6's commits new IDs, and the old ones are no longer on any branch. The round 5 build it was measured against keeps its old ID, marked as from before the rebase. The table of commits gains the LCP fix, 02afe94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sixth round: PostHog and Google Analytics load once the page has drawn, instead of before it. Stacked on #327 (
claude/perf-round-5); this PR's base is that branch. Details are inLOADING.md("Round 6").Why
On a phone, analysis and /play spend most of their wait running JavaScript before they can draw.
instrumentation-client.tsinitialised it before the app ran._appinitialised it a second time, which PostHog ignored.next/script's default) downloaded and ran right after hydration. On the analysis page that was while the game was being built.Change (387921b)
instrumentation-client.tsused, so its behaviour is unchanged.src/lib/posthogClient. They're sent once PostHog has loaded, with the time they happened.lazyOnload.instrumentation-client.ts(the file now only starts the LCP observer, below),_app's ignored second init, andPostHogProvider(nothing used its hooks).whenPageIsIdlegoes tosrc/lib, shared by_appand the Stockfish provider (unchanged).Results (round 5 → this branch)
Local builds with a test PostHog key (
/ingest, swallowed by the local proxy), 5 runs each, builds alternating. The phone is Chrome's 4x CPU throttling with 4G. Another project's Stockfish batch kept ada at load 95–100 of 160, so times vary.The cost is the last row. The analytics now start as the page first goes idle, which on /play is just after the dialog appears. A visitor who chooses at once waits for them.
I tried waiting longer (idle, 1 s, idle again) and dropped it. That moved the analytics' start-up onto clicks a second or so after the dialog appears, which is worse.
Analytics
Checked with PostHog's debug log, using an ordinary Chrome user agent; PostHog drops headless browsers as bots.
download_model_modal_shown, at 0.66 s) was sent at 1.3 s with its own time.After the rebase on main
#330 and #332 changed PostHog's settings. PostHog now starts with main's settings from
src/lib/posthog-browser-config.ts:/ingestThe resolution:
instrumentation-client.tsdoesn't initialise PostHog, andloadPostHogpasses main's settings. The one change to them is that LCP is sent by the observer below.Checked against main with a local stand-in for PostHog. The visit: an analysis game, a click, an uncaught error, a link to /play, a game against Maia, then leaving the page.
analysis_game_loadedandplay_game_started$exceptionand$web_vitalsLCP for visitors who tap before PostHog loads
PostHog's web-vitals script reports LCP at the first tap or key press after it has loaded, or when the page is hidden. It then holds it 5 s before sending.
The fix (02afe94):
src/lib/largestContentfulPaint, started byinstrumentation-client.tsbefore the app, with none of PostHog's code.$web_vitalsevent with$web_vitals_LCP_valueand$web_vitals_LCP_event, through the queue insrc/lib/posthogClient.web_vitals_allowed_metrics).Checked on the emulated phone, 3 runs each, with a local stand-in for PostHog:
Stockfish
Checked with
scripts/perf/chart-probe.mjson the analysis page: round 5 against rounds 6 and 7 together, 8 steps per run. Each pair was run in both orders.On the phone the run order mattered more than the code:
Either way the branch was sooner on about half the steps. Nothing here runs while the page steps through a game.
Testing
npm test(89, with main's),npx tsc --noEmitandnpm run lintpass, rebased on main after Capture PostHog web vitals again #330 and Track what visitors do, and stop losing events #332.🤖 Generated with Claude Code