Skip to content

SSR renderView churns ~12MB of mostly off-heap allocation per render of an unchanged template #1945

Description

@chrisbbreuer

Found gating server memory in stacksjs/harness (harness#11 has the full history). Rendering the same ~1,700-line view against identical props, per render:

  • bun:jsc heapStats shows only ~1.1MB of JS-heap allocation and nothing retained after GC — the projection is fine.
  • Process RSS climbs 30→209MB over the first ten renders and plateaus around 267MB, never returning to the OS — allocator high-water, not a leak. Forced Bun.gc(true) after every render caps almost nothing (253→224MB over 60 renders), so the churn is native-side (transpiler/bundler/regex/WTF-string territory), invisible to the JS heap.
  • A template containing one component tag (<Sidebar> from @stacksjs/components) produces a 211KB page — 192KB of it inline script — and burns ~9MB/render. A trivial <div>{{ x }}</div> still churns ~1.4MB/render.

Where it goes: the client-script bundler's disk cache hits every render (verified with STX_DEBUG), but everything downstream of it re-runs per render on unchanged inputs:

  • the ~190KB cached bundle is re-read from disk, re-wrapped and re-inlined,
  • the DOM-API validator re-scans the full client script (its warnings print once per render),
  • scope ids (stx_sidebar_<n>_<rand>) regenerate each render, so the output is never byte-identical and can't be memoized by callers from the outside,
  • the island wrapper is rebuilt.

The compiled island artifact is a function of (template content, config) only — the scope id is already threaded in as an IIFE argument (})(\"stx_sidebar_4_15gdi7\")), so the whole post-bundler artifact looks memoizable keyed by content hash, with the scope id (and data-stx-props) interpolated at render time. That would turn SSR of an unchanged template into string interpolation and cut per-render allocation by roughly an order of magnitude.

Numbers from bun 1.3.14, stx 0.2.188, macOS arm64. Reproduction is just renderView of any island-bearing view in a loop, watching ps -o rss= against heapStats().heapSize.

Downstream we now cache rendered pages keyed by a state revision (stacksjs/harness@75482a4), which covers polling — but every genuine state change still pays the full churn, and any hot stx SSR surface without an equivalent cache pays it per request.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions