Go Over JS for Production Apps
If you’ve followed me or my work, you know I use a lot of JavaScript and have been an active contributor to the OSS community for many years. However; there is one major issue that prevents me from going “all in” on JavaScript for production applications. Put simply, the distribution mechanism for JavaScript is subpar, across the board. It’s a runtime problem. I’m writing this, primarily as a plea to those directing Node.js, Deno, & Bun (and anyone else working on JavaScript runtimes).
Code Signing is Important
For the uninitiated, code-signing is to non-web apps what HTTPS is to web apps. It’s a cryptographic way to identify the developer and integrity of the code. Code signing is accomplished using a TLS/SSL certificate to compute a unique signature against the code of an app. The signature is embedded in the executable. The app signature can then be verified using the root certificate from whoever issued the code signing certificate. On most operating systems, a list of these certificates is stored in the root trust chain, which happens to be where the root certificates for HTTPS servers are stored too.
I long to return to a world where Docker isn’t necessary for the stable/consistent operation of an application. JS runtimes shouldn’t have to exist on a system to use the app. This is a fundamental capability of compiled languages that interpreted languages don’t offer. JavaScript crossed into this world quite some time ago. All of these runtimes support compilation to varying degrees, so it is possible to produce and ship standalone executables. That is a wonderful advancement for the JavaScript community. If you helped build one of these runtimes/features, hats off to you. Unfortunately, it’s only half of what the community needs.
It makes no difference what a runtime can do, how fast it can do it, or how easy it is to make it do what you want when you can’t deploy it. Ultimately, the operating system determines whether an app can be installed/executed. Each operating system handles this differently.
Most JS runtimes cater to a common client-server use case. The dominant platform for this use case is Linux (servers). Linux doesn’t explicitly enforce app verification, which is always the argument I hear against code signing. However; this is based on the narrow assumption the JS runtimes are predominantly used for client-server applications.
I’ve written one enterprise server app/API in the last 5 years. Just one. I used Deno. I create far more applications for desktop users. I’m not alone. Trends like local-first development are gaining momentum, which converge into desktop development. The major difference is, in these cases, Linux is the minority platform (Windows and macOS dominate this landscape).
Distributing apps on Windows is much more complicated than it looks on the surface. Code-signing (love it or hate it) is the best way to smooth deployment to Windows users (not to mention its standard practice). Windows has built-in protections to verify code signatures at varying levels of trust. In enterprise environments, it’s not uncommon for untrusted or loosely trusted apps to be blocked entirely. That’s right, all of your hard work is for nothing because Windows (or a Group Policy) won’t let your untrusted app run at all. That’s a good thing. Untrusted apps should be blocked and developers should be discerning about their levels of trust.
Windows Defender and other antivirus software leverage trust established by code signatures too. These tools use ML to identify malicious apps, meaning an algorithm determines whether it is blocked or not. Code-signing is often used as a heavily weighted metric in these algorithms. In simple terms, code-signing drastically reduces the chance of a false-positive virus detection. Unsigned apps suffer in this regard.
JS runtime makers need to address code-signing to make executables produced by their compilation tools trustworthy. Until this happens, JS developers will be at a disadvantage relative to developers using other programming languages.
But there are ways to trust apps without code signing!
Without code-signing, an app has to generate enough downloads without complaints before Microsoft’s algorithms indicate it is trustworthy. In other words, a certain number of people have to bypass the security mechanism (i.e. “install anyway”) before Microsoft recognizes the app as trustworthy. That’s a hard fail for internal apps that can’t reach 40K installs, or for developers who don’t have permission to bypass Windows trust mechanisms. That is pretty much every developer working in a decent size company.
There are other ways to trust apps via Active Directory Group Policy, but it requires a separate process/team to set that up/maintain it. It’s a needless burden to bypass the already-solved problem of trust baked into the OS.
Trust mechanisms exist on macOS too!
Don’t make the mistake of believing code signing is only relevant to Windows. macOS has a similar trust mechanism. The major difference is macOS requires an Apple-issued code signing certificate (which is part of the Apple Developer Program). Remember, many macOS users working in companies are still part of Active Directory networks, meaning Group Policies are still applicable to them as well.
Personally, I use Go whenever I know I’m going to deploy an app to desktops or in a corporate environment. Go apps can be code-signed. I’d use JS if I could, but this is a deal-breaker for the audience I serve.
Reduce Executable Size
Executable size isn’t a deal-breaker the way code-signing is, but it can still sway a decision to use/invest in a technology. Consider all the flack Electron gets for being large. You can code-sign Electron apps, so your bloated app will at least run on a user’s desktop. Still, developers seek lightweight solutions. Electron is overkill for many apps, especially those that don’t need a GUI (like CLI tools, APIs, HTTP servers, background daemons, etc).
Node executables (SEA) on Windows are 80–90MB for a “Hello World” app. I haven’t compiled with Bun yet, but it appears to be in the 90MB+ range as well. Deno, stripped down, is restricted to a minimum of ~23MB due to the inclusion of the Rust internals required to run any app.
The Go apps I write are typically 8–10MB. Using some well-known tweaks/software, I commonly get it down to 2–3MB, and sometimes even down to 1MB. Tools like Tauri boast app sizes of 600kb, but you can’t really do much with that. If you’re considering a new technology for your app, what is more attractive? A 90MB Hello World app or a 3MB functional app?
Again, this isn’t a deal breaker, but it’s a consideration many developers contend with and take seriously. When I’m building a product or something a team/org will use in an enterprise, size matters. In most cases, I’d still choose Go, even if code signing was available, to keep file sizes down. It would just be a harder decision.
Missed Market Opportunity
I want to use JavaScript to build apps, but I can’t justify it relative to Go. It’s not uncommon to spend more time working around deployment problems than writing the JS apps themselves. If these issues were resolved, using JavaScript would be a no-brainer for so many people like me.
Too many JS runtimes are focused on a narrow use case, missing out on an entire class of development/developer. The JS runtime world needs to pay more attention to this, focusing on local development and deployment. In my humble opinion, this is the next real frontier for JS. It seems like a very manageable challenge relative to many others the very talented folks behind these runtimes have achieved in the past. Outwardly, it just seems like everyone is avoiding it.
Update (July 31, 2024)
Deno will be adding support for code signing! See https://x.com/rough__sea/status/1818659018140795216