Skip to content

perf: faster app startup - #2934

Open
bajrangCoder wants to merge 7 commits into
mainfrom
perf/startup
Open

bajrangCoder wants to merge 7 commits into
mainfrom
perf/startup

Conversation

@bajrangCoder

Copy link
Copy Markdown
Member

Replaces #2930 and #2931, which merged early and are reverted in #2933. This PR is based on revert-startup-perf so the diff shows all of the startup work. Merge #2933 first; this PR then retargets to main.

Summary

Startup work that keeps app and plugin behaviour unchanged. Measured on a Moto Edge 40 Neo, production build:

  • main.js: 3,313 KB → 1,997 KB (−40%), so there's less to parse before anything runs.
  • Editor visible: ~1.6–1.8 s after the page starts loading.
  • Time from the splash hiding to all plugins being loaded: ~3.4 s → ~1.5 s. Plugin loading also uses far less main-thread CPU, so the UI responds sooner after the splash.

Changes

Startup sequence (src/main.js)

  • No fixed 500 ms splash delay. The splash hides once the app has rendered a frame. Plugins, login check and ads still run afterwards, in the same order.
  • Parallel native calls. Encodings, storage folders, install source and Android version are requested together instead of one after another.
  • Play Billing check runs alongside startup.
    • It starts at deviceready instead of blocking it.
    • On free builds, plugins (theme plugins included) still wait for it, so they initialize with the settled Pro status.
    • Non-Pro users also wait for it before the UI is usable, because restoreTheme() resets and saves a paid theme when a dialog closes while HAS_PRO is false.
    • Paid builds are always Pro and never wait.
    • Ads and the login check still run after the purchase result. A found purchase is cached.
  • Native splash fade: 500 ms → 150 ms (config.xml).

Smaller main.js

  • Terminal (xterm and add-ons) loads on demand through components/terminal/loader.js.
    • xterm.css stays in main.css.
    • The module is loaded before plugins, so the synchronous plugin terminal APIs behave as before.
    • Modules that only need theme data or defaults import the small terminal modules that don't pull in xterm.
  • Run/preview (lib/run) loads on first use. It pulls in markdown-it, its plugins and the markdown preview. runLazily captures the requested file first.
  • mime-types loads on demand at each lookup site (utils/mimeTypes.js).
  • Built-in editor themes skip the load-time check. They are still validated when applied; plugin themes are still validated when registered.
  • Build target is WebView 67+ (the app's minimum) instead of ES2015. core-js uses swc entry mode, so it includes only the polyfills that WebView needs, and every global a plugin could rely on is still present.

Native startup

  • Encodings are cached. The Android charset list is cached in localStorage, keyed by app version, Android version and device model, so the native call runs only when one of those changes.
  • Sidebar apps: the four built-in sidebar app chunks load in parallel and are added in their original order.

Command registration (found by profiling on the device)

Every command registration, including each plugin addCommand, rebuilt the whole keymap. Each rebuild compared every binding with every other one and re-parsed both key strings on each comparison, then reconfigured every editor pane. That was ~1.8 s of main-thread CPU while plugins loaded.

  • Cached key parsing. Canonical key strings are cached; the cache is capped at 1,000 entries.
  • No map copies. The conflict check finds the first conflicting binding without copying the claimed-keys map for every candidate.
  • Rebuild on read. The keymap is rebuilt when next read instead of on every registration, so reads are always current. The command object registerExternalCommand returns still has its final key and description immediately.
  • One editor update per burst. Keymap refreshes are applied once per burst in a microtask, which runs before the next key event.

Existing bugs fixed along the way

  • disabledMap was out of scope in the owned plugins list and crashed it when billing is available.
  • Cancelling the sponsor form caused an unhandled rejection.

Review feedback from #2930 and #2931

  • Plugins seeing a stale Pro status: fixed, plugins wait for the purchase check on free builds.
  • Terminal chunk awaited before plugins: kept on purpose. The chunk is fetched on every launch to restore terminal sessions, and waiting keeps the synchronous plugin terminal API unchanged. Measured cost: ~40 ms before plugins start.
  • Wrong file could run during the first load of the runner: fixed, the file is captured before loading.
  • Key cache could grow without limit: fixed, the cache is capped.

Not included

  • Restoring only the active tab. Implemented and measured, then dropped: no gain on the test device, and it carried risk.
  • Lazy-loading Emmet. It's the largest remaining part of main.js, but it's wired synchronously into editor creation. Left for a separate change.

Testing

  • vitest run: 728/728 passing. New tests: commandRegistryKeymap.test.js (batched keymap updates, current reads, removal, conflicts) and runLazily.test.js (runs the requested file after a tab switch).
  • Production build checked: xterm is not in main.js, and its styles are still in main.css.
  • Startup timings were measured on the device with temporary marks, which are not part of this PR.

🤖 Generated with Claude Code

bajrangCoder and others added 7 commits September 24, 2026 18:43
…mand registration (#2931)"

This reverts commit 60acb25.

The startup work is being consolidated into a single PR for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts commit 5a7cc93.

The startup work is being consolidated into a single PR for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove the fixed 500ms delay before hiding the splash; reveal after
  the app has rendered a frame.
- Run independent native startup calls (encodings, storage dirs,
  installer, Android version) in parallel.
- Run the Play Billing purchase check alongside startup. Non-Pro users
  still wait for it before the UI is usable so paid themes are never
  treated as locked; ads and the login check still wait for it.
- Load the terminal manager (xterm and addons) on demand instead of in
  main.js; keep xterm.css in main.css and have it loaded before plugins
  so the synchronous plugin terminal APIs behave as before.
- Target WebView 67+ instead of ES2015 and let swc reduce the core-js
  entry import to the polyfills that WebView needs.
- Shorten the native splash fade to 150ms.
- Fix disabledMap being out of scope in the owned plugins list and an
  unhandled rejection when the sponsor form is cancelled.
- Add temporary startup timing marks (src/lib/startupPerf.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On free builds, a cached acode_pro value no longer waited for the Play
Billing check, so plugins (including theme plugins) could initialize
before a downgrade was applied. Wait for the check before loading any
plugin, as before the check ran in parallel. Paid builds are always Pro
and do not wait.

Also remove the temporary startup timing marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Load lib/run (markdown-it, its plugins and the markdown preview) on
  first run/preview instead of in main.js.
- Load mime-types and its MIME database on demand at each lookup site.
- Skip load-time validation for bundled editor themes; they are still
  validated when applied, and plugin themes are still validated when
  registered.
- Cache the Android charset list per app build, OS version and device
  model instead of rebuilding it natively on every launch.
- Fetch the sidebar app chunks in parallel while adding them in their
  original order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each command registration (including every plugin addCommand) rebuilt the
whole keymap, re-parsing both key strings for every pairwise conflict
check, and immediately reconfigured every editor pane. Profiling on a
device showed ~1.8s of main-thread CPU spent here while plugins loaded.

- Cache canonicalized key strings; the result depends only on the input.
- Find the first conflicting claim without copying the claimed-keys map
  for every candidate.
- Rebuild the keymap lazily: registration marks it dirty and every
  reader rebuilds first, so reads stay current. The returned command
  still has its final key and description immediately.
- Apply keymap refreshes once per burst in a microtask, which always
  runs before the next key event.
- Drop the module-load rebuild that setKeyBindings immediately repeated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- runLazily captures the active file before loading the runner chunk and
  passes it to run(), so switching tabs during the first load cannot run
  or preview a different file.
- Cap the canonical key binding cache so repeated registrations with new
  keys cannot grow it without limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from revert-startup-perf to main September 24, 2026 13:19
@greptile-apps

greptile-apps Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

The PR is not ready to merge until purchase revocation and an unbounded billing startup wait are addressed.

Summary

The PR shortens startup by deferring large modules, parallelizing native initialization, caching encodings, and batching command-keymap work.

  • The new purchase cache can restore revoked Pro access on an offline launch.
  • Making the unbounded billing check a startup prerequisite can leave free-build users on the splash screen.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Device ready] --> B[Start purchase check]
  A --> C[Initialize native services]
  C --> D[Build workspace]
  B --> E[Wait for Pro status]
  D --> E
  E --> F[Load theme plugins and restore files]
  F --> G[Reveal app]
  G --> H[Load regular plugins, then login and ads]
Loading

Reviews (1) · Last reviewed commit: "fix: run the requested file and bound th..."

@greptile-apps

greptile-apps Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P1 Security Revoked Pro Access Returns Offline src/main.js:477 ▶

    A confirmed purchase is now saved as acode_pro, but a later check that finds no purchase does not clear it. After a refund or revocation, an online launch can remove Pro access for that session, yet the next offline launch reads the stale value and skips verification. Paid themes and icons become available again, and ads are suppressed. Clear the stored entitlement when verification finds no purchase.

    How this was verified: Startup reads the stored Pro flag before checking purchases, and the offline return bypasses the only branch that can downgrade it.

  • P1 Billing Can Block Startup src/main.js:814 ▶

    Free-build startup now waits for Play Billing before loading theme plugins, and the splash also waits for the check when the user is not Pro. If iap.startConnection or iap.getPurchases never calls back, neither wait has a timeout, so the app remains on the loading screen indefinitely. Previously, the purchase check ran after the workspace was revealed. Give this startup wait a bounded fallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant