Skip to content

feat: optimise input.content serialisation#3184

Open
vwong wants to merge 4 commits into
marko-js:mainfrom
vwong:feat/optimise-input-content-serialisation
Open

feat: optimise input.content serialisation#3184
vwong wants to merge 4 commits into
marko-js:mainfrom
vwong:feat/optimise-input-content-serialisation

Conversation

@vwong
Copy link
Copy Markdown
Contributor

@vwong vwong commented May 11, 2026

Description

In many cases, we want to pass static content from grandparent to parent to child, even if the intermediate parent may have event handlers (thus state-ful). This PR introduces an optimisation that avoids serialising code to generate the otherwise static content..

Checklist:

  • I have read the CONTRIBUTING document and have signed (or will sign) the CLA.
  • I have updated/added documentation affected by my changes.
  • I have added tests to cover my changes.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: c84c9b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@marko/runtime-tags Minor
marko Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vwong
Copy link
Copy Markdown
Contributor Author

vwong commented May 11, 2026

I'm terrible at finding names, let me know if you want to change the name of the fixture to match existing conventions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b3c7554-6de7-42f7-ae9e-967e315c43a5

📥 Commits

Reviewing files that changed from the base of the PR and between bf3559c and 618f355.

📒 Files selected for processing (3)
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/test.ts
  • packages/runtime-tags/src/html/writer.ts
  • packages/runtime-tags/src/translator/util/signals.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/runtime-tags/src/html/writer.ts
  • packages/runtime-tags/src/tests/fixtures/content-with-state/test.ts
  • packages/runtime-tags/src/translator/util/signals.ts

Walkthrough

This PR makes subscription writes conditional by widening _subscribe to accept an optional subscribers value and guarding the serializer write. In writeHTMLResumeStatements it imports isReasonDynamic, computes sectionSerializeReason early, reuses it, and conditionally passes a getExprIfSerialized(...)-wrapped argument to _subscribe when a closure-scope's serialization reason is dynamic and differs from the section reason. Adds static and stateful test fixtures and a changeset declaring minor version bumps.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main optimization: avoiding serialization of input.content when it is static.
Description check ✅ Passed The description clearly relates to the changeset, explaining the optimization that avoids serializing static content passed through intermediate stateful parents.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/runtime-tags/src/translator/util/signals.ts (1)

1091-1112: 💤 Low value

Logic looks correct; consider deduplicating the section-reason computation.

The case analysis is sound: only wrap identifier with getExprIfSerialized when the closure-scopes reason is dynamic and not the same condition as the section's effective reason — otherwise the subscribers Set is guaranteed to be serialized in lock-step with the _subscribe call site (or always present), so passing the bare identifier is safe.

Minor cleanup: effectiveSectionReason here is identical to sectionSerializeReason computed below at lines 1137-1139. It also doesn't depend on the closure, so it can be hoisted out of the forEach over section.referencedClosures and reused in both places to make the "is this section being serialized" notion single-sourced.

♻️ Suggested refactor
   const body = path.node.body as t.Statement[];
   const allSignals = Array.from(getSignals(section).values());
   const scopeIdIdentifier = getScopeIdIdentifier(section);
+  const sectionSerializeReason = nonAnalyzedForceSerializedSection.has(section)
+    ? true
+    : section.serializeReason;
   forEach(section.referencedClosures, (closure) => {
     ...
-        } else {
-          const closureScopesReason = getSerializeReason(
-            closure.section,
-            closure,
-            getAccessorPrefix().ClosureScopes,
-          );
-          const effectiveSectionReason = nonAnalyzedForceSerializedSection.has(
-            section,
-          )
-            ? true
-            : section.serializeReason;
-          const subscribeArg =
-            isReasonDynamic(closureScopesReason) &&
-            !isSameReason(closureScopesReason, effectiveSectionReason)
+        } else {
+          const closureScopesReason = getSerializeReason(
+            closure.section,
+            closure,
+            getAccessorPrefix().ClosureScopes,
+          );
+          const subscribeArg =
+            isReasonDynamic(closureScopesReason) &&
+            !isSameReason(closureScopesReason, sectionSerializeReason)
               ? getExprIfSerialized(
                   closure.section,
                   closureScopesReason,
                   identifier,
                 )
               : identifier;
   ...
-  const sectionSerializeReason = nonAnalyzedForceSerializedSection.has(section)
-    ? true
-    : section.serializeReason;
🤖 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/runtime-tags/src/translator/util/signals.ts` around lines 1091 -
1112, Hoist the section-level serialization check into a single variable (e.g.,
sectionSerializeReason) computed once using
getSerializeReason/nonAnalyzedForceSerializedSection logic instead of
recomputing effectiveSectionReason inside the referencedClosures loop; then use
that sectionSerializeReason in the closure loop where closureScopesReason,
subscribeArg, and getExprIfSerialized are determined and also reuse it where
sectionSerializeReason is computed later (previously lines computing
sectionSerializeReason), so the "is this section being serialized" logic is
single-sourced and shared between addWriteScopeBuilder calls and the subsequent
code that currently duplicates the computation.
packages/runtime-tags/src/html/writer.ts (1)

1569-1577: 💤 Low value

Type for subscribers is inconsistent with writer.ts conventions.

Elsewhere in this file, optional/conditional flags use 0 | 1 (e.g. _attr_content's serializeReason?: 1 | 0, _sep, _el_resume, the _for_* serialize* params) or undefined | 1 (e.g. _serialize_if, _serialize_guard). The parameter's runtime value comes from translator-generated code that either passes a Set directly or wraps it via getExprIfSerialized, which returns the Set or a falsy value (specifically undefined in static paths). The declared type uses false, which is not a falsy marker in this codebase's serialization conventions.

Tighten the union to Set<ScopeInternals> | undefined to match the actual values emitted from the translator.

🤖 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/runtime-tags/src/html/writer.ts` around lines 1569 - 1577, The
_subscribe function's subscribers parameter is typed incorrectly as
`Set<ScopeInternals> | false`; change it to `Set<ScopeInternals> | undefined` to
match translator/runtime conventions (translator uses `getExprIfSerialized`
which yields a Set or undefined). Update the function signature for
`_subscribe(subscribers: Set<ScopeInternals> | undefined, scope:
ScopeInternals)` and keep the existing runtime check and calls to
`$chunk.boundary.state.serializer.writeCall(scope, subscribers, "add")` and
`referenceScope(scope)` unchanged.
🤖 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.

Nitpick comments:
In `@packages/runtime-tags/src/html/writer.ts`:
- Around line 1569-1577: The _subscribe function's subscribers parameter is
typed incorrectly as `Set<ScopeInternals> | false`; change it to
`Set<ScopeInternals> | undefined` to match translator/runtime conventions
(translator uses `getExprIfSerialized` which yields a Set or undefined). Update
the function signature for `_subscribe(subscribers: Set<ScopeInternals> |
undefined, scope: ScopeInternals)` and keep the existing runtime check and calls
to `$chunk.boundary.state.serializer.writeCall(scope, subscribers, "add")` and
`referenceScope(scope)` unchanged.

In `@packages/runtime-tags/src/translator/util/signals.ts`:
- Around line 1091-1112: Hoist the section-level serialization check into a
single variable (e.g., sectionSerializeReason) computed once using
getSerializeReason/nonAnalyzedForceSerializedSection logic instead of
recomputing effectiveSectionReason inside the referencedClosures loop; then use
that sectionSerializeReason in the closure loop where closureScopesReason,
subscribeArg, and getExprIfSerialized are determined and also reuse it where
sectionSerializeReason is computed later (previously lines computing
sectionSerializeReason), so the "is this section being serialized" logic is
single-sourced and shared between addWriteScopeBuilder calls and the subsequent
code that currently duplicates the computation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39b7a834-5380-46b6-842b-1cde377c0288

📥 Commits

Reviewing files that changed from the base of the PR and between dc68e86 and ea1c476.

⛔ Files ignored due to path filters (21)
  • packages/runtime-tags/src/__tests__/fixtures/basic-nested-params/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/dynamic-closure-with-function/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/hoist-only/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/hoist-only/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/input-missing-property/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/namespaced-tags/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/namespaced-tags/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/spread-to-known-rest-input-with-attr-tags-deopt/__snapshots__/html.expected/tags/wrap.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/spread-to-known-rest-input-with-attr-tags-deopt/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/csr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/csr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/dom.expected/tags/inner.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/dom.expected/tags/outer.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/dom.expected/template.hydrate.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/dom.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/html.expected/tags/inner.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/html.expected/tags/outer.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/resume-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/static-content/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/toggle-nested/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
📒 Files selected for processing (7)
  • .changeset/strong-symbols-try.md
  • packages/runtime-tags/src/__tests__/fixtures/static-content/tags/inner.marko
  • packages/runtime-tags/src/__tests__/fixtures/static-content/tags/outer.marko
  • packages/runtime-tags/src/__tests__/fixtures/static-content/template.marko
  • packages/runtime-tags/src/__tests__/fixtures/static-content/test.ts
  • packages/runtime-tags/src/html/writer.ts
  • packages/runtime-tags/src/translator/util/signals.ts

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 96.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.75%. Comparing base (dc68e86) to head (ea1c476).

Files with missing lines Patch % Lines
...ckages/runtime-tags/src/translator/util/signals.ts 95.23% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3184   +/-   ##
=======================================
  Coverage   92.75%   92.75%           
=======================================
  Files         371      371           
  Lines       47296    47318   +22     
  Branches     3376     3380    +4     
=======================================
+ Hits        43870    43891   +21     
- Misses       3394     3395    +1     
  Partials       32       32           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/runtime-tags/src/__tests__/fixtures/content-with-state/test.ts (1)

3-8: ⚡ Quick win

Place exported config before helper declarations to match test file structure guidelines.

This file currently defines a helper before the public export. Reordering to put export const config first (while keeping click as a hoisted function declaration) aligns with the expected top-down layout.

Proposed refactor
 import type { TestConfig } from "../../main.test";
 
-function click(container: Element) {
-  container.querySelector<HTMLButtonElement>("#increment")!.click();
-}
-
 export const config: TestConfig = {
   steps: [{}, click, click, click],
 };
+
+function click(container: Element) {
+  container.querySelector<HTMLButtonElement>("#increment")!.click();
+}

As per coding guidelines, **/*.{js,ts,jsx,tsx} files should be organized from “Public API (exports)” to helpers and use hoisting for top-down structure.

🤖 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/runtime-tags/src/__tests__/fixtures/content-with-state/test.ts`
around lines 3 - 8, Move the exported TestConfig declaration to the top so the
public API appears before helpers: place "export const config: TestConfig = {
steps: [{}, click, click, click] }" before the helper declaration, keeping the
"function click(container: Element) { ... }" as a hoisted function declaration
below; ensure references to "click" remain unchanged and the file still exports
the same symbol order.
🤖 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.

Nitpick comments:
In `@packages/runtime-tags/src/__tests__/fixtures/content-with-state/test.ts`:
- Around line 3-8: Move the exported TestConfig declaration to the top so the
public API appears before helpers: place "export const config: TestConfig = {
steps: [{}, click, click, click] }" before the helper declaration, keeping the
"function click(container: Element) { ... }" as a hoisted function declaration
below; ensure references to "click" remain unchanged and the file still exports
the same symbol order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d423789-6b1e-4b02-a699-40b28a03d4d8

📥 Commits

Reviewing files that changed from the base of the PR and between ea1c476 and bf3559c.

⛔ Files ignored due to path filters (11)
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/csr-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/csr.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/dom.expected/tags/inner.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/dom.expected/tags/outer.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/dom.expected/template.hydrate.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/dom.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/html.expected/tags/inner.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/html.expected/tags/outer.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/html.expected/template.js is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/resume-sanitized.expected.md is excluded by !**/__snapshots__/** and included by **
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/__snapshots__/resume.expected.md is excluded by !**/__snapshots__/** and included by **
📒 Files selected for processing (4)
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/tags/inner.marko
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/tags/outer.marko
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/template.marko
  • packages/runtime-tags/src/__tests__/fixtures/content-with-state/test.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/runtime-tags/src/tests/fixtures/content-with-state/template.marko
  • packages/runtime-tags/src/tests/fixtures/content-with-state/tags/outer.marko
  • packages/runtime-tags/src/tests/fixtures/content-with-state/tags/inner.marko

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant