Repository navigation
feat(browser): skip the settle phase when the document already disowns itself - #118
Merged
Merged
Conversation
…s itself; v1.18.0
Settle is ~80% of a render (measured on kohls: 8.9s of a 10.9s mean, against 1.45s
of navigation), and the plugin's store guard is `statusCode === 200 && content` — so
a page that is already non-indexable at DOMContentLoaded can never be stored no
matter how long it settles. `navigation.skipSettleWhenNonIndexable` decides the
verdict against the pre-settle DOM and returns without settling when the answer is
already no, landing the bail at ~1.7s instead of ~10.9s.
The verdict logic moves into a shared `indexVerdict()` used by both the new
pre-settle bail and the long-standing post-settle check, so the two cannot drift
apart on what "non-indexable" means.
Two properties keep it safe, both pinned by tests:
- it can only ever SKIP a render, never keep one the settled DOM would have
rejected — a canonical or robots tag injected after DOMContentLoaded is still
caught by the post-settle check, which stays authoritative;
- sitemap-listed urls are exempt, so redirect.test.ts's blast-radius guarantee
(the corpus the site itself declared is structurally out of reach) still holds.
Default false, so an unconfigured deployment renders exactly as before: a site whose
canonical or robots tag is written by script rather than served in the document would
otherwise see pages skipped that a full render would have kept.
…t on an early bail
Two gaps in the pre-settle bail, both found reviewing my own diff:
- A client-side redirect can fire before DOMContentLoaded, so a bail could land on
the DESTINATION document and return a non-indexable verdict without ever setting
`redirectedTo`. The plugin adopts a destination as its own target off that field,
so swallowing it would make a client-side move read as "the source disowned
itself" and suppress the source instead. The bail now reports the landed url the
same way the post-settle path does.
- The pre-settle `page.evaluate` can lose its execution context to a navigation
still in flight. That now falls through to the settle phase instead of throwing,
which keeps the pre-flag behavior exactly: the post-settle evaluate either fails
the same way it would have, or succeeds.
There was a problem hiding this comment.
Code Review
This pull request introduces a performance optimization via the skipSettleWhenNonIndexable configuration option, allowing the renderer to skip the settle phase if the pre-settle DOM indicates the page is non-indexable. This is supported by refactoring the indexability check into a shared indexVerdict helper and adding comprehensive unit tests. Feedback suggests wrapping the page.evaluate call in a standard try...catch block to safely handle both synchronous and asynchronous exceptions across the Puppeteer interface boundary.
… goto A review argued the non-200 arm was dead code: that the `status >= 400` response handler's `ac.abort()` makes `page.goto` reject, so a 404 never reaches the bail. Executing the suite disproved it. That abort only gates SUBRESOURCE interception (the `ac.signal.aborted` guards in the request handlers) — the main document has already answered by the time it fires, so `goto` resolves normally and a 404 would otherwise settle in full (measured: 566ms of settle once the arm was removed). The arm is restored and the test now asserts the mechanism rather than the outcome: it checks `navTotal` is set (goto RESOLVED) alongside `settle` being absent, so the saving is attributed to the bail and cannot pass for the wrong reason again. Also from the same review: trimmed the rationale that was duplicated across the config docstring, the renderer block, the README and the test header down to one statement per audience, and added the case redirect.test.ts never covered — a client-side redirect to an INDEXABLE destination with the flag on, which must still render through and serialize.
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.
Settle is ~80% of a render, and the plugin's store guard is
statusCode === 200 && content— so a page that is already non-indexable atDOMContentLoadedcan never be stored no matter how long it settles.navigation.skipSettleWhenNonIndexable(default false) decides the verdict against the pre-settle DOM right afterpage.gotoand returns without settling when the answer is already no.Measured on the kohls fleet, from the workers' own
phaseMs:navTtfbnavTotal(full goto)settlepostProcessNon-indexable renders are 12.7% of that fleet's throughput and cost the same as indexable ones (10,878 ms vs 11,013 ms mean) — we pay full price to learn the answer. A pre-settle bail lands at ~1.7 s instead, which is ~10% of fleet capacity on that corpus.
The verdict logic moves into a shared
indexVerdict()used by both the new bail and the long-standing post-settle check, so the two cannot drift on what "non-indexable" means.Not independently reviewed by an outside model — every local outside leg was unavailable (see Review coverage). The GitHub review bots are the first outside lens on this diff.
For the human reviewer
page.evaluateon the ~87% of renders that ARE indexable, to save the settle phase on the ~13% that aren't. A CDP evaluate is single-digit ms against an 8.9 s settle, so it's strongly net-positive — but it is new work on the hot path, and it's inherent: the post-settle DOM can differ, which is the whole point, so the second evaluate can't be cached away.job.isFromSitemap), because the post-settle path stores their content even when non-indexable. Bailing would change what gets cached rather than only how long it took, and that would break the blast-radius guaranteeredirect.test.tspins. The exemption is asserted in the new tests too.timings.settleis now absent for bailed renders, so the workers'phaseMs.settledescribes only renders that actually settled. That matches what the existing redirect bail already does, but it does shift the meaning of that stat if you track it.canonical-variantcase followscanonical.strictrather than introducing new policy: lenient keeps its own target and settles, strict skips. Both directions are pinned.Verification
Route: new integration test.
packages/browser/test/earlySettleSkip.test.ts— 11 tests drivingrenderOnce(real Chrome) against a local origin, asserting ontimings.settleto prove the phase was skipped rather than merely fast.The durations in that run are themselves the evidence: bailed renders 117–329 ms, settled renders 690–847 ms.
Covered: noindex skips settle; canonical-elsewhere skips settle; non-200 skips settle and still posts its status; an indexable page is unaffected and still serializes; a verdict injected after
DOMContentLoadedis still caught post-settle (the safety property — it can only skip, never wrongly keep); a sitemap url is exempt; disabled-by-default still settles;canonical.strictgoverns the variant case;indexVerdict()unit cases; a bail still reports a client-side redirect it landed on; and — the caseredirect.test.tsnever covered — a client-side redirect to an indexable destination still renders through with the flag on.Full gates, all green: browser 131, plugin 715, console 116.
npm run lintclean,format:checkclean,tscclean.Not verified: the throughput claim itself. The phase numbers above are measured production observations, but the post-change fleet effect can only be confirmed after deploy — the signal to watch is the existing
render_outcometime_msseries fortype: non-candidate, whose mean should fall from ~10.9 s toward ~1.7 s. No new instrumentation needed.Review coverage
Generated by Claude (Opus 5). No independent outside-model coverage — read from the run's own
progress.json(run-idd92c15d5):refresh token was already used, needscodex loginagy)cursor-agentnot installed (exit 127)Per the cross-model skill's degraded-run rule a same-family advisory pass is not coverage, so the GitHub review bots are the first outside lens. The fallback pass did finish after the PR was opened; its findings are logged below because two of them were real, but it does not change the coverage line.
Findings acted on before review
My own read of the diff, and the same-family pass, independently surfaced the same two defects — both fixed in 54a0bd0:
DOMContentLoaded, so a bail could land on the destination and return a verdict with noredirectedTo. That matters more than it looks:RenderQueue.js:555has a dedicated branch that deletes the source and pre-suppresses the destination when a redirect lands somewhere non-indexable — "so neither keeps rendering at full cadence" — and it only fires whenredirectedTois set. Losing it would instead suppress the source (4 rechecks over ~28 days) and leave the destination to be re-discovered and rendered from scratch. Roughly 5 renders per occurrence, in the wrong direction for a change whose whole point is load.page.evaluatewas unguarded, so an in-flight navigation destroying the execution context turned a previously-rendering page into a failed render. It now falls through to settle, which reproduces the pre-flag behavior exactly.One finding I checked and refuted (dffab0e): the pass argued the non-200 arm was dead code — that the
status >= 400handler'sac.abort()makesgotoreject, so a 404 never reaches the bail, and the/gonetest should therefore fail. It doesn't. That abort only gates subresource interception; the main document has already answered when it fires, sogotoresolves and a 404 settles in full — measured at 566 ms of wasted settle when I removed the arm on the strength of the review. The arm is restored, and the test now assertsnavTotalis set (goto resolved) alongsidesettlebeing absent, so the saving is attributed to the bail and can't pass for the wrong reason again. Worth noting the pass said plainly that it could not execute the suite; that is exactly the class of claim execution settles.Also addressed: the rationale had been duplicated across the config docstring, the renderer block, the README and the test header — trimmed to one statement per audience.
Human-Review-Need: 4 @ dffab0e