Skip to content

fix(site): pre-bundle react-dom so dev islands hydrate#1711

Draft
decepulis wants to merge 2 commits into
mainfrom
claude/ecstatic-hopper-hsn9pa
Draft

fix(site): pre-bundle react-dom so dev islands hydrate#1711
decepulis wants to merge 2 commits into
mainfrom
claude/ecstatic-hopper-hsn9pa

Conversation

@decepulis

@decepulis decepulis commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Symptom

Running the site locally (pnpm dev) renders but never hydrates. The console fills with:

SyntaxError: Importing binding name 'createRoot' is not found.
[astro-island] Error hydrating /src/components/...

Every React island fails to hydrate. Dev only — production builds are unaffected.

Root cause

@astrojs/react's client renderer does import { createRoot, hydrateRoot } from "react-dom/client". react-dom/client is a CommonJS module with no native ESM createRoot export, so in dev it relies on Vite's optimizer pre-bundling it into an ESM-interop wrapper. Here it was being served raw instead:

// served client.js (broken) — raw CJS, no named binding
import { createRoot, hydrateRoot } from "/@fs/.../react-dom/client.js?v=...";

Why react-dom specifically wasn't pre-bundled:

  • The @astrojs/react integration adds react-dom / react-dom/client to optimizeDeps.include, but as of Astro 6 / Vite 7 it injects them at the client-environment layer via Vite's Environment API (configEnvironment).
  • The site's astro.config.mjs sets vite.optimizeDeps (with only exclude, for @videojs/* and @resvg/resvg-js) at the root/shared config layer.
  • Under the Environment API, the site's root-level optimizeDeps shadows the integration's per-environment include, so react-dom silently dropped out of the optimized set. react still got pre-bundled (discovered via normal module scanning); react-dom/client is only imported by the renderer that loads dynamically at hydration, so the scanner never saw it and — with the include hint gone — it was served as raw CJS.

That's why it was all islands and only in dev: the React DOM client renderer is the common dependency of every client:* island, and dependency pre-bundling is a dev-server concern the production bundler doesn't rely on.

When / why it broke

  • It regressed in chore(site): upgrade to Astro 6 #946 "chore(site): upgrade to Astro 6" (2026-04-06), which bumped astro ^5.14.4 → ^6.1.3 (later ^6.3.1 in chore(site): upgrade astro to 6.3.1 #1499) and pulled in Vite 7 + the Environment API.
  • The upgrade applied the documented Astro 6 breaking changes correctly (experimental.fonts → stable fonts, Netlify environmentVariables). The optimizeDeps shadowing is an undocumented Vite-layer interaction — it only bites projects that both set a custom vite.optimizeDeps and use a renderer integration that relies on per-environment includes, and only in pnpm dev. CI has no browser-hydration check against the dev server, so it went unnoticed for ~2 months.

Fix

Re-declare the React DOM entries in the site's own optimizeDeps.include so they survive the merge regardless of which config layer the integration uses. After the fix the renderer links against the optimized dep with proper CJS interop:

import __vite__cjsImport1_reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v=...";
const createRoot = __vite__cjsImport1_reactDom_client["createRoot"];

optimizeDeps only affects the dev optimizer, so the production Rollup build is untouched.

Verification (local, real browser)

Ran the actual dev server and loaded localhost:4321 in headless WebKit (matching the Safari error phrasing) before/after, against the real repo state:

hydration errors islands hydrated
before many 0 / 7
after 0 7 / 7

Also confirmed at the network level that the served renderer switched from importing raw /@fs/.../react-dom/client.js to the optimized /node_modules/.vite/deps/react-dom_client.js.

Also in this PR

site/CLAUDE.md documented Astro 5.14.4 (plus stale Shiki/Vitest versions). Updated the tech-stack versions and added a "Dependency Optimization (Vite) — gotcha" note so the next person who edits vite.optimizeDeps or adds a renderer integration knows to include its client deps.

Not in this PR (follow-up?)

The root package.json carries unused react@^18, react-dom@^18, and react-compiler-runtime@^1 devDependencies. Nothing at the root imports React, and they install a duplicate React 18 in a React 19 monorepo (which also makes the site resolve react-compiler-runtime to the React-18-bound copy). Removing them did not fix this bug, so I kept them out to stay focused — but they're stray and worth removing separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_0189rofb2dpGEtCWDxZAmWxi

The Vite dev optimizer was serving react-dom/client as a raw CommonJS
module, so the @astrojs/react client renderer's
`import { createRoot, hydrateRoot } from 'react-dom/client'` failed to
link with `SyntaxError: Importing binding name 'createRoot' is not found`,
leaving every React island unhydrated in dev.

The React integration adds react-dom/react-dom/client to optimizeDeps
.include, but the site's own vite.optimizeDeps (which set only `exclude`
for @videojs/* and @resvg) shadowed that per-environment include, so
react-dom was never pre-bundled. Re-declaring the include here restores
the CJS→ESM interop wrapper. Dev only; the production Rollup build was
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189rofb2dpGEtCWDxZAmWxi
@netlify

netlify Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit db5e435
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a34b2317cd39400086f334b
😎 Deploy Preview https://deploy-preview-1711--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Jun 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 19, 2026 3:06am

Request Review

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 44.32 kB
/video (default + hls) 183.79 kB
/video (minimal) 43.98 kB
/video (minimal + hls) 183.63 kB
/audio (default) 37.83 kB
/audio (minimal) 36.61 kB
/background 4.20 kB
Media (10)
Entry Size
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 242.78 kB
/media/hls-video 141.27 kB
/media/mux-audio 163.91 kB
/media/mux-video 163.68 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 16.99 kB
/media/simple-hls-video 18.83 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Size
/video/player 8.07 kB
/audio/player 5.38 kB
/background/player 3.92 kB
/live-video/player 7.64 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Size
/video/minimal-skin.css css 5.45 kB
/video/skin.css css 5.43 kB
/video/minimal-skin js 44.01 kB
/video/minimal-skin.tailwind js 44.58 kB
/video/skin js 44.28 kB
/video/skin.tailwind js 45.00 kB
/audio/minimal-skin.css css 3.60 kB
/audio/skin.css css 3.53 kB
/audio/minimal-skin js 36.62 kB
/audio/minimal-skin.tailwind js 37.04 kB
/audio/skin js 37.80 kB
/audio/skin.tailwind js 38.17 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.45 kB
/live-video/skin.css css 5.43 kB
/live-video/minimal-skin js 43.12 kB
/live-video/minimal-skin.tailwind js 43.58 kB
/live-video/skin js 43.08 kB
/live-video/skin.tailwind js 43.62 kB
/live-audio/minimal-skin.css css 3.60 kB
/live-audio/skin.css css 3.53 kB
/live-audio/minimal-skin js 29.73 kB
/live-audio/minimal-skin.tailwind js 29.20 kB
/live-audio/skin js 31.04 kB
/live-audio/skin.tailwind js 30.66 kB
/global.css css 176 B
/shared.css css 88 B
/tailwind.css css 228 B
/skin-element js 1.44 kB
UI Components (38)
Entry Size
/ui/airplay-button 3.59 kB
/ui/alert-dialog 1.23 kB
/ui/alert-dialog-close 483 B
/ui/alert-dialog-description 423 B
/ui/alert-dialog-title 348 B
/ui/buffering-indicator 2.93 kB
/ui/captions-button 3.68 kB
/ui/captions-radio-group 3.34 kB
/ui/cast-button 3.64 kB
/ui/compounds 8.69 kB
/ui/controls 2.40 kB
/ui/error-dialog 3.34 kB
/ui/fullscreen-button 3.60 kB
/ui/hotkey 2.21 kB
/ui/menu 5.60 kB
/ui/mute-button 3.67 kB
/ui/pip-button 3.61 kB
/ui/play-button 3.61 kB
/ui/playback-rate-button 3.66 kB
/ui/playback-rate-radio-group 3.03 kB
/ui/popover 1.98 kB
/ui/poster 2.28 kB
/ui/quality-radio-group 2.41 kB
/ui/seek-button 3.61 kB
/ui/seek-indicator 3.81 kB
/ui/seek-indicator-value 180 B
/ui/slider 1.52 kB
/ui/status-announcer 3.56 kB
/ui/status-indicator 3.69 kB
/ui/status-indicator-value 211 B
/ui/thumbnail 3.28 kB
/ui/time 2.96 kB
/ui/time-slider 4.01 kB
/ui/tooltip 2.24 kB
/ui/volume-indicator 3.88 kB
/ui/volume-indicator-fill 178 B
/ui/volume-indicator-value 201 B
/ui/volume-slider 2.88 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 36.83 kB
/video (default + hls) 175.18 kB
/video (minimal) 36.91 kB
/video (minimal + hls) 175.13 kB
/audio (default) 29.97 kB
/audio (minimal) 30.04 kB
/background 754 B
Media (9)
Entry Size
/media/background-video 575 B
/media/dash-video 241.23 kB
/media/hls-video 139.80 kB
/media/mux-audio 162.26 kB
/media/mux-video 162.26 kB
/media/native-hls-video 7.39 kB
/media/simple-hls-audio-only 15.44 kB
/media/simple-hls-video 17.18 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Size
/tailwind.css css 228 B
/video/minimal-skin.css css 5.37 kB
/video/skin.css css 5.34 kB
/video/minimal-skin js 36.81 kB
/video/minimal-skin.tailwind js 42.58 kB
/video/skin js 36.73 kB
/video/skin.tailwind js 42.46 kB
/audio/minimal-skin.css css 3.47 kB
/audio/skin.css css 3.39 kB
/audio/minimal-skin js 29.98 kB
/audio/minimal-skin.tailwind js 31.76 kB
/audio/skin js 29.91 kB
/audio/skin.tailwind js 33.75 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.37 kB
/live-video/skin.css css 5.34 kB
/live-video/minimal-skin js 32.56 kB
/live-video/minimal-skin.tailwind js 38.22 kB
/live-video/skin js 32.58 kB
/live-video/skin.tailwind js 38.27 kB
/live-audio/minimal-skin.css css 3.47 kB
/live-audio/skin.css css 3.39 kB
/live-audio/minimal-skin js 21.74 kB
/live-audio/minimal-skin.tailwind js 24.64 kB
/live-audio/skin js 21.78 kB
/live-audio/skin.tailwind js 24.78 kB
UI Components (32)
Entry Size
/ui/airplay-button 2.74 kB
/ui/alert-dialog 1.27 kB
/ui/buffering-indicator 2.44 kB
/ui/captions-button 2.77 kB
/ui/captions-radio-group 2.87 kB
/ui/cast-button 2.71 kB
/ui/controls 2.47 kB
/ui/error-dialog 2.68 kB
/ui/fullscreen-button 2.73 kB
/ui/gesture 2.21 kB
/ui/hotkey 2.11 kB
/ui/live-button 3.33 kB
/ui/menu 7.28 kB
/ui/mute-button 2.71 kB
/ui/pip-button 3.06 kB
/ui/play-button 2.77 kB
/ui/playback-rate 2.85 kB
/ui/playback-rate-button 2.73 kB
/ui/popover 2.37 kB
/ui/poster 2.40 kB
/ui/quality 2.99 kB
/ui/seek-button 2.74 kB
/ui/seek-indicator 2.17 kB
/ui/slider 3.70 kB
/ui/status-announcer 1.93 kB
/ui/status-indicator 2.17 kB
/ui/thumbnail 2.68 kB
/ui/time 3.30 kB
/ui/time-slider 4.23 kB
/ui/tooltip 2.68 kB
/ui/volume-indicator 2.13 kB
/ui/volume-slider 4.13 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (14)
Entry Size
. 9.08 kB
/dom 17.02 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 236.88 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls 135.63 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.05 kB
/dom/media/simple-hls 16.56 kB
/dom/media/simple-hls-audio-only 14.75 kB
/dom/media/vimeo 9.86 kB
/media/predicate 573 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 2.26 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Size
. 4.45 kB
/dom 6.33 kB
/hls 15.44 kB
/background-looping-video 12.95 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

The tech-stack list still said Astro 5.14.4 (and stale Shiki/Vitest
versions); the site moved to Astro 6.3.1 + Vite 7 in #946. Update the
versions and add a "Dependency Optimization" note explaining why a
root-level vite.optimizeDeps shadows a renderer integration's
per-environment include under Vite's Environment API — the cause of the
dev-only createRoot hydration failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189rofb2dpGEtCWDxZAmWxi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants