Skip to content

Trim the packaged app by about 350 MB: unused PHP builds, renderer libraries and Chromium locales - #707

Merged
zaerl merged 5 commits into
trunkfrom
update/trim-packaged-app-size
Oct 23, 2026
Merged

zaerl merged 5 commits into
trunkfrom
update/trim-packaged-app-size

Conversation

@zaerl

@zaerl zaerl commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Why

The packaged app is about 920 MB on disk on macOS (320 MB zipped), and about a third of that is code it never loads. That is a long download on a Contributor Day network, where everyone fetches the app at the same time. It is the same problem #23 reported. That PR has since gone stale against trunk, and its claim that @xterm/xterm is unused no longer holds, so this branch starts again from trunk.

What changes

Three things the app ships and never loads are left out:

  • The asyncify PHP builds, about 220 MB. Each @php-wasm/node-* package carries every PHP version twice, as jspi/ and as asyncify/, and @php-wasm/node reads asyncify/ only when "Suspending" in WebAssembly is false. Electron 43.6.0 has JSPI, so those folders never load. @php-wasm/node-5-2 goes too, because the app offers only SupportedPHPVersions (7.4–8.5). Both are build.files exclusions; node_modules is untouched.
  • The renderer's libraries, about 95 MB in app.asar. esbuild already bundles React, Emotion, xterm and the @wordpress UI packages into src/renderer/index.js, but they were listed in dependencies, so electron-builder copied their trees in as well. They move to devDependencies. @wordpress/i18n and @wordpress/hooks stay, because the main process loads them. The 22 hand-kept build.files exclusions that trimmed part of that tree since Update @wordpress/components from 28 to 41 as the base for the design system redesign #563 are now unnecessary and are removed.
  • Chromium's locales for languages the app has no catalog for, about 43 MB. The app shows English for those anyway, so build.electronLanguages keeps English and the Chromium locale of each catalog in src/languages. npm run i18n:download now writes that list whenever it adds or removes a catalog, so the version-bump PR carries the change with no manual step. scripts/chromium-locales.cjs maps a catalog to the locale Chromium itself would load, including Chromium's regional folding (es-mx → es-419, pt → pt-PT, zh-hk → zh-TW, en-ca → en-GB). Every regional locale is written in both spellings, pt-BR for Windows and Linux and pt_BR for macOS, because electron-builder matches file names.

Unit tests guard the cuts:

  • electron-wasm-jspi asks the bundled Electron for WebAssembly.Suspending, so an Electron bump that loses JSPI fails CI instead of the first dev server start.
  • electron-languages asserts that the list equals the one the catalogs call for, which catches both a missing entry and a stale one. It also checks that electron-builder keeps every locale file the list names.
  • chromium-locales covers the mapping with both platforms' locale names.
  • php-builds fails when an installed @php-wasm/node-X-Y whose version SupportedPHPVersions does not offer would ship, and when a version the settings offer is excluded.

Run against translate.wordpress.org today, the script adds sr and tr for two catalogs that are above the cut-off but not yet in the repository, and leaves out lo, which Chromium has no locale for.

Not in this PR: dropping PHP versions the settings offer (a product decision since #559), and the intl extension and ICU data, which core's tests use.

macOS arm64, unsigned, same node_modules trunk this branch
.app on disk 920 MB 571 MB
zipped 320 MB 218 MB
app.asar 205 MB 108 MB
app.asar.unpacked 436 MB 228 MB
Electron Framework 277 MB 234 MB

How to test this

Platforms: macOS and Windows, both needed, because the PHP runtime is chosen when the server starts and no CI job starts the real Playground server (see the Electron pin note in AGENTS.md). Use the Buildkite artifact for the current head commit.

Starting state: the app from this branch's Buildkite build, with a site that has finished setup and whose server is stopped.

  1. Open the site and click Start development server. The server comes up, and the site's front page and /wp-admin/ both load in the browser.
  2. Open the app's log for that site. It has no Cannot find module error, and no path containing asyncify.
  3. In the settings, change the PHP version to 7.4, then start the server again. The site loads, and the log names PHP 7.4.
  4. Use the app as usual: the site list, the terminal (xterm), the changed files, a dialog. Everything draws as before. The renderer no longer reaches node_modules for any of it.
  5. On macOS, compare the size of the app with a trunk build in Finder (Get Info): about 570 MB against about 920 MB.

What must not have happened:

  • The dev server failing to start, or starting on a PHP version other than the one chosen.
  • A blank or unstyled window, which would mean the renderer still needed a package that is no longer shipped.
  • Native form strings, such as a validation bubble, in a different language from before for cs, hu, nl, pl or English.

Risks and limitations

  • The asyncify cut depends on Electron keeping JSPI. That is what electron-wasm-jspi guards.
  • On macOS, electron-builder also removes the grammatical-gender overlays (cs_FEMININE.lproj and similar) for the kept languages; each holds an 18-byte locale.pak. Chromium should fall back to the base locale, but I have not checked a macOS user with a grammatical gender set.
  • electron-languages copies electron-builder's matching rule, because it is not exported. An electron-builder release that changes that rule would not be caught.
  • Chromium's regional folding rules in scripts/chromium-locales.cjs follow CheckAndResolveLocale in Chromium's l10n_util.cc, but no Chromium source is vendored here to check them against. Only en-ca → en-GB affects today's list, and en-GB is kept for en-gb anyway.
  • npm run i18n:download now needs an installed Electron, which it downloads on first use. It reads Electron's locales before changing any catalog, so a failed download leaves everything as it was.
  • The packaged smoke suite passed locally only with --user-data-dir added to its launch, because a dev copy of the app held the single-instance lock. Trunk's build failed the same way without it. CI's run on this PR is the unmodified one.
  • Review: one completed fresh-context review, 0 findings; details below.

Related


Design decisions and alternatives considered
  • Exclusions in build.files, not a postinstall prune. node_modules stays complete for development, the tests and the journeys. Only what electron-builder copies changes.
  • devDependencies rather than more !node_modules/... lines. The exclusion list from Update @wordpress/components from 28 to 41 as the base for the design system redesign #563 had to grow every time a @wordpress package gained a dependency. Moving the cause leaves nothing to maintain, and lets 22 lines go.
  • A hand-written electronLanguages list with a test, rather than one generated at build time. A generated list would need a JS build config or a hook; a static list plus a test that names the missing entry is simpler, and changes only when a catalog is added.
  • Chromium locales for the app's catalogs only. Keeping a broader set would leave Chromium's strings in, say, Italian around an English app, which is no better than English.
Review outcome (required — see AGENTS.md)

0 [fix here] · 0 [follow-up]. One style note was applied: the electron-languages header claimed Chromium's strings follow the app's language, but they follow the operating system's.

  • Review: completed. Reviewer: a separate Claude agent context, given only the diff and .github/instructions/code-review.instructions.md. Reviewed head a7be033 against base 2dda92c (origin/trunk). Evidence: npm run lint passed; npm test passed with 2202 tests, 2200 pass, 0 fail, 2 skipped; npm run test:electron passed with 2202 tests, 2201 pass, 0 fail, 1 skipped. The five dimensions found nothing. Specifically checked: no main-process, preload or runner import of a moved package; no production dependency still reaching a formerly excluded package; asyncify reached only when JSPI is absent, including for intl and xdebug; PHP 5.2 unreachable from the settings; electronLanguages against electron-builder's matching rule and the app's locale resolution.
  • Since review: a7be033 → 7ac9e25 checked. The only change is the comment wording in tests/unit/electron-languages.test.cjs, and that test still passes.
  • Since review: 7ac9e25 → 8e1d005, a CI fix, re-checked by the author against the same standard: 0 findings. CI's unit job failed because Electron 43's npm package downloads its binary on the first require('electron'), and three test files now asked for it at the same moment. I reproduced that locally by removing node_modules/electron/dist: two downloads ran and two tests failed. A pretest script now asks once before node --test starts, and the workflow comment that said npm ci fetches Electron is corrected. Run from a missing binary, npm test downloads once and passes 2200 tests with 0 failures, and npm run test:electron and npm run lint pass too.
  • Review of 8e1d005 → dde0f94 (the i18n:download change): completed by a separate Claude agent context against the same standard, in the context of origin/trunk...dde0f94. Result: 1 [fix here] · 0 [follow-up], fixed in 26e9250. The finding: Electron's locale names were read after the catalogs had been replaced, so a failed Electron download would leave the run half applied. They are now read before anything is written. One style note was applied as well, a test title that claimed more than it asserted. The reviewer also ran about 130 slugs through the mapping without a crash, and checked that rewriting package.json reproduces the file byte for byte. Lint passes, and npm test and npm run test:electron each pass 2212 tests with 0 failures.
  • Since that review: dde0f94 → 26e9250. Only that fix and the renamed test changed; the suites above ran on 26e9250.
  • Since that review: 26e9250 → 55ab855 adds only tests/unit/php-builds.test.cjs, for Remove unused PHP versions #704. I checked it against the standard myself: 0 findings. Broken both ways in turn (5.2 kept, 8.3 excluded), it fails each time, and it passes as committed. npm test and npm run test:electron each pass 2214 tests with 0 failures.
  • CodeRabbit: not requested yet.
Implementation notes
  • Sizes come from electron-builder --dir with -c.mac.identity=null -c.mac.notarize=false, run twice from the same node_modules with trunk's and this branch's package.json and lockfile, then zipped with ditto -c -k. Signed artifacts will differ slightly.
  • Locally, from the trimmed package, @php-wasm/node booted PHP 7.4.33, 8.3.33 and 8.5.10 with intl loaded, run on the packaged binary with ELECTRON_RUN_AS_NODE=1.
  • npm run test:e2e:packaged passed all 15 tests on the trimmed build, including the first window painting, the preload bridge, and the build.files allow-list checks.
  • In the lockfile, the moved packages and their trees change from devOptional to dev. No versions change.
  • pretest runs node -e "require('electron')". The electron package downloads its binary on the first require, so this fetches it once before node --test runs its files in parallel, where concurrent downloads raced. npm run test:electron already did this in its runner, and a single file run with node --test <file> has nothing to race with.
  • The asyncify choice is made in @php-wasm/node/index.cjs (loadNodeRuntime) and in each @php-wasm/node-X-Y/index.cjs (getPHPLoaderModule and the extension paths).
Screenshots or recording

Nothing on screen changes.

🤖 Generated with Claude Code

…mium locales

Three things the app shipped and never loads:

- The asyncify PHP builds. Every @php-wasm/node-* package carries each
  PHP version as jspi/ and asyncify/, and reads asyncify/ only when the
  runtime lacks WebAssembly.Suspending. Electron 43.6.0 has it. PHP 5.2,
  which the app does not offer, goes with them.
- The renderer's libraries. React, Emotion, xterm and the @WordPress UI
  packages are bundled into src/renderer/index.js by esbuild, but sat in
  dependencies, so electron-builder copied their trees into app.asar too.
  They move to devDependencies; @wordpress/i18n and @wordpress/hooks stay,
  because the main process loads them. The 22 build.files exclusions that
  kept part of that tree out are no longer needed.
- Chromium's locales for languages the app has no catalog for. The app
  shows English for those anyway, so build.electronLanguages keeps English
  and the languages in src/languages.

The unsigned macOS arm64 app goes from 920 MB to 571 MB, 320 MB to 218 MB
zipped. Two unit tests guard the cuts: one asks the bundled Electron for
JSPI, the other fails when a catalog's Chromium locale is not kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e561dff5-d2cb-4294-972d-b8d85f1de17a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

zaerl and others added 4 commits October 21, 2026 14:31
Electron 43's npm package has no install script: the first
`require('electron')` downloads and unpacks the binary. CI's unit job
reaches that during `npm test`, and with three test files now asking at
once, each started its own download and read a half-unpacked
Electron.app (ENOENT on the framework's Resources, dyld unable to load
the framework). A `pretest` script asks once, before `node --test` runs
the files in parallel. The languages test also asks before reading
dist/, so it holds when run on its own from a fresh clone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build.electronLanguages was a hand-written list, and a test only told
whoever ran `npm run i18n:download` which entries to add. That test
also asked for the wrong locale for a regional Spanish catalog: es where
Chromium loads es-419.

scripts/chromium-locales.cjs now maps a catalog to the Chromium locale
Chromium itself would load, with its regional folding (es-419, pt-PT,
zh-TW, en-GB) on top of slugTag's aliases, and builds the list: English,
then each locale in both spellings, `pt-BR` for Windows and Linux and
`pt_BR` for macOS. The download writes it into package.json from every
catalog it leaves, only when it changes; a JSON round trip gives the
file back byte for byte, so the list is the whole diff. The test now
asserts the list equals the computed one, which also catches a stale
entry, and that electron-builder keeps every file the list names.

Run against translate.wordpress.org today, it adds sr and tr for the
two catalogs above the cut-off that the repository does not have yet,
and leaves out lo, which Chromium has no locale for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A missing Electron binary is downloaded on the first require; failing
there after the catalogs were replaced left the run half applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#704 asks that every @php-wasm/node-X-Y package whose version
SupportedPHPVersions does not report stay out of the app. Today that is
node-5-2 alone, excluded by name. A Playground update that drops a
version from the list or ships another legacy one would change which
names those are; this test then names the build.files exclusion to add
or to remove.

Fixes #704.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zaerl
zaerl requested a review from ryanwelcher October 21, 2026 13:45
@zaerl zaerl self-assigned this Oct 21, 2026

@ryanwelcher ryanwelcher left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work! I've compared builds in buildkite and the artifacts are much smaller

@zaerl
zaerl merged commit 630253f into trunk Oct 23, 2026
13 checks passed
@zaerl
zaerl deleted the update/trim-packaged-app-size branch October 23, 2026 13:36
ryanwelcher added a commit that referenced this pull request Oct 23, 2026
## Why

`v2.0.0-beta.1` shipped on 13 October. Ten commits have reached trunk since then (#667 to #707), and milestone `v2.0.0` has 8 closed issues and none open. This is a stable release: it takes `releases/latest` from `v1.2.1`.

`electron-builder` names the artifacts after `package.json`, so the version has to move before the tag and artifacts exist.

## What changes

- **The version.** The three root version fields in `package.json` and `package-lock.json` move from `2.0.0-beta.1` to `2.0.0`. No dependency versions change.
- **Translations.** `npm run i18n:download` refreshed the seven existing catalogs and added `lo`, `sr` and `tr`. None was removed, and no right-to-left locale was held back.
- **Chromium locales.** Since #707 the same script keeps `build.electronLanguages` in line with the shipped catalogs, so it added `sr` and `tr` there. `lo` has no Chromium locale of its own.

| Locale | Translated | Strings |
| --- | --- | --- |
| cs | 94% | 1037 |
| en-ca | 95% | 1050 |
| en-gb | 100% | 1100 |
| gl | 96% | 1066 |
| hu | 95% | 1050 |
| lo | 95% | 1050 |
| nl | 100% | 1100 |
| pl | 99% | 1090 |
| sr | 100% | 1100 |
| tr | 100% | 1100 |

## How to test this

**Platforms:** any for steps 1 to 3. Step 4 needs macOS for `Info.plist` and Windows for the `.exe`. Steps 4 and 5 use the Buildkite build for the current head.

**Starting state:** this branch checked out with `node_modules` installed. Run every command from the repository root.

1. `npm run lint` is clean and `npm test` passes.
2. `node -e "console.log(require('./package.json').version)"` prints `2.0.0`.
3. `python3 -c "import json;d=json.load(open('package-lock.json'));print(d['version'], d['packages']['']['version'])"` prints `2.0.0 2.0.0`.
4. On the Buildkite build for the current head, the artifacts are named `wordpress-contributor-toolkit-2.0.0-*`. The `app.asar` root holds exactly `node_modules`, `package.json` and `src`, with npm 11 under `node_modules/npm` and no `.codesigning`. On macOS, `CFBundleShortVersionString` reads `2.0.0`.
5. In the installed app, set the language to Turkish in the settings and relaunch. The interface is in Turkish.

**What must not have happened:** no dependency version may change. `git diff trunk -- . ':!src/languages'` shows five changed lines: the three root version fields and the two new `electronLanguages` entries.

## Risks and limitations

None known. RELEASING.md still says to expect exactly three changed lines outside `src/languages/`. Since #707 a release that adds a locale with a Chromium locale changes `electronLanguages` too. #710 rewrites RELEASING.md and can update that line.

## Related

Milestone `v2.0.0`. Release notes follow on the release.

---

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

0 [fix here] · 0 [follow-up]. Automatic checks only, as in #681: this change has no logic to review.

- **Review:** completed, deterministic layer, in session; head `9df142b` / base `630253f` (trunk). `npm run lint` passes, `npm test` passes (2214 tests, 0 failures, 2 skipped), both files are valid JSON, and the diff outside `src/languages/` is the three root version fields plus the two `electronLanguages` entries. Outcome: nothing to fix.
- **Since review:** none.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ryanwelcher added a commit that referenced this pull request Oct 23, 2026
- Allow the build.electronLanguages lines in the bump diff, which i18n:download rewrites since #707.
- Check the signatures in section 2: codesign, spctl and stapler on macOS, and the Authenticode certificate of the Windows installer.
- Say that Buildkite reports nothing to the pull request, so the person confirms the build's commit.
- Merge with --match-head-commit, and let the skill merge when the person asks.
- Check the docs site's Download button from a new tab, since it keeps the release it read for the tab's session.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove all the not used Chromium locales Remove unused PHP versions Remove Playground Asyncify builds

2 participants