perf: defer the shared plugin list out of the initial document (metric: byte weight / FCP) - #8
Open
phyceClaw wants to merge 1 commit into
Open
Conversation
Metric: total byte weight / FCP (~907 KB off every non-homepage document) `plugins` is a shared Inertia prop, so all 2048 records are embedded in every page even though only the header search reads them. Measured on production it is nearly the whole payload: /naturalspeech 925 KB JSON, 907 KB plugins (98%) /developers/growing 938 KB JSON, 907 KB plugins (97%) /top 969 KB JSON, 907 KB plugins (94%) It becomes a deferred prop, so the initial document omits it and the client fetches it after paint, and a once-prop, so navigating between pages does not re-send it. Without `once()` deferring would only move the bytes off the critical path rather than stop repeating them. The homepage is unaffected. PluginController@index passes its own eager `plugins`, and PropsResolver merges page props over shared props before resolution, so the DeferProp is replaced outright: the homepage still server-renders the table and advertises no deferred prop. There is a test for this, since it is the part most likely to break silently. CacheResponse had to be fixed in the same change. It keyed on the URL and the X-Inertia header only, but a deferred fetch hits the same URL with the same X-Inertia: true as a full visit, differing only in the partial headers. The two would have shared a cache entry for 60s, so a partial response carrying a single prop could be served as a whole page. The key now includes the partial and once-prop headers. Reverting just that hunk fails the accompanying test. The header search shows a pending state while the list is in flight, instead of looking like it found no matches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Metric targeted: Total byte weight → FCP
pluginsis a shared Inertia prop, so all 2048 records are embedded in the HTML of every page even though only the header search box reads them. Measured against production:plugins/naturalspeech/developers/growing/top/developersChange
Inertia::defer(...)->once(until: 10 minutes):X-Inertia-Except-Once-Propson later visits, so navigating between pages does not re-send it. This matters: deferring alone would only move the same bytes off the critical path, not stop repeating them on every navigation.The homepage is deliberately unaffected
PluginController@indexpasses its own eagerpluginsprop, andPropsResolver::resolve()doesarray_merge($shared, $props)before resolution — so on/the page prop replaces theDeferPropobject outright. The homepage still server-renders the full table (SSR and crawlers see it as today) and advertises no deferred prop, so it issues no follow-up request.That behaviour is load-bearing and would break silently, so there is a test asserting the homepage still has an eager
pluginsprop and nodeferredPropsmetadata.A cache bug this would otherwise have exposed
CacheResponsekeyed onmd5(fullUrl + X-Inertia). A deferred fetch hits the same URL with the sameX-Inertia: trueas a full visit — the only difference isX-Inertia-Partial-Data/X-Inertia-Partial-Component. So the two would have shared a cache entry for 60 seconds, and a partial response containing onlypluginscould be served in place of a whole page (or vice versa).The key now also covers the partial and once-prop headers.
test_partial_and_full_responses_do_not_share_a_cache_entryfails if that hunk is reverted — I verified this rather than assuming it.Without deferred props this bug was latent, since nothing issued partial requests. It has to be fixed in this PR, not after.
Tradeoffs worth a maintainer's opinion
once()the search index can be up to 10 minutes old within a single session, while the homepage table stays fresh. Install counts move slowly so this seemed a fair trade, but the TTL is one argument — or drop->once(...)entirely and accept the re-fetch per navigation.Verification
vendor/bin/pint --dirty,npm run build,vue-tsc --noEmitall pass; suite green (7 passed). ESLint onAppHeader.vuereports 3import/ordererrors — identical tomain, since this branch adds no imports there.Interaction with the other open PRs
share(); whichever merges second should keepgetClientPlugins()inside theInertia::defer()closure.AppHeader.vuetoo. Conflicts there are in different hunks (imports/search wiring vs. the pending state).🤖 Generated with Claude Code