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
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/[email protected] package:
-
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.
-
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
git clone https://github.com/kile-lindgren/storyblok-bundle-leak
cd storyblok-bundle-leak && pnpm install
cp .env.example .env.local and paste a Storyblok delivery token from any eu space that contains at least one published draft story
pnpm test (runs next build followed by node scripts/measure.mjs)
- 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
Package
@storyblok/react
Bug Description
Summary
Any value import from
@storyblok/react/rsc— including helpers that have no runtime client requirement, such asstoryblokEditable— 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-latestURL constant,storyblok-javascript-bridgescript element id) and the entire ProseMirror runtime that backs TipTap.The leak persists when
@storyblok/react/rscis used exactly per the official Next.js docs (storyblokInit+apiPlugin+<StoryblokProvider>+<StoryblokStory>+ realcdn/storiesfetch). It is intrinsic to the package's barrel structure, not to user setup.Environment
@storyblok/[email protected]@storyblok/[email protected]@storyblok/[email protected][email protected][email protected](App Router, Turbopack — the Next 16 default)[email protected],[email protected]Reproduction
Test harness: https://github.com/kile-lindgren/storyblok-bundle-leak
The harness contains seven routes, each isolating one import pattern from
@storyblok/react/rsc, plus an/officialroute that mirrors the docs end-to-end. The measurement script walks.next/server/app/<route>/page_client-reference-manifest.jsfor each route, sums theentryJSFileschunks, and searches them for minification-safe marker strings. Results are written toresults/run-<timestamp>.md.Latest measurement run on
main(commit018ed0f):https://github.com/kile-lindgren/storyblok-bundle-leak/blob/018ed0f/results/run-2026-04-22T06-25-12-061Z.md
Measured impact
/rsc(framework baseline)/control/type-onlyimport type { SbBlokData }/editable-onlystoryblokEditable(value)/server-component-onlysetComponents,StoryblokServerComponent/richtextStoryblokServerRichText+ 1 TipTap extension/previewStoryblokStory/officialstoryblokInit+apiPlugin+<StoryblokProvider>+cdn/storiesfetch +<StoryblokStory>)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/rscimport surface into one shared chunk and ships it to every route that touches it.Markers:
Bridge URL= literalstoryblok-v2-latestBridge script-id= literalstoryblok-javascript-bridge(the<script id="...">the bridge loader injects)ProseMirror= literalProseMirror(CSS class names likeProseMirror-hideselection)Schema+NodeType= bothSchemaandNodeTypesubstrings present in the same chunkRoot cause
Two static
importlines in the published@storyblok/[email protected]package:dist/rsc.mjsline 6:The
/rscbarrel itself imports the'use client'module at the top.dist/rsc/live-editing.mjsline 1: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. Solive-editing.mjsdrags@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.mjsline 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/rscbarrel tolive-editing.mjsis walked by Next's RSC bundler regardless of whetherStoryblokLiveEditingis 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-onlyroute in the harness confirms type-only imports are erased correctly, soimport typeis the only currently-safe import shape from@storyblok/react/rsc.Steps to Reproduce
Test harness: https://github.com/kile-lindgren/storyblok-bundle-leak
git clone https://github.com/kile-lindgren/storyblok-bundle-leakcd storyblok-bundle-leak && pnpm installcp .env.example .env.localand paste a Storyblok delivery token from anyeuspace that contains at least one published draft storypnpm test(runsnext buildfollowed bynode scripts/measure.mjs)results/run-<timestamp>.md(also printed to stdout) and inspect theSummarytableThe harness contains seven routes, each isolating one import pattern from
@storyblok/react/rsc, plus an/officialroute that mirrors the docs end-to-end. The measurement script walks.next/server/app/<route>/page_client-reference-manifest.jsfor each route, sums theentryJSFileschunks, and searches them for minification-safe marker strings.Pre-captured run on
main(commit018ed0f) 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
storyblokEditablefrom@storyblok/react/rscshould add zero client bytes beyond the framework baseline.storyblokEditableis 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, onstoryblok-js-client, or on TipTap/ProseMirror. Its measurement should match/controland/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.3Error Logs
Additional Context
No response