Skip to content

perf: compact DOM script handling - #7976

Open
Sheraff wants to merge 7 commits into
mainfrom
codex/dom-script-micro-passes
Open

perf: compact DOM script handling#7976
Sheraff wants to merge 7 commits into
mainfrom
codex/dom-script-micro-passes

Conversation

@Sheraff

@Sheraff Sheraff commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • share the private script-attribute copier used by Solid and Vue assets
  • scan static querySelectorAll results directly instead of allocating arrays and callbacks
  • use idempotent script.remove() for Solid cleanup

Public props, rendered attributes, script matching, and lifecycle behavior are unchanged.

Bundle impact

Measured against exact main base 697ebb6ddbd433d052b6b4707938a5c595865d58:

  • affected scenarios improve by 10–40 gzip bytes
  • 10 unaffected scenarios are byte-identical
  • every isolated production hunk improves gzip
  • raw, initial gzip, and total gzip have no regressions across all 17 scenarios
  • Brotli tradeoffs in four Start contexts are disclosed in the checked-in results report

Validation

  • Solid Router: 839 client tests passed, 1 skipped; 3 server tests passed
  • Vue Router: 815 tests passed, 1 skipped
  • Solid types passed on TypeScript 5.6 through 7.0
  • Vue types: 138 passed
  • ESLint: zero errors
  • five independent semantic, compatibility, tree-shaking, test, and measurement reviews approved the final diff

Full attribution and the 17-scenario matrix are in RESULT-optimization-dom-script-passes.md.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of external and inline scripts, including attribute preservation and duplicate prevention.
    • Made script cleanup safer when scripts are removed before component unmounting.
  • Tests

    • Added coverage for script rendering, deduplication, attributes, inline content, and cleanup across supported router integrations.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Solid and Vue router script handling now uses shared attribute application, direct duplicate detection, and explicit element cleanup. New client-side tests verify external and inline script rendering, deduplication, attributes, contents, and teardown behavior.

Changes

Script handling

Layer / File(s) Summary
Asset script attributes and lifecycle
packages/solid-router/src/Asset.tsx, packages/vue-router/src/Asset.tsx
Both routers centralize eligible script attributes, detect duplicate external and inline scripts with direct iteration, and remove created scripts during cleanup.
Client-side script integration coverage
packages/solid-router/tests/Scripts.test.tsx, packages/vue-router/tests/Scripts.test.tsx
Tests verify external and inline script attributes, contents, deduplication, cleanup, and safe teardown after manual removal.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: schiller-manuel

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the DOM script handling performance changes in the Solid and Vue routers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/dom-script-micro-passes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit d8c453f

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 9m 40s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 37s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-06 15:10:30 UTC

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7976

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7976

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7976

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7976

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7976

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7976

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7976

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7976

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7976

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7976

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7976

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7976

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7976

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7976

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7976

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7976

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7976

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7976

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7976

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7976

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7976

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7976

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7976

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7976

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7976

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7976

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7976

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7976

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7976

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7976

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7976

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7976

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7976

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7976

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7976

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7976

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7976

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7976

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7976

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7976

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7976

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7976

commit: d8c453f

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 3de64ec1e0e0
  • Measured at: 2026-08-06T14:59:11.516Z
  • Baseline source: history:abf9b81b1f14
  • Dashboard: bundle-size history

The following scenarios have bundle-size changes compared with the baseline:

Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
solid-router.full 39.64 KiB -27 B (-0.07%) 39.52 KiB 113.59 KiB 35.70 KiB █▃▃▃▃▃▃▃▁▁▁▁
vue-router.full 57.72 KiB -16 B (-0.03%) 57.59 KiB 160.07 KiB 51.79 KiB █▃▃▃▃▃▃▃▁▁▁▁
solid-start.minimal 47.91 KiB -18 B (-0.04%) 47.78 KiB 140.51 KiB 42.69 KiB ████▂▂▂▂▁▁▁▁
solid-start.deferred-hydration 51.17 KiB -17 B (-0.03%) 47.84 KiB 148.55 KiB 45.59 KiB ████▂▂▂▂▁▁▁▁
solid-start.full 53.17 KiB -29 B (-0.05%) 53.04 KiB 156.23 KiB 47.22 KiB █▇▇▇▂▂▂▂▁▁▁▁
vue-start.minimal 68.61 KiB -7 B (-0.01%) 68.48 KiB 193.35 KiB 61.06 KiB █▆▆▆▂▂▂▂▁▁▁▁
vue-start.full 72.61 KiB +7 B (+0.01%) 72.48 KiB 205.85 KiB 64.53 KiB █▆▆▆▂▂▂▂▁▁▁▁

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@codspeed-hq

