Skip to content

perf: faster app startup - #18

Open
bajrangCoder wants to merge 7 commits into
Acode-Foundation:mainfrom
bajrangCoder:perf/faster-startup
Open

bajrangCoder wants to merge 7 commits into
Acode-Foundation:mainfrom
bajrangCoder:perf/faster-startup

Conversation

@bajrangCoder

@bajrangCoder bajrangCoder commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Makes the editor usable much sooner on Android and iOS, and keeps plugins, file I/O and optional features from slowing startup. App and plugin behaviour stay the same.

Results

Cold starts of a signed release on a Motorola Edge 40 Neo (Android 15) with a real set of installed plugins and developer mode on, plus the iPhone 17 Pro simulator (free edition).

Before After
Android: editor visible (from page start) 2,100 ms ~860 ms
Android: all plugins loaded 10.8 s ~4.0 s
Android: file opened from another app ~7.7 s ~2.3 s
iOS: editor visible 995 ms ~406 ms
main.js (Android / iOS) 3,349 / 3,356 KB 1,515 / 1,460 KB

From tap to editor on the same Android device it is now ~1.15 s, including ~430 ms for Android to show the native splash and start the WebView.

Startup sequence

  • The splash hides once the app has rendered a frame instead of after a fixed 500 ms delay.
  • Independent native calls (encodings, storage folders, install source, Android version) run in parallel.
  • The purchase check runs alongside startup.
    • Free builds still settle Pro status before plugins initialize, and non-Pro users before the UI is usable.
    • Paid builds never wait.
    • The wait is capped at 5 s.
  • Developer tools (eruda) start after the editor is visible. Console output and errors captured during startup are replayed into eruda's console, so nothing is lost. With developer mode off, the capture is dropped as soon as settings load.

Smaller startup bundle

Code that is not needed for the first frame now loads on demand:

  • Terminal / xterm. It is ready before plugins load, so the synchronous plugin terminal APIs behave as before.
  • Run/preview with markdown-it.
  • mime-types.
  • The LSP client. It loads only when an enabled server handles an open file, after the editor is visible. This covers the client manager, editor features, @codemirror/lsp-client, marked and the protocol types. The plugin-facing server registry, the diagnostics UI and the server launcher stay eager.
  • Emmet. It loads, through an editor compartment, only when a file uses an Emmet syntax.

Polyfills are bundled once (native.js always loads first). swc targets the oldest supported engine instead of ES2015 (chrome >= 67 on Android, ios_saf >= 18 on iOS), with core-js in entry mode so plugins still get every global.

Less startup work

  • Encodings are cached per app version and device.
  • Built-in editor themes skip the load-time check; they are still validated when applied.
  • Built-in sidebar app chunks load in parallel.
  • Command registration no longer rebuilds the whole keymap per command. The keymap is rebuilt lazily on read and applied to editors once per burst, before the next key event.

File I/O and plugins

  • UTF-8 text is decoded and encoded in the WebView instead of being sent to native and back as base64. A 3.7 MB read went from 814 ms to 9 ms with identical output. Invalid UTF-8, lone surrogates and other charsets still use native, and the BOM handling matches each platform.
  • Android file URLs are formatted in JS when the result provably matches native, which saves a resolve round trip per read and per plugin path. It matched native for every path checked on device, and falls back otherwise. iOS keeps resolving natively.
  • Fewer native round trips per plugin. The plugin's cache file and token are prepared while its script downloads.
  • Files shared from other apps no longer wait for the slowest plugin. They open after a 1 s grace period. Files a plugin may handle (remembered handler extensions, PDF/Office documents) still wait for plugins, and order is kept.

Fixes

  • A billing service that never answers can no longer keep the app on the splash screen.
  • Revoked Pro status no longer returns offline.
  • The owned plugins list no longer crashes (disabledMap scope).
  • Cancelling the sponsor form no longer causes an unhandled rejection.

bajrangCoder and others added 7 commits October 7, 2026 16:00
- Reveal the app after its first rendered frame instead of a fixed 500 ms delay
- Run independent native startup calls and the purchase check in parallel
- Bound the startup purchase check so an unresponsive store cannot hold the splash screen
- Load the terminal, run/preview and mime-types code on demand
- Target the oldest supported WebView/Safari instead of ES2015
- Cache the native encoding list, skip load-time validation of built-in themes
- Rebuild the command keymap lazily and apply it once per burst
- Load the built-in sidebar app chunks in parallel
- Fix the owned plugins list crash and the unhandled sponsor form cancellation
native.js already installs core-js/stable and boot.js always loads it before
main.js, so main.js no longer bundles a second copy (-113 KB).
- Decode and encode UTF-8 in the WebView instead of round-tripping base64
  through native; invalid UTF-8, lone surrogates and other charsets still
  use native so behaviour is unchanged (BOM kept on Android, stripped on iOS)
- Format Android internal file URLs in JS when the result provably matches
  native, skipping a resolve round trip per read and per plugin path
- Load plugins with fewer native round trips: no exists() check for main,
  and the cache file and plugin token are prepared while the script loads
- Open files shared from other apps after a short plugin grace period
  instead of waiting for every plugin; files a plugin may handle (remembered
  handler extensions, documents) still wait as before
- Move the language-server client (clientManager, editor features,
  @codemirror/lsp-client, marked, protocol types) into a chunk that loads
  only when an enabled server handles an open file, after the editor is
  visible; the plugin-facing registry and diagnostics UI stay in main.js
- Load Emmet through a compartment only when a file uses an Emmet syntax
- Start developer tools (eruda) after the editor is visible and replay the
  console output and errors captured during startup into its console
- main.js: 1,895 KB -> 1,515 KB
Android's native long-press fires after our tap-hold timer and blurs the
terminal, which closed the keyboard, resized the grid and made the
selection menu disappear and reappear. Refocus in the same task so the
IME never hides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hide banners when a Pro purchase is confirmed after the startup check
  timed out, as purchase restoration does
- Clear the cached Pro flag when the store reports no purchase, even if the
  result is too late to change this session
- Wait for plugins before opening shared files until a full plugin load has
  recorded which extensions plugins handle (first launch after updating)
- Cancel a pending developer tools initialization when developer mode is
  turned off, so Eruda does not activate afterwards
- Move the startup purchase check into lib/verifyProPurchase.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant