Performance
Status: partly implemented. The harness checks the start-up, idle memory, idle CPU, bundle and size budgets on every pull request. See implementation status.
Performance comes first among Astela’s principles. Budgets are measured in CI, and every regression fails the build. The decision record is ADR 0017 (performance budgets in CI); see the decision record index.
Baseline
Section titled “Baseline”These facts set the starting point (as of October 2026). Unless noted otherwise, they refer to Electron 45.0.0 with Chromium 156.0.8078.48, Node 24.22.0 and V8 15.6.
- Package size: Electron itself makes up about 97 % of the package size. The Electron 45 zip archives measure 123.9 MB on Linux, 158.2 MB on Windows and 120.7 MB on macOS arm64. Typical installers of real-world apps on Electron 44: NSIS 121 MB, dmg 133 MB, AppImage 129 MB, deb 100 MB, rpm 89 MB.
- Memory: the fixed processes (browser, GPU and network service) take about 220–250 MB resident. Every sandboxed renderer adds about 17–28 MB of private memory.
- Startup: spawn to
readytakes about 149 ms (Linux, Electron 45 with its Node snapshot); spawn to the first contentful paint of an empty page takes about 286–317 ms (Apple Silicon). - CI runners:
- Linux and Windows: 4 vCPU, 16 GB.
- macOS arm64: only 3 vCPU, 7 GB.
- No GPU rendering, only the software path.
- Variance is high, so measurements are taken relative to a bare Electron reference in the same job.
Most of the size and the fixed memory cost come from Electron itself. The budgets therefore limit what Astela adds on top of a bare Electron app with the same structure.
Mandatory techniques
Section titled “Mandatory techniques”These techniques apply from M1:
- A tiny main entry point:
- The BootGuard, policies, settings and the window come first; everything else loads through
import()after the first paint. - Nothing awaits large bundles before
ready. Menu.setApplicationMenuis set early.- The policies are read once at start. The focus that the first window gets while it opens does not read them again; on Windows, that read started two more
reg.exeprocesses while the shell loaded.
- The BootGuard, policies, settings and the window come first; everything else loads through
- Shell UI from the
app://scheme with thecodeCacheprivilege andv8CacheOptions: 'bypassHeatCheck', served withprotocol.registerSource, so files stream without the main thread (see ADR 0008). The pages come without aCross-Origin-Opener-Policy, so that each loads in the renderer that its view started (see security): with the header, Chromium started a second renderer for the shell and for the new tab page, and the shell was interactive 27 ms after the page of the bare reference instead of 12 ms (medians of 20 starts on Linux, November 2026). - Bundles: main, preload and UI are built with Vite and Rolldown. Module chunks load only when their module is active (see modules). The bundles keep licence comments but no doc comments; that alone took the main process start-up path from 140 to 104 KB. Code that only some starts need, such as the self-test of the packages, the help page for a missing sandbox and the update feed, is in chunks of its own that load when they are needed; on 2026-11-01 that took 11 KB off the start-up path, which was then 138 KiB. The handlers of the browser’s own pages other than the new tab page load with the first of these pages, before it loads; on 2026-11-09 that took the start-up path from 149.4 to 141.6 KiB. The code of the content blocker that works with its filter engine loads with the engine, at the first web page; the start-up path keeps only the part that decides whether a request needs the engine, which took it from 148.0 to 143.8 KiB on the same day. The parsers of the Windows registry and of macOS property lists load with the first policy read on their system (ADR 0037), which took it from 146.1 to 138.6 KiB. Requests of the shell that its first paint does not need, such as find in page, the browser menu and the popovers, load with the first of them, and the code that shuts the browser down loads when it quits; together that took the start-up path of 0.1.0-beta.4 from 149.9 to 145.4 KiB, to leave room for the settings of 0.1.0-beta.5. The main bundle is not minified, so the stack traces of Astela’s own code stay readable; its chunks that hold only dependencies, such as valibot, which every start reads, are minified, with the names of functions and classes kept. That took the start-up path from 147.2 to 137.8 KiB on 2026-11-09.
- No synchronous IPC. State patches are batched per frame. SQLite and hashing run in a
utilityProcess(see IPC and data). - Tab resources:
- Tabs are lazy and restore their history with
navigationHistory.restore(). - A sleeping tab keeps its navigation entries and a thumbnail; its
webContentsis closed. backgroundThrottlingstays on.- Background tabs use
focusOnNavigation: false.
- Tabs are lazy and restore their history with
- UI and package:
- UI animations use only
transformandopacity, respectprefers-reduced-motionand run no timers while idle (see design system). electronLanguagesis limited to the shipped UI languages, English and German, which saves up to 12.8 MB compressed on Windows (as of October 2026).- The asar archive is unpacked as little as possible.
- UI animations use only
- Decided later, based on measurements:
- A V8 snapshot, only if it gains more than 50 ms, because it replaces the built-in Node snapshot.
--process-per-sitefor Astela’s own UI surfaces only, never for web tabs.- A spare renderer for faster new tabs.
New dependencies need a stated reason. Anything larger than 20 KB gzipped in the UI, or with native code, needs a decision record (see the maintainer workflow).
Budgets
Section titled “Budgets”The budgets are CI gates: a regression fails the build. “Bare” means the bare Electron fixture described under measurement method.
| Metric | Budget | Type |
|---|---|---|
| Warm start: spawn to interactive shell | ≤ bare + 120 ms and ≤ 1.25 × bare; ceiling 1.0 s (Linux, Windows). On macOS this is the target; until the shell starts faster there, the gate is 1.75 × bare (see known gaps) | relative + ceiling |
| Spawn to first restored tab painted | ≤ interactive shell + 150 ms | relative |
Cold start (cache dropped: drop_caches on Linux, purge on macOS) |
≤ bare + 250 ms and ≤ 1.5 × bare; gates on Linux and macOS. Windows reports only, because its file cache cannot be cleared reliably. | relative |
| Memory, idle (1 window, new tab page), after 15 s | Linux PSS and Windows private bytes ≤ bare + 60 MB; macOS phys_footprint ≤ bare + 50 MB; ceiling 350 MB PSS |
relative + ceiling |
| Memory, 20 active tabs (20 local fixture origins) | ≤ 1.10 × bare with 20 views + 80 MB; ceiling 1.5 GB PSS | relative + ceiling |
| Memory, 1 active and 19 sleeping tabs | ≤ idle memory + 30 MB; at least 85 % of a tab’s memory released within 5 s | relative |
| CPU, idle for 60 s, 1 window | sum of all processes ≤ 0.5 % of one core and ≤ bare + 0.2 percentage points | absolute + relative |
| CPU, 20 static background tabs | ≤ 1.0 % of one core | absolute |
| Tab switch, live tab | p50 ≤ 33 ms, p95 ≤ 66 ms | absolute |
| Tab switch, sleeping tab | thumbnail ≤ 50 ms; content p95 ≤ 400 ms | absolute |
| Workspace switch (20 live tabs) | p95 ≤ 100 ms, no new processes | absolute |
| Installer per format | ≤ bare Electron installer of the same format, built in the same run, + 8 MB. Absolute caps are calibrated in the walking skeleton from measured bare packages and recalibrated for every Electron major. | delta |
| Unpacked size | ≤ bare + 25 MB | delta |
| UI JavaScript | start chunk ≤ 300 KB minified (about 90 KB Brotli); all UI JavaScript ≤ 1 MB; preload ≤ 20 KB; main startup path ≤ 150 KB; growth above 2 % or 10 KB per pull request needs a justification in the pull request | absolute |
Related targets
Section titled “Related targets”The M1 acceptance criteria and the settings, policy and theme design add these targets, each designed to be checked by an automated test:
- Opening and closing 100 tabs leaves memory at no more than 110 % of the starting value (no leak).
- Restoring a session with 100 tabs stays within the startup budget plus 150 ms; only visible tabs load.
- Full-text history search answers in less than 50 ms with 100,000 entries.
- Address bar suggestions from history, bookmarks and open tabs appear in less than 30 ms.
- A change in the theme editor is visible in less than 100 ms; switching a workspace theme is a token swap within one frame.
- Reading policies does not block the start: the last known policies apply before
app.whenReady(), and the sources are read while Electron starts (see settings, policies and themes and ADR 0025). Read synchronously, they took 51 ms of the warm start on Windows (median, 2026-11-01).
Measurement method
Section titled “Measurement method”Bare fixture
Section titled “Bare fixture”The reference is a bare Electron app with:
- the same Electron version,
- the same fuses and flags,
- the same topology: a
BaseWindowwith a UI view and N tab views, - the same local fixture pages.
Procedure
Section titled “Procedure”- One job runs three variants in alternation: the pull request build, the
mainbuild (the artifact of the last greenmainrun) and the bare fixture. Results are never compared across runs on shared runners. - Timing: n = 20, with median and p90. A metric fails only when a Mann-Whitney U test gives p < 0.01 and the effect exceeds the threshold.
- Memory: n = 10, with the threshold rule applied to the median, because PSS varies little.
- One automatic rerun happens before a check fails.
- The first start after an update is excluded from the gates and reported separately.
Memory metrics
Section titled “Memory metrics”- Linux: the sum of
Pssfrom/proc/<pid>/smaps_rollupacross all processes (gating). - Windows:
privateBytes. - macOS:
phys_footprint. - RSS is never summed, because shared pages would be counted once per process.
CPU load is the change in cumulativeCPUUsage over 60 seconds.
The browser runs with its update check turned off by the UpdatePolicy policy (0). The first check fetches the update feed 30 to 60 seconds after the start, inside the window, and what it costs depends on the network and on the feed: once the first release was live, the browser’s idle load on macOS rose from 0.01 to between 0.13 and 0.23 percentage points above the reference. The check repeats only every four hours, so a 60-second window would weigh it far too much. To measure locally in the same way, set the same policy.
Startup marks
Section titled “Startup marks”The harness will record these marks:
- spawn,
- first main-process JavaScript,
ready,- window visible,
- first contentful paint of the shell and the interactive mark,
- first tab painted.
History without commits
Section titled “History without commits”Measurement history is never committed to the repository:
- Every CI run will keep its results as a workflow artifact.
- Every release will carry a
perf.jsonasset. - The website will show the real measurements under
/features/performance.
Runner variance
Section titled “Runner variance”An A/A run once a week compares identical builds. It measures the runner variance and calibrates the thresholds.
The performance harness is designed to run on every pull request on all three operating systems; on macOS, the limited runners allow only relative values. See testing for the overall test strategy.
Implementation status
Section titled “Implementation status”The harness (tools/perf/run.ts) runs in the perf job of every pull request on ubuntu-24.04, windows-2025 and macos-15 and fails the build when a budget is exceeded. It checks:
- Warm start: n = 20 after two warm-up rounds, median against the bare reference and, on Linux and Windows, the ceiling. On macOS it gates at 1.75 × bare for now and reports whether the regular budget was met (see known gaps). A median over the budget fails only when a one-sided Mann-Whitney U test (
tools/perf/stats.ts) shows that the browser’s times are significantly larger than the reference times plus the allowed difference (p < 0.01); the job summary shows the p-value. On the 3-core macOS runners the difference to the reference varied between 73 and 152 ms across identical builds, so a median alone was not a reliable gate. Each variant keeps its profile between runs and quits normally, as in real use. The interactive mark is the ready time in the self-test report, taken once the shell has loaded and rendered; the report also carries the marksmain,policies,ready,window,settings,shellandrenderedin milliseconds since process start. After the ready time, the browser and the reference read the timing of their UI page and add it on the same clock:page.start(start of the navigation),page.fetch,page.responseEnd,page.interactive,page.domReady,page.domReadyEndandpage.loadStartfrom Navigation Timing, and in the browserpage.preloadandpage.script, marks that the shell’s preload and its main script set when they start to run. Pages fromapp://andfile://get no Resource Timing entries, so the marks cannot split the time per file. The job summary shows the medians of all marks in one table, in the order of the browser’s start-up, next to those of the reference and with the difference, so it shows in which step the browser loses time. - Memory, idle: n = 10 after 15 s, median, with the metric per operating system above. On Linux, the harness reads the memory maps of sandboxed processes through
sudo. - CPU, idle: one 60-second window per variant, after 15 s. When the browser is over the budget, each variant gets a second window, and the quieter window of each counts: an idle browser does not get busier by itself, while background work of the runner comes and goes. On Windows, one window of 15 runs in November 2026 measured 0.31 % where the others measured between 0.00 and 0.23 %. The
perfjob sets theUpdatePolicypolicy to0through the registry, managed preferences or/etc/astela/policies/managed/, and with--updates-offthe harness reads the browser’s log and fails if the update service was not off in every run. - UI JavaScript, preloads and the main start-up path: sizes of the built bundles.
- Unpacked size: the unpacked package against the Electron distribution.
The bare reference (app/tests/perf/bare/) runs in a copy of the Electron distribution as its app folder, with the fuses of a shipped package except the two asar fuses. Each group of measurements that fails is measured once more before the job fails. Results are kept as the perf-<os> workflow artifact.
Known gaps
Section titled “Known gaps”- Warm start on macOS: on the 3-core
macos-15runners the browser reaches its interactive shell 10 to 60 % later than the bare reference (median 28 % across 12 runs in October 2026), while Linux stays within 6 % and Windows within 16 %. In the two runs with start-up marks, the extra time fell between the window and the loaded shell: 379 and 508 ms there, against 254 and 172 ms for the reference’s page; thepage.*marks split this interval into the steps of the page load. The reference alone took between 337 and 935 ms on these runners, so an absolute ceiling would judge the runner. Until the cause is found and the shell starts faster on macOS, the harness gates macOS at 1.75 × bare, which still catches a large regression, and reports the regular budget as the target.
Not implemented yet: the comparison with the main build, the cold start, the tab, workspace and 20-tab budgets, the installer budgets, the first-tab-painted mark, the weekly A/A run and the perf.json release asset.