codspeed-hq Bot commented Aug 6, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 5.6%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 4 improved benchmarks
❌ 6 regressed benchmarks
✅ 160 untouched benchmarks
⏩ 10 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem server peak-large-page (react) 953 KB 2,166.8 KB -56.02%
Memory mem server error-paths redirect (vue) 358.2 KB 474.7 KB -24.54%
Memory mem server error-paths unmatched (react) 263.2 KB 316.2 KB -16.75%
Memory mem client navigation-churn (solid) 538.1 KB 574.3 KB -6.31%
Memory mem server error-paths not-found (react) 250.7 KB 260.7 KB -3.84%
Memory mem server server-fn-churn (react) 265 KB 275.6 KB -3.84%
Memory mem server request-churn (react) 940 KB 493.6 KB +90.44%
Memory mem client unique-location-churn (react) 762.5 KB 664.9 KB +14.68%
Memory mem server server-fn-churn (vue) 274.7 KB 263.3 KB +4.3%
Simulation ssr request loop (solid) 157.7 ms 153 ms +3.07%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing codex/dom-script-micro-passes (d8c453f) with main (abf9b81)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Sheraff
Sheraff marked this pull request as ready for review August 6, 2026 07:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/solid-router/tests/Scripts.test.tsx (1)

260-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the inline type attribute.

Both tests declare type: 'module' but only assert inline script content. A regression that omits type in setScriptAttrs still passes.

  • packages/solid-router/tests/Scripts.test.tsx#L260-L266: Assert that getInlineScript()?.getAttribute('type') is 'module'.
  • packages/vue-router/tests/Scripts.test.tsx#L215-L221: Assert that getInlineScript()?.getAttribute('type') is 'module'.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/tests/Scripts.test.tsx` around lines 260 - 266, Update
the inline script assertions in
packages/solid-router/tests/Scripts.test.tsx:260-266 and
packages/vue-router/tests/Scripts.test.tsx:215-221 to verify
getInlineScript()?.getAttribute('type') is 'module', alongside the existing
content and uniqueness checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/solid-router/src/Asset.tsx`:
- Around line 105-116: Wrap the early return in the dataScript condition with
braces in packages/solid-router/src/Asset.tsx at lines 105-116 and
packages/vue-router/src/Asset.tsx at lines 95-127, updating the lifecycle
functions containing if (dataScript). Make only this local formatting change and
avoid broader control-statement normalization.
- Around line 119-145: Change the string-children handling in Asset so it is an
else-if branch guarded by an undefined attrs.src, preventing inline script
creation when an external source is present. Preserve the existing inline
deduplication and cleanup behavior for assets without src, matching the SSR
external-script behavior.

---

Nitpick comments:
In `@packages/solid-router/tests/Scripts.test.tsx`:
- Around line 260-266: Update the inline script assertions in
packages/solid-router/tests/Scripts.test.tsx:260-266 and
packages/vue-router/tests/Scripts.test.tsx:215-221 to verify
getInlineScript()?.getAttribute('type') is 'module', alongside the existing
content and uniqueness checks.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18630d2c-bba4-4cd7-9789-c526d1a57af8

📥 Commits

Reviewing files that changed from the base of the PR and between 697ebb6 and 6015c73.

📒 Files selected for processing (4)
  • packages/solid-router/src/Asset.tsx
  • packages/solid-router/tests/Scripts.test.tsx
  • packages/vue-router/src/Asset.tsx
  • packages/vue-router/tests/Scripts.test.tsx

Comment on lines +105 to +116
for (const el of document.querySelectorAll('script[src]')) {
if ((el as HTMLScriptElement).src === normSrc) {
return
}
}

const script = document.createElement('script')

for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined && value !== false) {
script.setAttribute(
key,
typeof value === 'boolean' ? '' : String(value),
)
}
}
setScriptAttrs(script, attrs)

document.head.appendChild(script)

onCleanup(() => {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
})
onCleanup(() => script.remove())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use braces for the early returns.

Both changed lifecycle functions contain an unbraced if (dataScript) return.

  • packages/solid-router/src/Asset.tsx#L105-L116: Use braces around the if (dataScript) return at Line 94.
  • packages/vue-router/src/Asset.tsx#L95-L127: Use braces around the if (dataScript) return at Line 81.

As per coding guidelines, TSX control statements must use curly braces. Based on learnings, make this local change in the Solid adapter without broad normalization.

📍 Affects 2 files
  • packages/solid-router/src/Asset.tsx#L105-L116 (this comment)
  • packages/vue-router/src/Asset.tsx#L95-L127
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/src/Asset.tsx` around lines 105 - 116, Wrap the early
return in the dataScript condition with braces in
packages/solid-router/src/Asset.tsx at lines 105-116 and
packages/vue-router/src/Asset.tsx at lines 95-127, updating the lifecycle
functions containing if (dataScript). Make only this local formatting change and
avoid broader control-statement normalization.

Sources: Coding guidelines, Learnings

Comment on lines 119 to +145
if (typeof children === 'string') {
const typeAttr =
typeof attrs?.type === 'string' ? attrs.type : 'text/javascript'
const nonceAttr =
typeof attrs?.nonce === 'string' ? attrs.nonce : undefined
const existingScript = Array.from(
document.querySelectorAll('script:not([src])'),
).find((el) => {
if (!(el instanceof HTMLScriptElement)) return false
for (const el of document.querySelectorAll('script:not([src])')) {
if (!(el instanceof HTMLScriptElement)) {
continue
}
const sType = el.getAttribute('type') ?? 'text/javascript'
const sNonce = el.getAttribute('nonce') ?? undefined
return (
if (
el.textContent === children &&
sType === typeAttr &&
sNonce === nonceAttr
)
})

if (existingScript) {
return
) {
return
}
}

const script = document.createElement('script')
script.textContent = children

if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined && value !== false) {
script.setAttribute(
key,
typeof value === 'boolean' ? '' : String(value),
)
}
}
}
setScriptAttrs(script, attrs)

document.head.appendChild(script)

onCleanup(() => {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
})
onCleanup(() => script.remove())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use else if when attrs.src is defined.

When one asset has both src and string children, this separate if appends an inline script after the external script. The SSR branch at Lines 158-160 renders only the external script. The Vue implementation also uses else if. This creates client and server divergence.

Proposed fix
-    if (typeof children === 'string') {
+    else if (typeof children === 'string') {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof children === 'string') {
const typeAttr =
typeof attrs?.type === 'string' ? attrs.type : 'text/javascript'
const nonceAttr =
typeof attrs?.nonce === 'string' ? attrs.nonce : undefined
const existingScript = Array.from(
document.querySelectorAll('script:not([src])'),
).find((el) => {
if (!(el instanceof HTMLScriptElement)) return false
for (const el of document.querySelectorAll('script:not([src])')) {
if (!(el instanceof HTMLScriptElement)) {
continue
}
const sType = el.getAttribute('type') ?? 'text/javascript'
const sNonce = el.getAttribute('nonce') ?? undefined
return (
if (
el.textContent === children &&
sType === typeAttr &&
sNonce === nonceAttr
)
})
if (existingScript) {
return
) {
return
}
}
const script = document.createElement('script')
script.textContent = children
if (attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (value !== undefined && value !== false) {
script.setAttribute(
key,
typeof value === 'boolean' ? '' : String(value),
)
}
}
}
setScriptAttrs(script, attrs)
document.head.appendChild(script)
onCleanup(() => {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
})
onCleanup(() => script.remove())
else if (typeof children === 'string') {
const typeAttr =
typeof attrs?.type === 'string' ? attrs.type : 'text/javascript'
const nonceAttr =
typeof attrs?.nonce === 'string' ? attrs.nonce : undefined
for (const el of document.querySelectorAll('script:not([src])')) {
if (!(el instanceof HTMLScriptElement)) {
continue
}
const sType = el.getAttribute('type') ?? 'text/javascript'
const sNonce = el.getAttribute('nonce') ?? undefined
if (
el.textContent === children &&
sType === typeAttr &&
sNonce === nonceAttr
) {
return
}
}
const script = document.createElement('script')
script.textContent = children
setScriptAttrs(script, attrs)
document.head.appendChild(script)
onCleanup(() => script.remove())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-router/src/Asset.tsx` around lines 119 - 145, Change the
string-children handling in Asset so it is an else-if branch guarded by an
undefined attrs.src, preventing inline script creation when an external source
is present. Preserve the existing inline deduplication and cleanup behavior for
assets without src, matching the SSR external-script behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant