Hacker News new | ask | show | jobs
by hu3 1128 days ago
> Standalone executables. You can now create standalone executables with bun build.

> bun build --compile ./foo.ts

> This lets you distribute your app as a single executable file, without requiring users to install Bun.

> ./foo

This is big! Part of Go's popularity is due to how easy it is to produce self-contained executables.

And it seems to support amd64 and arm according too: https://twitter.com/jarredsumner/status/1657964313888575489

9 comments

QuickJS has a custom unicode library for this reason.
It’s slow as molasses in some parts though. (Deliberately, it’s a trade-off.)

The Unicode part of ICU shouldn’t be that large, however (on the order of hundreds of kilobytes), it’s the locale data that’s big[1]. Does Bun implement ECMA-402 internationalization? Even without locales, one of the largest parts of ICU data is normalization and casing tables, which I think bare ECMAScript does not require. (It does mean bare ECMAScript cannot adequately process Unicode text, but meh, you get what you pay for.)

[1] https://unicode-org.github.io/icu/userguide/icu_data/buildto...

Bare ECMAScript does require normalization [1] and case conversions (for the root locale only) [2].

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Ah, I see, String.prototype.normalize() is in ES6. I remembered that Duktape (which has its own, even slower Unicode library) can’t do it[1], but then it doesn’t try for ES6 either.

[1] https://github.com/svaarala/duktape/issues/1718

Is 90MB really that big when compared against the other types of binaries that would get deployed: containers and VM images? How big is the platform (k8s, docker, etc) you need installed to run your app? Probably more than 90MB.
A native Java "Hello World" can be around 8MB in size.[1] So, yes, 90MB is too much.

1. https://sergiomartinrubio.com/articles/getting-started-with-...

I feel like using Graal for the comparison is cheating a bit because it's so fundamentally different from what bun is doing. You need to compare it to tools that ship class files and a JVM, or something like PyInstaller which will have much much more overhead.
I wouldn’t call it cheating.

I’d also compare against stripped Rust binaries statically linked against musl.

PyInstaller actually seems to have less overhead, in terms of space, not "much much more". Building a hello world script with "pyinstaller --onefile" gives me a 5.6 MB executable on Linux or 4.9 MB on Windows.
A leaned up JRE (only containing the base jdk classes) and an included hello world is 47.2MB.
“The generated file is 7.7MB, which is quite impressive for a Java application since this executable does need a JVM.”

I assume this is a typo and they mean “does NOT”?

I think it isn't, and there saying it's impressive how small the JVM overhead is.
Is the JVM inside that 8MB then I guess? That is pretty great.

Originally I thought the alternative was “8MB but supply your own virtual machine.”

A full Quarkus web application with REST uses just 12MB of RAM (with Graalvm). So yes, Java has come a long way
Part of that I imagine is the same problem deno has.

ICU locale data is pretty hefty and there aren't says to trim it down.

Depends. Doesn't seem much for a server but if you want to distribute an executable to your users then 90MB seems huge. IIRC a hello world Go binary is like 2MB.
Yeah but no one distributes hello world.
If the base line is 90MB, then it only goes up from there.
The question is "how fast". If bun executables are always 45x go's that's a problem. If they are always 98mb more than go's, then it's less of a problem as the size grows.
A car that can transport 7 people is not 3.5x heavier than a car that can only have place for two — the initial size is much bigger, additional js code won’t bloat it further that much.
Python's embeddable package for Windows [1] is 16 MB unpacked.

The OpenJDK runtime with the java.base and java.desktop modules is 64 MB. Replacing Swing with SWT (leaving out the java.desktop module) gets it below 50 MB. The full OpenJDK runtime with all modules is around 128 MB. (With Java 17 on Windows.)

[1] https://docs.python.org/3/using/windows.html#windows-embedda...

yeah 90MB for a hello world is big.

Approximately 2,700 times bigger than it could be.

I got 4Gigs on my phone and 4x that on my laptop. I don't care about 90megs.

What's the point in looking at size ? I can see two:

- want to email the exe, and you have limits on mail size - want to be ecofriendly, in that case stop watching netflix for 2 hours and you'll have your megs

The demo of Quake II, contained 3 fully playable levels, and is 11 MB.
I see it differently, what if I want to have more than 40 apps on my phone?

Although, to be fair it seems likely the executable size will shrink with time.

I have 13043 .exe files on this computer, at 90MB a piece that would be 1TB.
If most executables on your machine used Bun, then it would be a bundled shared library on your system eliminating the bulk of the 90mb executable size. Just like the shared libraries the 13043 .exe files on your computer are currently linking to.
Sure but then we're loosing the simplicity advantage of having only a single executable right?
I'm curious why that would be so big. Even my python3.10 binary + /usr/lib/python3.10 is only 30MB
It bundles an entire JS engine with it. I think JSC in Bun's case. V8 for Deno and Node.
The engine is not the largest part of it. just-js, which is pretty close to barebones V8, sits at ~20MB. JSC is supposed to be about 4MB, Hermes is 2-3MB. The largest parts I think are ICU and the built-in libraries.
yes. author of just-js here. a minimal build of a v8 based runtime weighs in around 23-25 MB on modern linux using latest v8. this gets bigger all the time, due to new functionality being added to JS/V8 and no easy way to decide what parts of JS to include. when i started working on just-js ~3.5 years ago i'm pretty sure it was only 15MB or so - can verify when i have time.
i just tried recompiling v0.0.2 (https://github.com/just-js/just/releases/tag/0.0.2) of just-js and comparing it to current. for the completely static build on ubuntu 22.04 i see following:

0.0.2 (v8 v8.4.371.18) - file size: 15.2 MB, startup RSS: 8.4 MB current (v8 v10.6.194.9) - file size: 19.5 MB, startup RSS: 12.3 MB

so, that's roughly 30% binary size increase and 50% greater startup memory usage in 2.5 years. =/

IIRC the latest Linux Node.js binary is over 70MB.

Though Bun doesn't use Node.js I believe, there's a reference

(At least it's not JVM Hotspot...)

A Java Runtime for Hello world is just 32 MB: https://adoptium.net/blog/2021/10/jlink-to-produce-own-runti...
A full Java runtime is 95MB. (Smaller than the JDK size of 312MB.)

> The jlinked runtime using the above command is about 95Mb.

Simple programs -- like Hello World -- can indeed exclude certain parts of the JRE using jdeps, for a smaller size.

You can actually get it smaller if you compile your own JDK and look at the compressed size. I got it down to 7mb at one point.
The storage part is not a big deal, it does not look nice but this is simply a bit disgusting.

The worrying part is that this is mostly code, not dead junk simply occupying space, this is part of the code path, filling the caches, or should I say trashing the caches…

Well, I do appreciate the transparency to be honest.
For most use cases, that doesn’t matter.
That small!?
Perhaps it's specific to the applications i've built, but pkg has _ALWAYS_ given me issues. The ideal is the packaged code Just Works as if you ran it with node, and in my experience pkg does not deliver in that regard.

Glad others are finding value there, but i wish it was more of a drop-in replacement for `node <script.js>`. Feels similar to `ts-node` (always having issues) and `tsx` (just works).

Produces smaller binaries than bun too-- one of my applications, packaged by pkg for Windows, is about 70 MB (and actually does things; this isn't a Hello World). And it compresses well (must be including library sources in the binary as plain text); in a ZIP it's about 25 MB.

Still not small, but I'm not sure what Bun's doing to come out 20 MB larger than an app with actual functionality and dependencies (this one has express, sqlite, passport, bcrypt, and a bunch of others-- essentially everything you'd need for a web service).

Does anyone know of a good comparison of these bundling methods?
Experimental, so probably at least 5 years before it's considered "stable"
As if bun isn't experimental
Node moves pretty slow these days, I wouldn't be surprised if Bun's version gets stabilized before Node's
Which is probably a good thing. Node is starting to get firmly in the boring technology camp these days.
Has it ever moved fast? It's been at a steady pace for a long time.
Yeah, I used Deno to build a simple URL switch case utility in late 2021 to handle sending different URLs to different browsers, and that was ~56MB compiled at the time. I don't know if it's changed in the base size since then, but if bun is resulting in 90MB binaries (as reported here), then Deno may yield a significant reduction in size (if it hasn't gotten much worse in that time).
I'm sure it'll get better (I'm sure both will); there's probably a lot of potential optimization to be done, dropping unused parts of the standard library and platform APIs and such (the way Deno loads most of the standard library as remote modules might actually be helping it here at the beginning), and Jarred is a stickler for hyper-optimization
Interestingly, I'm sure a lot of people that want to compile to a binary would love an option that had trade-offs for the size even if it greatly hurt performance. For example, my case was ~100 lines of code not including the parseArgs and parseIni modules I used, and is meant to be run with an argument and then it exits with an action. If I could have chosen a dead simple and dumb JS interpreter without JIT but that was < 10MB, I would have.

It might even have resulted in a faster runtime as well, since it wouldn't need to load nearly as much of the binary into memory, and also wouldn't need to initialize as many special purpose data structures to deal with advanced performance cases.

It's why I dove into Go.

This definitely took me from the 'eh, kinda cool project' to 'I cant wait to try this out immediately' camp.

The binaries are pretty huge, hoping they can bring that down in time.

> The binaries are pretty huge, hoping they can bring that down in time.

I'm surprised the numbers are as high as they are and hope they can reduce them... but they'll never get down to the kind of numbers Go and Rust get to because Bun depends on JavaScriptCore, which isn't small, and unless they're doing some truly insane optimizations they're not going to be able to reduce its size.

FWIW QuickJS is a tiny JS runtime by Fabrice Ballard... that also supports creation of standalone executables: https://bellard.org/quickjs/quickjs.html#Executable-generati... though its runtime speed is considerably worse than JSC or V8.

For 90% of things this will be used for, it's more than enough so it might be a good default unless you enable a `-Ojit` flag when building more complex applications where the JIT will be a benefit. In fact, startup times might even be faster.

The challenge of course is supporting two JS runtimes within the same codebase.

Yeah I suspect the weird little differences between runtimes (e.g. what language features they do and do not support) would lead you down a path of a thousand cuts.

It still feels like a graceful idea, though.

> but they'll never get down to the kind of numbers Go and Rust

Can we please not put the two next to each other? There is absolutely nothing similar between the two.. why don’t mention go and haskell, or go and D instead?

JSC is ridiculously fast, this is what makes bun great
It really depends on what you're doing. If 95% of your code is file or network I/O then it's really not going to make the slightest difference whether you're running JSC, V8 or QuickJS. If your code is enormously complex and benefits from JIT then yes, you're going to really feel that downgrade.
The difference in real world performance between web servers written in things like Python or Ruby to those written in Go, C# or even lower-level languages like Rust would indicate that's an over-estimation of how much IO dominates total runtime, even if it is by far the slowest part.
Why is JavascriptCore so big? What's going on internally to end up with binaries that big?
You've been able to do this with Deno for a long time (and Node too, as of recently). The downside is it bundles all of V8 so a "hello world" binary ends up being at least 70mb.
I've been desensitized by my world of 500mb docker containers.
Yeah, this is honestly one of the things that turns me off of containers in general.

Like, the whole point was to effectively use linux kernel namespaces with cgroups in an intelligent way to give VM-like isolation, but non-emulated performance - and supposedly not having to deal with image size bloat from the OS like you get in VMs.

What we got was an unholy mashup of difficult to debug, bloated images and ridiculously complex deployment and maintenance mechanisms like kubernetes.

I just do old school /home/app_name deployments with systemd unit files, and user-level permissions.

Oh, and it's webscale[1].

[1] https://www.youtube.com/watch?v=b2F-DItXtZs

It doesn't HAVE to be that bulky or complex. You have a lighter space like Dokku or just direct scripted deployments pretty easily. As to the size, you can use debian-slim or alpine as a base for smaller options. There's also bare containers, or close to bare for some languages and platforms as well (go in particular).
What's even "lighter" is a single binary sitting in /home/app running under "app" user and launched by systemd unit file with auto restart.

Look, I totally get the unholy hell that's (for example) python dependency management, and containers are a great solve for that.

Sometimes you don't have a choice of technology, so I get it.

What I don't understand is folks that use containers for stuff like go binaries. Or nodejs. I mean, it's just an "npm install". Or now bun with it's fancy new build option, you don't even need that.

I honestly don't get the point of containers with languages that have good dependency management, unless you're in a big matrix organization or something.

Or, as one HN user put it years ago, "containers are static compilation for millennials".

I snorted beer out of my nose the first time I read that.

Docker containers are actually smaller if they share layers with other containers in the system. A ton of containers based on the same image reaps many deduplication benefits.
Yeah, I notice many/most images are based on a recent Debian base if they aren't on Alpine or closer to bare images. I don't consider even Alpine as a base too bad for a lot of apps.
Have you tried using alpine based images instead of debian/ubuntu/others? I know it's not always possible especially because of musl but for most things it works fine and is tiny.
If everyone is on the same base image, then you aren't really dealing with 500mb images, but much smaller layers on top.
It becomes a political issue at this point w/ battling the ops team. I have more important battles.
If it's a battle by all means avoid it. But it's weird that your ops team would care about the image type. The whole point of containers is that they don't need to care.
Yeah that's fair, if it works it works.

Depending on your setup it doesn't really matter anyway.

how about ubuntu chisel
Why even include Alpine? Distroless is the way to go.
`-trimpath -s -w` makes binary size smaller.

`xz -z -9e` is good to compress it for distribution.

> Part of Go's popularity is due to how easy it is to produce self-contained executables.

Rust too... :D

Nah, rust still depends on libc at runtime which is a pain. Go doesn't have this problem afaik as it has its own stdlib and runtime.
You can statically link libc in Rust too; at least, if teh interwebz is correct (not a Rust expert); you just need some extra flags.

This is actually not that different from Go in practice; many Go binaries are linked to libc: it happens as soon as you or a dependency imports the very commonly used "net" package unless you specifically set CGO_ENABLED=0, -tags=netgo, or use -extldflags=-static.

Statically linking libC is problematic in various ways. I really appreciate that Zig has its own runtime that is designed specifically for this use case.
> Statically linking libC is problematic in various ways.

Are we just talking about standard rop gadget vulnerabilities, or is there something else that's a problem with it?

glibc, Linux's traditional libc, can dynamically load hostname resolution policies, but that only works if the executable has access to dynamic loading, i.e. if it's not a static executable.

Dynamically loading hostname resolution policies doesn't happen often, but when it does happen it's a right pain to diagnose why some tools see the right hostname and other tools don't.

You can, but usually, if you really want a binary with no runtime dependencies, most people will just compile their code against musl libc instead.

The only issue is that some lib sometime do not compile (or at least without some workaround) with musl. Although it often concern one specific platform (looking at you Mac OS).

> Nah, rust still depends on libc at runtime which is a pain.

It does in general, though I don't really think this is a big pain or blocker in the general case, there are very version requirements around libc.

> Go doesn't have this problem afaik as it has its own stdlib and runtime.

That's also true, but it's not really a pure win. Choosing not to use a battletested libc has led to a variety of subtle and difficult to diagnose bugs over the years, e.g. https://marcan.st/2017/12/debugging-an-evil-go-runtime-bug/

I think it's a pure win. Writing your program in 1 language instead of 2 is worth one simple misunderstanding of vDSO.
This is obviously a trade off, it’s not a bug there’re certain things one must overcome “with time”, even if Go starts using libc am pretty sure the Go team will have their own libc which makes no difference unless it deals with the problem Cgo have
Indeed, I built Supabase's edge-runtime and sent the binary to another pc with a earlier Ubuntu version only to discover it wont work

I went on a wild goose chase to build static Rust but deno can't target musl yet and the issue is a few years old

Not always, because some platforms require you to use their libc.
What bothers me is that even when that capability exists, most of the Rust open source programs that I've tried don't distribute binaries, and still ask you to install cargo and compile the program from source.
My experience is the opposite. Nearly every Rust tool I've used offers static binaries for various platforms.
Yeah, because compilers that produce static binaries don't exist since Macro Assemblers and FORTRAN were created. /s
Node supports this too (experimentally) https://nodejs.org/api/single-executable-applications.html
Doesn't Deno already let us do this?
Bun is intended to be a drop in replacement for Node.js, with Node.js compatible APIs. Deno chose to go a different route with the design of the runtime, encouraging more modern web-native paradigms.
Deno changed their opinion recently and will offer Node.js compatibility. Apparently it wasn't such a good idea to not be compatible on purpose.
I actually have mixed feelings on this one... since I think Deno's approach has been generally cleaner, but also recognize the scale of what's in NPM.
Didn't realize that, thank you. I can empathize with Deno's desire to take backend JS in a more web-native direction.
Yes, that it does. Bun is mostly towing Deno with Zig + JSCore (and per some microbenchmarks, faster than both Node and Deno).
Is this like a phar file?
Phar files still need the PHP runtime installed to run them. These files have the JS runtime embedded in them.
golang is the most famous all-in-one-binary language with reasonable size.

zig is by default static linked, it probably has the smallest binary size, smaller than static C.