How omp traded 50 MB for 7.7x faster startup

Efe Çakıcı Efe Çakıcı #performance#startup#bytecode#bun

I updated omp and it opened instantly instead of the usual split-second lag. 24 alternating runs per version later: 232 ms down to 30 ms, and a binary 50 MB heavier.

I updated omp last night and it opened instantly instead of the usual split-second lag. I did not trust my eyes, so I benchmarked it: 24 runs per version on my M4 Pro, alternating 18.1.18 and 18.2.0 so thermal drift would not favour either one.

The numbers were real. 18.2.0 starts in 30 ms. 18.1.18 took 232 ms. Four days apart, same machine.

18.1.18 18.2.0 Change
Binary size 135.7 MB 185.4 MB +49.7 MB
--version 232.5 ms 30.3 ms 7.7x faster
ps 318.5 ms 61.7 ms 5.2x faster

The two commands do not speed up by the same factor, so I report them separately. For --version, the slowest run of the new version is 32 ms and the fastest run of the old one is 230 ms. Nothing overlaps.

So the binary got 50 MB heavier and starts several times faster. I wanted to know which change did that, so I read the commits from that day.

The build script explains it

The release notes do not mention startup. They talk about model routing and rate limits. One commit from that day does.

omp ships as a single executable built with Bun. The build bundles the TypeScript codebase into one file, roughly 20 MB by the commit’s own description. That is 20 MB of source the engine has to turn into bytecode before anything runs. Engines parse functions lazily, on first call, so part of that cost keeps recurring during a session instead of landing once at boot.

The commit adds one line to the build script:

// Precompiled bytecode skips parsing the ~20 MB bundle at boot:
// `omp --version` 256 ms -> 30 ms on M4 Max (+52 MB binary).
// Bytecode rejects top-level await in the bundle graph.
bytecode: true,

That moves the work to build time. The executable carries the source plus a precompiled bytecode copy, and the engine skips straight to running it instead of parsing 20 MB of TypeScript first.

Bytecode caching is decades old. Python ships .pyc files and Node has a compile cache. What I had not seen measured is the price on a bundle this size, and what it forbids.

What it costs, and what it forbids

I did not isolate how much of the 50 MB is bytecode. Bun’s documentation says the bytecode file typically runs two to eight times larger than the JavaScript it comes from, so a 20 MB bundle landing in that range is consistent. The rest of the growth may come from other changes in the release. I measured the binary at both endpoints. I did not look inside it.

The trade is 50 MB of disk against 200 ms of startup. A tool you launch once and leave open does not care about 200 ms. A tool invoked from a shell prompt pays it hundreds of times a day. The same goes for one called from a script or a loop of subshells.

The constraint is the part I have not seen mentioned anywhere else. Bytecode compilation rejects top-level await anywhere in the bundle graph. Turning it on changes which language features the project may use. The people writing the code pay that cost, and no benchmark shows it.

Bun’s documentation adds another: bytecode is not portable across Bun versions. Every Bun update has to regenerate it during the build, and Bun silently falls back to parsing the source when the versions do not match.

Two independent measurements

I measured 30.3 ms before I read the build script. The comment in that script claims 30 ms on an M4 Max.

M4 Max (author) M4 Pro (mine)
Before 256 ms 232.5 ms
After 30 ms 30.3 ms
Binary +52 MB +49.7 MB

Different machines, same answer on both ends. The changelog puts it at “~30 ms instead of ~250 ms” and adds one number I did not measure: the interactive prompt accepts input about 300 ms sooner.

Bun’s documentation gives 2 to 4 times faster for applications over 5 MB. I measured 7.7 times on --version. I cannot account for the gap from my own numbers. That table is general guidance across many projects, and it is not a controlled baseline for this one. I have no second 20 MB project to compare against.

What this does not tell you

One laptop, macOS, Apple M4 Pro. Your numbers will differ.

I only timed --version and ps. Both exit early. A real session loads a repository and talks to a model, so neither the 7.7x nor the 5.2x figure carries over to how the tool feels in use.

I did not check whether 18.1.18 could be warmed up to something closer either. Some of that 232 ms might be first-run cost rather than parsing.

Both binaries are on the project’s GitHub releases page. The measurement is a loop around subprocess.run and a median.