Skip to content

Ship the Gutenberg JS bundle once instead of twice (−7.7 MB) - #25965

Merged
jkmassel merged 4 commits into
trunkfrom
jkmassel/dedupe-gutenberg-jsbundle
Sep 3, 2026
Merged

Ship the Gutenberg JS bundle once instead of twice (−7.7 MB)#25965
jkmassel merged 4 commits into
trunkfrom
jkmassel/dedupe-gutenberg-jsbundle

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Draft — opened to produce a CI build for on-device validation. Needs a manual editor smoke-test before it's ready for review (see Test Plan).

Summary

  • Removes a redundant ~7.7 MB copy of the Gutenberg React Native JS bundle from the WordPress and Jetpack apps (Release ships it once; Debug builds get the same trim).

Root Cause

The RN runtime loads the JS bundle via RCTBundleURLProvider / jsBundleURLForBundleRoot: — i.e. main.jsbundle from the app bundle root, placed there by the Copy Gutenberg JS build phase (which copies it from Gutenberg.framework/App.js). Once that root copy exists, the embedded Gutenberg.framework/App.js is never loaded at runtime — the framework binary references it zero times — so it is pure dead weight, embedded into (and code-signed with) the app.

Fix

Strip the redundant App.js from the embedded framework in a build phase that works entirely on the build products, so the on-disk XCFramework (and the repo) is never mutated:

  • Scripts/BuildPhases/StripGutenbergAppJS.sh: delete App.js from the embedded Gutenberg.framework and re-sign it. Gutenberg is embedded with CodeSignOnCopy, so removing a sealed resource invalidates its signature — the re-sign restores it. The phase runs after Embed Frameworks and before the app's final code signing.
  • Safe by construction: it only removes the framework copy once the copy the runtime loads — main.jsbundle at the app root — is present, so it never deletes the last copy. A Debug build served from a Metro dev server that produced no root bundle keeps the framework's App.js.
  • Runs in all configurations (Debug and Release): the duplicate is dropped from local dev installs too, and because nothing on disk changes, local checkouts and other branches are unaffected regardless of configuration.
  • Wired as the final build phase on the WordPress and Jetpack app targets — the two that ship both a root main.jsbundle and the framework copy. Reader ships only the framework copy (no root fallback), so it is intentionally left untouched.

What We Explored

An earlier revision stripped App.js from WordPress/Frameworks/Gutenberg.xcframework during rake dependencies. Because that directory is gitignored, the strip persisted across checkouts and broke every build on branches whose Copy Gutenberg JS phase still copies App.js from the framework (trunk, release/*) with cp: …/App.js: No such file or directory until rake dependencies was re-run there. The build-phase strip avoids mutating shared on-disk state entirely.

Test Plan

  • On-device: open the classic (React Native) Gutenberg editor, load an existing post, insert/edit a block — confirm it loads and functions. This is the one that matters: App.js is gone from the framework, so a working editor proves the root main.jsbundle is what the runtime loads.
  • The Release .ipa no longer contains Gutenberg.framework/App.js and installs cleanly — confirm on the CI prototype build.
  • Verified in real Xcode Debug and Release Jetpack builds: Gutenberg.framework/App.js absent, root main.jsbundle present (7.7 MB), the whole .app passes codesign --verify --deep --strict, and on-disk WordPress/Frameworks is unchanged.
  • Strip + re-sign verified against the real device-slice Gutenberg.framework: −7.7 MB, passes codesign --verify --deep --strict.
  • Script bash -n; gating unit-tested (strips in Debug and Release, preserves App.js when no root bundle, no-op when already stripped).

Notes

The RN editor is being replaced by GutenbergKit; this doesn't touch that migration, it just stops shipping the RN bundle twice in the meantime.

The React Native runtime loads main.jsbundle from the app bundle root, but a
byte-identical copy (~7.7MB) also ships inside Gutenberg.framework/App.js where
it is never loaded — the framework resolves the bundle via RCTBundleURLProvider
from the main bundle, not from itself (App.js is referenced zero times in the
framework binary).

Strip App.js from the framework at dependency-download time so it isn't embedded
and code-signed into the app, and source the app-root main.jsbundle from the
standalone react-native-bundle-source-map/main.jsbundle that already ships
alongside the XCFramework. Removes ~7.7MB from the app bundle, no runtime change.
@jkmassel jkmassel added Tooling Build, Release, and Validation Tools [Type] Tech Debt labels Sep 1, 2026
@jkmassel jkmassel self-assigned this Sep 1, 2026
@jkmassel jkmassel added this to the 27.3 milestone Sep 1, 2026
@wpmobilebot

wpmobilebot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number34356
VersionPR #25965
Bundle IDorg.wordpress.alpha
Commitbcfdcd2
Installation URL6ink88ejqa440
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number34356
VersionPR #25965
Bundle IDcom.jetpack.alpha
Commitbcfdcd2
Installation URL2dooeo1d02dk8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@jkmassel
jkmassel requested a review from dcalhoun September 1, 2026 22:52

@dcalhoun dcalhoun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I smoke tested the prototype build on the PR, quickly testing text changes and image uploads/selection. Things appear stable.

I did not test local Xcode builds.

I left a few inline comments with findings raised by Claude that appear legitimate.

Comment on lines +58 to +61
# The React Native runtime loads the JS bundle from main.jsbundle at the app
# bundle root (copied in by the CopyGutenbergJS build phase). The identical
# copy inside Gutenberg.framework/App.js is never loaded at runtime and only
# adds ~7.7MB of dead weight to the embedded framework, so drop it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding from Claude:

Since WordPress/Frameworks is gitignored, this strip persists across branch switches. Trunk's CopyGutenbergJS.sh gates on the framework directory existing and then unconditionally cps App.js, so after running rake dependencies here, switching back to trunk (or a release/* branch) fails every build with cp: .../Gutenberg.framework/App.js: No such file or directory until rake dependencies is rerun there. Worth a line in the PR notes alongside the existing "re-run rake dependencies" one, since the reverse direction isn't obvious.

# bundle root (copied in by the CopyGutenbergJS build phase). The identical
# copy inside Gutenberg.framework/App.js is never loaded at runtime and only
# adds ~7.7MB of dead weight to the embedded framework, so drop it.
find "${FRAMEWORKS_DIR}/Gutenberg.xcframework" -path '*/Gutenberg.framework/App.js' -delete

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding from Claude:

Previously "framework present" implied "bundle present". Now the source is a sibling folder that can be absent independently (e.g. a hand-copied *.xcframework from a local build.sh, which writes that folder beside Frameworks/, not inside it). In Debug the copy phase only warns, so the app ships with no main.jsbundle and the editor fails at runtime. Failing fast here keeps the invariant:

Suggested change
find "${FRAMEWORKS_DIR}/Gutenberg.xcframework" -path '*/Gutenberg.framework/App.js' -delete
[[ -f "${FRAMEWORKS_DIR}/react-native-bundle-source-map/main.jsbundle" ]] || { echo "error: expected react-native-bundle-source-map/main.jsbundle alongside Gutenberg.xcframework" >&2; exit 1; }
find "${FRAMEWORKS_DIR}/Gutenberg.xcframework" -path '*/Gutenberg.framework/App.js' -delete

Comment thread Scripts/BuildPhases/CopyGutenbergJS.sh Outdated

if [[ -d $XCFRAMEWORK_BUNDLE_ROOT ]]; then
cp "$XCFRAMEWORK_BUNDLE_ROOT/App.js" "$BUNDLE_FILE"
if [[ -f $DOWNLOADED_JS_BUNDLE ]]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding from Claude:

The warning:/error: messages in the else branch below (lines 28 and 30) still say the bundle wasn't found "in the XCFramework". The script no longer reads from it, and App.js is now absent from the framework by design, so the message sends someone to the wrong place. Naming the actual path helps:

echo "error: Could not find Gutenberg bundle at $DOWNLOADED_JS_BUNDLE."

@@ -1 +1 @@
$SRCROOT/Frameworks/Gutenberg
$SRCROOT/Frameworks/react-native-bundle-source-map/main.jsbundle

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding from Claude:

Pre-existing, but this change makes it load-bearing: the WordPress target's Copy Gutenberg JS phase (E1C5456F in project.pbxproj) lists this file under inputPaths rather than inputFileListPaths (Jetpack's FABB264C is correct), so Xcode tracks the xcfilelist file itself, not main.jsbundle. Latent today only because the declared assets/ output is never produced, which forces a rerun every build. Moving it to inputFileListPaths and dropping the empty outputPaths = ("") would be a cheap fix to fold in here.

Re-does the de-dup so it only affects Release builds and never mutates the
gitignored on-disk XCFramework.

Stripping App.js from WordPress/Frameworks during `rake dependencies` persisted
across checkouts and broke every build on branches whose CopyGutenbergJS.sh
still copies App.js from the framework (trunk, release/*) until dependencies
were re-fetched. Instead:

- Revert the three dependency/copy-phase scripts to trunk.
- Add Scripts/BuildPhases/StripGutenbergAppJS.sh, which on *Release* only
  deletes the redundant App.js from the embedded Gutenberg.framework and
  re-signs it (CodeSignOnCopy seals the resource, so removal requires a
  re-sign). Runs as the final phase on the WordPress and Jetpack targets.

Debug and simulator builds are untouched. Verified against the real framework:
-7.7 MB, still passes codesign --verify --deep --strict.
The strip runs on build products and never touches the on-disk XCFramework, so
the original Release-only gate (there to avoid changing dev builds) isn't needed
— dropping the duplicate in Debug too gives smaller local installs and one less
Debug/Release divergence.

Replace the `*Release*` check with a safety check on the copy the runtime loads:
only delete the framework's App.js once main.jsbundle exists at the app root, so
we never remove the last copy (a Metro/dev-server Debug build that produced no
root bundle keeps App.js).

Verified in real Debug and Release Jetpack builds: App.js absent, main.jsbundle
present, the app passes codesign --verify --deep --strict, on-disk Frameworks
untouched.
@jkmassel

jkmassel commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@dcalhoun – these are some good findings, and I didn't come across them because I pretty much exclusively use worktrees. I reworked the PR so that nothing really changes during development, and instead we just strip out the duplicate JS at the last moment while building.

@jkmassel
jkmassel marked this pull request as ready for review September 3, 2026 21:16
@jkmassel
jkmassel added this pull request to the merge queue Sep 3, 2026
Merged via the queue into trunk with commit 58fb5b5 Sep 3, 2026
28 checks passed
@jkmassel
jkmassel deleted the jkmassel/dedupe-gutenberg-jsbundle branch September 3, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Tooling Build, Release, and Validation Tools [Type] Tech Debt

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants