Skip to content

[Bug]: @storyblok/react/rsc barrel pulls live-editing bridge + ProseMirror runtime into client bundles #583

@kile-lindgren

Description

@kile-lindgren

Package

@storyblok/react

Bug Description

Summary

Any value import from @storyblok/react/rsc — including helpers that have no runtime client requirement, such as storyblokEditable — causes Next.js (App Router, Turbopack) to ship a ~155 KB gzipped client chunk to the route. That chunk contains the live-editing bridge loader (storyblok-v2-latest URL constant, storyblok-javascript-bridge script element id) and the entire ProseMirror runtime that backs TipTap.

The leak persists when @storyblok/react/rsc is used exactly per the official Next.js docs (storyblokInit + apiPlugin + <StoryblokProvider> + <StoryblokStory> + real cdn/stories fetch). It is intrinsic to the package's barrel structure, not to user setup.

Environment

  • @storyblok/react@6.1.2
  • @storyblok/js@5.1.2
  • @storyblok/richtext@4.1.2
  • storyblok-js-client@7.3.0
  • next@16.0.10 (App Router, Turbopack — the Next 16 default)
  • react@19.2.3, react-dom@19.2.3
  • Node 22

Reproduction

Test harness: https://github.com/kile-lindgren/storyblok-bundle-leak

git clone https://github.com/kile-lindgren/storyblok-bundle-leak
cd storyblok-bundle-leak
pnpm install
cp .env.example .env.local        # paste any Storyblok delivery token (eu region) for a demo space
pnpm test                          # = next build && node scripts/measure.mjs

The harness contains seven routes, each isolating one import pattern from @storyblok/react/rsc, plus an /official route that mirrors the docs end-to-end. The measurement script walks .next/server/app/<route>/page_client-reference-manifest.js for each route, sums the entryJSFiles chunks, and searches them for minification-safe marker strings. Results are written to results/run-<timestamp>.md.

Latest measurement run on main (commit 018ed0f):
https://github.com/kile-lindgren/storyblok-bundle-leak/blob/018ed0f/results/run-2026-04-22T06-25-12-061Z.md

Measured impact

Route Imports from /rsc Chunks added Raw Gz Bridge URL Bridge script-id ProseMirror Schema+NodeType
(framework baseline) n/a 9 542.9 KB 165.5 KB no no no no
/control nothing 0 0 B 0 B no no no no
/type-only import type { SbBlokData } 0 0 B 0 B no no no no
/editable-only storyblokEditable (value) 2 764.5 KB 154.6 KB YES YES YES YES
/server-component-only setComponents, StoryblokServerComponent 2 764.5 KB 154.6 KB YES YES YES YES
/richtext StoryblokServerRichText + 1 TipTap extension 2 764.5 KB 154.6 KB YES YES YES YES
/preview StoryblokStory 2 764.5 KB 154.6 KB YES YES YES YES
/official docs-faithful (storyblokInit + apiPlugin + <StoryblokProvider> + cdn/stories fetch + <StoryblokStory>) 2 764.5 KB 154.6 KB YES YES YES YES

Numbers are bytes added on top of the framework baseline, not totals. Every leaky route references the same two chunks (4ee6a58cbb237f3d.js + 1386ec3cd8a4bd63.js); Turbopack collapses the entire /rsc import surface into one shared chunk and ships it to every route that touches it.

Markers:

  • Bridge URL = literal storyblok-v2-latest
  • Bridge script-id = literal storyblok-javascript-bridge (the <script id="..."> the bridge loader injects)
  • ProseMirror = literal ProseMirror (CSS class names like ProseMirror-hideselection)
  • Schema+NodeType = both Schema and NodeType substrings present in the same chunk

Root cause

Two static import lines in the published @storyblok/react@6.1.2 package:

  1. dist/rsc.mjs line 6:

    import { default as b } from "./rsc/live-editing.mjs";

    The /rsc barrel itself imports the 'use client' module at the top.

  2. dist/rsc/live-editing.mjs line 1:

    "use client";
    import { loadStoryblokBridge, registerStoryblokBridge } from "@storyblok/js";

Next's RSC bundler walks any 'use client' module reachable from a server barrel into the client graph, then walks all of its static imports too. So live-editing.mjs drags @storyblok/js (a single ~1.1 MB monolith bundle that inlines TipTap + ProseMirror + storyblok-js-client) into the client graph for every route that imports anything from /rsc.

A secondary leak point exists for richtext consumers via dist/core/richtext-hoc.mjs line 2 (import { ComponentBlok, richTextResolver } from "@storyblok/js"), but the live-editing trapdoor in the barrel alone is sufficient to produce the table above.

Why this can't be worked around on the consumer side

The 'use client' static edge from the /rsc barrel to live-editing.mjs is walked by Next's RSC bundler regardless of whether StoryblokLiveEditing is ever referenced. Tree-shaking does not cross 'use client' boundaries. So no application-level mitigation (route-group splits, dynamic imports, custom shims) can prevent the bridge from being added to the client graph for any route that imports any value from /rsc.

The /type-only route in the harness confirms type-only imports are erased correctly, so import type is the only currently-safe import shape from @storyblok/react/rsc.

Steps to Reproduce

Test harness: https://github.com/kile-lindgren/storyblok-bundle-leak

  1. git clone https://github.com/kile-lindgren/storyblok-bundle-leak
  2. cd storyblok-bundle-leak && pnpm install
  3. cp .env.example .env.local and paste a Storyblok delivery token from any eu space that contains at least one published draft story
  4. pnpm test (runs next build followed by node scripts/measure.mjs)
  5. Open results/run-<timestamp>.md (also printed to stdout) and inspect the Summary table

The harness contains seven routes, each isolating one import pattern from @storyblok/react/rsc, plus an /official route that mirrors the docs end-to-end. The measurement script walks .next/server/app/<route>/page_client-reference-manifest.js for each route, sums the entryJSFiles chunks, and searches them for minification-safe marker strings.

Pre-captured run on main (commit 018ed0f) for inspection without running the harness:
https://github.com/kile-lindgren/storyblok-bundle-leak/blob/018ed0f/results/run-2026-04-22T06-25-12-061Z.md

Simplest example is in the /src/app/editable-only/page.tsx. Which just imports storyblokEditable
import { storyblokEditable } from '@storyblok/react/rsc';. If you look at the resulting client bundle for this route, you will see the bridge and prosemirror client bundles are being included.

Reproduction URL

https://github.com/kile-lindgren/storyblok-bundle-leak

Expected Behavior

A route that imports only storyblokEditable from @storyblok/react/rsc should add zero client bytes beyond the framework baseline. storyblokEditable is a pure data-formatting helper that returns HTML attributes (data-blok-c, data-blok-uid); it has no runtime requirement on the live-editing bridge, on storyblok-js-client, or on TipTap/ProseMirror. Its measurement should match /control and /type-only (0 B added).

More broadly: only routes that explicitly opt into live editing (StoryblokStory / StoryblokLiveEditing) should ship the bridge loader. Routes that renderRichText on the server should NEVER ship the TipTap/ProseMirror bundles (I don't need the Emoji map ever in my public facing code)

Actual Behavior

any component importing anything from @storyblok/react/rsc, will trigger an automatic inclusion of the client bundles, even if the code is pure RSC and doesn't need the bundles.

Environment

System:
    OS: Windows 10 10.0.19045
    CPU: (16) x64 AMD Ryzen 7 PRO 6850U with Radeon Graphics       
    Memory: 4.20 GB / 30.77 GB
  Binaries:
    Node: 22.14.0 - C:\Program Files\nodejs\node.EXE
    npm: 10.9.2 - C:\Program Files\nodejs\npm.CMD
    pnpm: 10.33.0 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Chrome: 147.0.7727.102
    Edge: Chromium (140.0.3485.54)
    Firefox: 149.0.2 - C:\Program Files\Mozilla Firefox\firefox.exe
    Internet Explorer: 11.0.19041.5794
  npmPackages:
    @storyblok/react: 6.1.2 => 6.1.2
    next: 16.0.10 => 16.0.10
    react: 19.2.3 => 19.2.3

Error Logs

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions