DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 74: Neutralino

Let's continue exploring Electron alternatives. This time, Neutralino.

As we've done the terminal apps so many times already, and the changes are minimal, I won't do separate hello world episode, followed by terminal app episode. I'll just implement terminal app as before. As it won't be all that different from what we had before, I won't be going through everything in too much detail.


I tried to create a new app with npx but it didn't work

$ npm install -g @neutralinojs/neu
$ neu create episode-74-neutralino
Enter fullscreen mode Exit fullscreen mode

After cleaning up some code we don't need, I went on to implement the usual terminal app.


Here's the neutralino.config.json file created by the installer:

  "applicationId": "js.neutralino.sample",
  "port": 0,
  "defaultMode": "window",
  "enableHTTPServer": true,
  "enableNativeAPI": true,
  "url": "/resources/",
  "nativeBlockList": [],
  "globalVariables": {
    "TEST": "Test Value"
  "modes": {
    "window": {
      "title": "episode-74-neutralino",
      "width": 800,
      "height": 500,
      "minWidth": 400,
      "minHeight": 250,
      "fullScreen": false,
      "alwaysOnTop": false,
      "enableInspector": true,
      "borderless": false,
      "maximize": false
    "browser": {},
    "cloud": {}
  "cli": {
    "binaryName": "episode-74-neutralino",
    "resourcesPath": "/resources/",
    "clientLibrary": "/resources/js/neutralino.js",
    "binaryVersion": "2.8.0",
    "clientVersion": "1.5.0"
Enter fullscreen mode Exit fullscreen mode

We specify entry point, window size and so on. This information is in package.json in NW.js, and in index.js in Electron.


The root page has some extra js/neutralino.js stuff, other than that it just loads our CSS and code, and has some placeholders for our app to work with.

<!DOCTYPE html>
    <meta charset="UTF-8">
    <title>Neutralino terminal app</title>
    <link rel="stylesheet" href="styles.css">
    <h1>Neutralino terminal app</h1>
    <div id="terminal">
      <div id="history">

      <div class="input-line">
        <span class="prompt">$</span>
          <input type="text" autofocus />
    <!-- Neutralino.js client. This file is gitignored,
        because `neu update` typically downloads it.
        Avoid copy-pasting it.
    <script src="js/neutralino.js"></script>
    <!-- Your app's source files -->
    <script src="js/main.js"></script>
Enter fullscreen mode Exit fullscreen mode


This is identical to what we had before - simple dark mode terminal app.

body {
  margin: 1em;
  background-color: #333;
  color: #fff;
  font-family: monospace;

header {
  text-align: center;
  font-size: 400%;
  font-family: monospace;

.input-line {
  display: flex;

.input-line > * {
  flex: 1;

.input-line > .prompt {
  flex: 0;
  padding-right: 0.5rem;

.output {
  padding-bottom: 0.5rem;

.input {
  color: #ffa;

.output {
  color: #afa;
  white-space: pre;

form {
  display: flex;

input {
  flex: 1;
  font-family: inherit;
  background-color: #444;
  color: #fff;
  border: none;
Enter fullscreen mode Exit fullscreen mode


Everything was going well so far, except we run into our first problem. We cannot use node modules here, we only have much more limited set of APIs, and if we need anything beyond that? Too bad.

For our use case we only need one command - Neutralino.os.execCommand, but no access to node ecosystem makes it far less useful than Electron or NW.js. And we don't even get any security benefit for this restricted access, as those limited commands are totally sufficient to own the machine.

let form = document.querySelector("form")
let input = document.querySelector("input")
let terminalHistory = document.querySelector("#history")

function createInputLine(command) {
  let inputLine = document.createElement("div")
  inputLine.className = "input-line"

  let promptSpan = document.createElement("span")
  promptSpan.className = "prompt"
  let inputSpan = document.createElement("span")
  inputSpan.className = "input"


  return inputLine

function createTerminalHistoryEntry(command, commandOutput) {
  let inputLine = createInputLine(command)
  let output = document.createElement("div")
  output.className = "output"

form.addEventListener("submit", async (e) => {
  let command = input.value
  let output = (await Neutralino.os.execCommand({command})).output.trim()
  createTerminalHistoryEntry(command, output)
  input.value = ""
Enter fullscreen mode Exit fullscreen mode

All that changed was no require line, form submit being async, and (await Neutralino.os.execCommand({command})).output.trim() replacing previous child_process.execSync(command).toString().trim().


So far it looks like Neutralino is drastically worse than Electron, as you lose access to the whole npm ecosystem, but it would be good enough for simple apps at least?

Hard no.

Unfortunately Neutralino also fails to bundle Chromium, and just uses whatever you have installed on the machine - and for OSX it defaults to Safari, the IE of OSX. So not only you'll suffer from extremely limited APIs, you'll also suffer from all the cross browser incompatibilities.

Should you use Neutralino?

There are zero advantages of Neutralino I can see.

Smaller binaries really don't count - it matters for people accessing websites on shitty phone networks, but if users are downloading your app on a computer, they generally have good network connections, and in any case waiting a few extra seconds for those extra MBs is not a big deal. The difference is less than one TikTok video.

At this point it should be totally clear that you should not use Neutralino. Electron and NW.js both do things a lot better.


Here's the results:

Episode 74 Screenshot

That's it for Neutralino. In the next episode we'll try to check out some other Electron alternatives.

As usual, all the code for the episode is here.

Top comments (0)