perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

perf(coil): halve the desktop release bundle (#53) - #89

Merged
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size
Aug 12, 2026
Merged

perf(coil): halve the desktop release bundle (#53)#89
radroid merged 1 commit into
mainfrom
t3x/desktop-bundle-size

Conversation

@radroid

@radroidradroid commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes#53.

app.asar goes from 189.66 MiB / 14,765 files to 99.02 MiB / 3,429 files (−47.8% bytes, −76.8% files). Measured, not estimated, and re-measured on 136b3514f after the #71 Coil rename.

BaselineAfterΔ
app.asar189.66 MiB99.02 MiB−90.64 MiB (−47.8%)
Files in the asar14,7653,429−76.8%
.zip (what the updater downloads)144,592,118 B124,467,727 B−20.1 MB
.dmg150,392,957 B129,226,158 B−21.2 MB
electron-builder phase~126 s~48 s−62%
Staged vp install --prod12.6 s4.4 s−65%

The compressed artifacts fall 14% rather than 48% because what was removed is text, which compressed well already. The .zip number is the one users pay, on every release.

Rebuilt on the renamed fork

This branch was originally cut before #71. It has been rebuilt from 136b3514f, not merged forward, so nothing carries an old path:

The rename moved none of the measurements. Both builds were re-run on the new base: the packed asar is the same size on both sides (189.66 → 99.02 MiB), only the .dmg/.zip moved by ~3 KB from the renamed product string. All 66 non-wildcard globs still match a package that is actually shipped, so none has gone stale. The 385-chunk renderer byte-identity proof and the Clerk execution probe were both re-run and both reproduce.

Sync cost: one upstream file, strictly additive

Upstream-owned files touched1 (scripts/build-desktop-artifact.ts)
This issue's delta on it+21 / −0
That file's total fork delta+32 / −1 — the −1 is #70's pre-existing displacement
New seam rows0
New fork-owned files8, under scripts/coil/ and docs/coil/ — paths upstream has never had

The additive-seam invariant holds: no deletion count moved and the file count did not change. The only realistic conflict is textual — upstream adding its own entry to DESKTOP_FILE_EXCLUSIONS collides with the ...coilExtraFileExclusions, line at the end of that array, a keep-both resolution.

The justification for the env hook has been corrected. The earlier version of this PR said an inline list would "cost a second seam row on a second upstream file." Since #71 that is no longer true — build-desktop-artifact.test.ts already carries a fork row. The real reason is stronger and survives the rename: the list is 67 globs and grows, so inline it makes every future size fix an edit to an upstream test assertion, in a file resolved by hand at every sync, where the conflict is decided by re-reading 67 lines instead of one. Through the environment, an unset environment packages exactly what upstream packages and upstream's deepStrictEqual keeps passing untouched.

What was cut, and the evidence for each

Web renderer source maps — 37.4 MiB. The largest line item in the bundle, unmentioned in #53. Turned off with T3CODE_WEB_SOURCEMAP=0, an existing upstream variable, so the biggest win costs zero seam rows. Provably behaviour-preserving: all 385 renderer chunks are byte-identical after stripping the trailing //# sourceMappingURL= comment, and their content-hashed filenames do not move.

Third-party source maps — 18.2 MiB. Node only parses a .map under --enable-source-maps, which the desktop backend passes through rather than sets. First-party maps (~12 MiB) are kept so an operator who does export it still gets legible frames for our own code.

Clerk's browser SDK — 23.2 MiB, 16 packages. The main process requires only two of @clerk/electron's six entry points (root and /storage), which between them require only electron and electron-store. The entries pulling the browser SDK are /react and /passkeys, whose only importer is apps/web/src/main.tsx — compiled before staging, and run in a renderer created with sandbox: true, nodeIntegration: false, which has no module resolution at all.

Verified by execution: loading @clerk/electron and @clerk/electron/storage from inside the optimized asar under ELECTRON_RUN_AS_NODE returns createClerkBridge, setupPasskeysMain and storage, and the transitive module list touches none of the excluded packages. @clerk/electron, @clerk/electron-passkeys and its native binaries are not excluded, and a test asserts they never will be.

The shiki / @pierre/diffs tree — 21.0 MiB, 50 packages. This answers #53 §3.3. The server imports one subpath (utils/parsePatchFiles) and the server build bundles it — bin.mjs has no @pierre/* import left. The highlighting users see is the web client's own per-language chunks, so @shikijs/langs was shipped twice and this was the copy nobody could load.

Two tools, and two bugs testing found in them

desktop-bundle-reachability.mjs (analysis) follows literal import edges and string-mention edges. The second exists because PlaywrightInjectedRuntime.ts resolves playwright-core through a const — an import-only scan calls 10.2 MiB of live code dead.

verify-desktop-bundle.mjs gates every release on both platforms. Testing the gate rather than trusting it found two real defects:

  • It originally read only the archive. On Windows asarUnpack puts the bundles and all of node_modules on disk, so it would have failed every Windows release.
  • Its severity originally depended on attribution. A real build with a deliberately over-broad !**/node_modules/effect/**/* produced a bundle that cannot start and the gate went green. Severity is now unconditional; attribution only makes the message actionable. That case is now a fixture test.

Five corrections to issue #53

In docs/coil/desktop-bundle-size.md:

  1. The size table measured du block allocation, not content.core-js "15 MB (3,684 files)" is 1.25 MiB — 3,684 files × 4 KiB blocks. Every many-small-files package is overstated the same way, which is why the Clerk tree was not the biggest win.
  2. Source maps were 35.6% of the asar and go unmentioned.
  3. DESKTOP_FILE_EXCLUSIONS is not fork-owned. The recommendation to prefer it is right; the reason is not.
  4. The mac leg is not the release critical path. The legs run in parallel; Windows is 17.5 min to mac's 9.6. And --concurrency-limit 1 costs only 48–58 s, so raising it buys ~20 s while re-opening Windows desktop build intermittently fails with STATUS_DLL_INIT_FAILED (0xC0000142) in parallel vp tasks #47's crashes — recommend leaving it alone.
  5. The pnpm store is already cached.setup-vp's cache: true restores a 1,943 MB cache with downloaded 0.

The real remaining build-time target is the 442 s Spectre MSVC install — 42% of the Windows leg. Not touched: it cannot be verified without a Windows release run, and this workflow publishes on success.

Testing

  • 86 tests across the affected files pass; upstream's build-desktop-artifact.test.ts passes unmodified.
  • Full before/after builds on the post-rename base; the verifier passes on both and fails the deliberately-broken one.
  • Real-asar fixture tests pin the header offset arithmetic.
  • tsgo --noEmit clean, vp lint clean, vp fmt applied. Formatter churn outside this change's scope was reverted; the 50 deletions in the diff are all table-padding in the fork-owned ledger.

Not done: a full GUI launch with an interactive sign-in. A live app instance was running on port 3773 and a second instance risked disturbing that session. The renderer half is covered by the byte-identity proof and the main-process half by the execution probe — installing this build and signing in once is the natural last check before it ships.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 12, 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecdf249e-1114-4881-912b-c683a53efde6

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

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.

@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch 2 times, most recently from 59d3ce1 to 510922aCompareAugust 12, 2026 15:23
@radroidradroid changed the title perf(t3x): halve the desktop release bundle (#53)perf(coil): halve the desktop release bundle (#53)Aug 12, 2026
app.asar 189.66 MiB / 14,765 files -> 99.02 MiB / 3,429 files (-47.8%, -76.8%
files). The updater's .zip drops 20.1 MB; the electron-builder phase drops 62%.
Three cuts, each measured from the packed asar rather than the install tree:
- Web renderer source maps (37.4 MiB) via T3CODE_WEB_SOURCEMAP=0, an existing
upstream variable. All 385 renderer chunks are byte-identical afterwards apart
from the trailing sourceMappingURL comment.
- Third-party source maps (18.2 MiB). First-party maps are kept, so
NODE_OPTIONS=--enable-source-maps still gives readable frames for our code.
- Two renderer-only dependency trees: Clerk's browser SDK (23.2 MiB, 16
packages) and @pierre/diffs' shiki tree (21.0 MiB, 50 packages). Both are
bundled by a client build before staging, and the renderer runs sandboxed with
nodeIntegration off, so neither is resolvable at runtime.
Sync cost is one upstream file, +21/-0. The seam is an env hook on
scripts/build-desktop-artifact.ts rather than an edited literal: upstream's test
asserts DESKTOP_FILE_EXCLUSIONS deep-equals exactly its one entry, and the fork's
list is 67 globs and grows, so an inline list would make every future size fix an
edit to an upstream test assertion. An unset environment packages exactly what
upstream packages. No new seam rows, no new deletions.
Two new fork-owned tools:
- desktop-bundle-reachability.mjs analyses a staged build. It follows literal
import edges AND string-mention edges, because PlaywrightInjectedRuntime.ts
resolves playwright-core through a const and an import-only scan calls 10.2 MiB
of live code dead.
- verify-desktop-bundle.mjs gates every release on both platforms, failing any
packaged import that is not loadable. Found two bugs in itself under test: it
would have failed every Windows release by ignoring app.asar.unpacked, and an
earlier severity split passed a deliberately-broken build.
Re-measured on 136b351, after the #71 Coil rename, #58's dependency
re-resolution and #92 retiring the local autobuild. The packed asar is the same
size on both sides of the rename, and all 66 non-wildcard globs still match a
shipped package.
Corrects issue #53 on five points, recorded in docs/coil/desktop-bundle-size.md —
its size table measured `du` block allocation (core-js "15 MB" is 1.25 MiB of
content), it misses source maps entirely, DESKTOP_FILE_EXCLUSIONS is not
fork-owned, the mac leg is not the release critical path, and the pnpm store is
already cached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@radroid
radroidforce-pushed the t3x/desktop-bundle-size branch from 510922a to 451c5d1CompareAugust 12, 2026 15:36
@radroid
radroid merged commit d854dbd into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/desktop-bundle-size branch August 12, 2026 15:48
Sign up for freeto 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.

Desktop release build optimization: size, time, and one false lead

1 participant

@radroid