Skip to content

Post page: rendered article is duplicated in the RSC payload (11 KB) — inherent to dangerouslySetInnerHTML, not the NSFW wrapper #1538

Description

@feruzm

Every post page serializes the rendered article HTML into the RSC flight payload in addition to painting it in the SSR HTML. Measured on a live 4 KB post: the rendered body is a hoisted flight text row ($4f, 11,044 B) referenced from a dangerouslySetInnerHTML.__html prop. Nothing on the client reads it — EntryPageBodyViewer only enhances the SSR-painted #post-body.

Why it crosses the boundary

EntryPageStaticBody is a server component, but entry-page-content-ssr.tsx renders it inside EntryPageNsfwBodyWrapper, which is "use client":

<EntryPageNsfwBodyWrapper entry={entry}>          // "use client"
  ...
  <EntryPageStaticBody entry={entry} />            // server, dangerouslySetInnerHTML

Server subtrees passed as children through a client boundary get their element props serialized to the client. __html is an element prop, so the whole rendered article ships. Confirmed in the live payload:

3f:["$","$L4d",null,{"entry":"$1e:...","children":[... {"id":"post-body", "dangerouslySetInnerHTML":{"__html":"$4f"}} ...

Why the wrapper is a client component at all

It gates the body behind an NSFW warning, and the reveal is a real click (setShowIfNsfw(true)), so the gate must stay client-side for NSFW posts. But its predicate is only

Array.isArray(entry.json_metadata?.tags) && entry.json_metadata.tags.includes("nsfw")

For the ~99% of posts that are not tagged nsfw, the wrapper does nothing except force the article through a client boundary.

Proposal

Evaluate that same predicate on the server, and only wrap in the client gate when it is true. Non-NSFW posts render the static body directly in the server tree, so __html never enters the flight payload. NSFW posts keep exactly today's behaviour.

Two things this deliberately does not do:

  • It does not switch to isNsfwEntry(). That helper is broader (NSFW communities, title stems) than the wrapper's literal-tag check. Using it would start gating DPorn-community posts that carry no nsfw tag — a behaviour change worth considering, but a separate decision, not one to smuggle into a byte optimisation.
  • It does not touch the raw-markdown copy in the dehydrated entry ($32, 4.3 KB). That has six real client consumers including the reply/edit editor (comment/index.tsx does setText(entry.body)), so it needs an on-demand fetch pattern and its own PR.

Size

~11 KB per post page for the rendered article, plus the wrapper's own client-boundary overhead. This is the second of the page-weight items from #1533; the crawl-budget theory there is still a correlate, not a proven cause, but this is a free byte reduction with no feature cost either way.

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