Introduction: A Veteran's Take on Node.js vs. Phoenix
Hello everybody! This is Alchemist Camp where usually we learn Elixir and Phoenix by building things, but not this time because this video is a question answer. This question actually comes from numerous different listeners, and the question is, since I've used Node in the past and I currently use Elixir and Phoenix, what do I think about how the two compare?
Actually, I like this question a bit more than the earlier ones about what I thought about Rails, and that's because my Rails experience has been very positive but limited to shorter-term contracts and personal hobby projects. Whereas with Node, that's actually probably what I've spent more time with than all other backends combined. And the entire three years I was in the San Francisco area working for startups there, I was working with Node on the backend and some kind of full-blown JavaScript framework on the front-end, whether if it's Backbone or React. Actually, it was always Backbone or React; I kind of missed the whole Angular thing.
So I have some experience to talk about this. I'm not gonna present myself as any kind of Node expert. I wasn't making libraries for it, I wasn't working with it directly at a low level. It's pretty much always using Express or something similar on top of Node. So this is just my experience working with NodeJS in the way that it's used in a lot of companies.
To be honest, if you look at Alchemist Camp and go to the welcome episode, you'll see a chart that I've made that compares various server backends on a few metrics that I think are important. That's how fast it is for one developer to get things done, or a small team, and how performant it is for a web server, and also how performing it is for number crunching, how easy it is to debug your apps, and whether if you've got any kind of concurrency story. And then also whether if there's fault tolerance. Turns out fault tolerance is pretty rare. You actually only see that in Elixir of the languages that I did. I think it's a pretty good chart and it's worth checking out, but a chart can only tell you so much. So let's dig in a little bit further.
I would say server performance is probably the main reason people started hearing about Elixir and Phoenix to begin with. There's a pretty well-known benchmark that was done a couple years ago on GitHub by someone called M Roth, and Phoenix did very, very well in it. So if we look at this benchmark and just remove all of the things that are not full MVC frameworks, we'll see that remaining are Phoenix, which is based on Elixir; Martini, which is based on Go; and Rails, which is based on Ruby. And in these benchmarks, Phoenix came out not only much faster than Rails but also significantly faster than Martini, which is amazing because Go compiles to a binary executable whereas Elixir just compiles into a bytecode, and that bytecode still gets interpreted by the Erlang virtual machine.
Concurrency and Scale: Beyond Asynchronous
The other thing that's interesting is in that same set of benchmarks was Express, which is not an MVC framework. It's much, much lower level than something like Rails or Martini or Phoenix. And Express was able to handle less than a third of the requests per second that the Phoenix server could. This is a pretty stark result, considering that a lot of people moved at least parts of their apps from Rails to Node and Express back in 2011-2012, partially for these performance reasons to cut down on their server costs. And the other big reason was just concurrency.
Node doesn't really have concurrency, but it's asynchronous. So as long as nothing ever blocks, then it's pretty good—not amazing, but pretty good at dealing with a lot of evented traffic. And you may have noticed a lot of the early Node tutorials were talking about making chat rooms. So then Phoenix showed up, being much more performant, and it actually is concurrent. And the concurrency model is extremely simple to use. It's very much like the Scala actor model, except all that is built into your VM.
And the maker of the Phoenix framework put out what is now a pretty famous video of handling 2 million WebSocket connections on a single server with Phoenix. He actually set up a chatroom and then sent a chat to two million connected clients, which is ridiculous. It was a fairly beefy server, but two million people in one chat room on one server is something that I had never heard of before. I think the same is true for many others. I think it's fair to say that the very area where Node most impressed people is an area where Elixir and Phoenix is far more impressive. But that's just one thing. So this is all performance-related stuff, and yes, there are very compelling reasons to care about performance if you have a large-scale application and you're spending a lot of money on servers. But I actually don't care about this too much.
Productivity Over Premature Optimization
I think most people care way too much about scaling. And most... not just solo developers, but most funded startups that I have seen have actually done all kinds of things that they didn't need to do in preparation for scaling when they should have been trying to get users. So I think scaling is a wonderful problem to have. But in the big scheme of things, you know, you would go back like 12 years, look at Twitter, which grew way faster than even most unicorn startups. And they were running Ruby on Rails, and this was Ruby on Rails of 12 years ago, which was much less performant than Ruby on Rails is today. And they were also running on hardware from the mid-2000s, which was much lower than hardware from today. And they had some problems, but ultimately all those users and all that traffic that were causing those problems, at least with that setup, even the resources they needed to invest in engineering, and they did fine. And many other startups that used inefficient tech stacks, we can call them, also did fine. That's because those tech stacks were efficient for their productivity as opposed to just using a low number of servers.
Quantifying Productivity: A Language Comparison Study
But what I think should be more exciting is productivity. So there's a really interesting paper I read. It's called "Software Economics and Function Point Metrics: 30 Years of IFPUG Progress" by Capers Jones. And it's a long paper, but if you go to page 49 of it, there is a specific comparison of economic productivity of different programming languages. They found the very least productive language to be machine language, no surprise there, taking 120 hours of work to get one unit of economic productivity.
Basic assembly was about twice as productive as that. C, about two and a half times more productive still. Then JavaScript was a bit more productive than C. A bit further down the list, found C++, Go, Java, PHP, and Python. And a bit more productive, F#, Ruby, Simula. Erlang beat that by a bit. Then Elixir and Haskell came out even more productive than that. These kinds of rankings are a bit fraught to say the least, but for the most part, I think their methodology was good. They were looking at what changed in terms of productivity when moving projects from one language to another. And they also found that, say if A was more productive than B, and B was more productive than C, they would actually see a greater improvement moving from A to C than from A to B or from B to C. So at least mathematically, it all makes sense.
There were a couple of surprises, like Perl did better than I thought it would, and Excel beat pretty much everything, which I guess is fair enough. If you're in a situation where you could use Excel, you probably should use Excel instead of another programming language... well, 90% of the time. There are some people I've seen make Excel do crazy things.
Framework Philosophy: Integrated vs. Piecemeal
So that's productivity of the language itself. But what about the advantage of having a framework? So before, I was talking about a benchmark with various numbers for Phoenix versus Martini versus Rails. And I would argue there actually is a big difference between using something like Rails versus just using Rack, or using Express. So if I want to make an API endpoint with Phoenix or with Rails, I can actually generate the schema for whatever table, and I can generate a route, and I can generate a controller. I can generate a database migration. Migrations can be, you know, replayed or taken backwards and forwards. You don't get any of that with Express.
With Express, you're going to be, you know, manually setting up your router, or more likely, you'll be pulling in 15 different dependencies that don't quite fit together nicely. I would say probably the closest thing that you can find in the JavaScript world to the productivity of Rails or Phoenix would be either Meteor, which gets kind of weird in a lot of cases and it's really its own universe, or more likely Sails. It's an attempt at cloning Rails, and it's actually better than Rails at certain things. It's been much better for dealing with WebSockets for much longer, whereas Rails just recently got Action Cable in version 5. But I think on the whole, it's actually not as easy to work with, and that's because it doesn't have a unified community. And most people on Node just aren't using Sails. Mostly, they're, well, they're using a hodgepodge of different libraries that they've chosen at pretty much every single level. So there's no golden path to follow, whereas there really is with Rails and there really is with Phoenix. And more important probably even than that is the debugging experience.
The Debugging Experience: Node's Achilles' Heel
Debugging is a horrible experience in Node. It's way worse than any other backend I've worked with. This isn't an argument for static typing or anything like that. With Elixir, you do get a lot of the benefits of static typing just from pattern matching and structs. In a lot of ways, the structs can kind of act like your types, but it's not even about that. You don't really get reasonable stack traces in JavaScript. A lot of that's due to anonymous functions, promises, and a lot of times the error you do get will be something like object object is not a function. You've just never seen anything like that in Rails or Elixir.
I would actually say that Rails is harder to debug than most frameworks I've seen, due to various magic, but it still gets stack traces. It's still got Pry. Pry is an interactive debugger that's just built in. Elixir has that as well. And because it's a functional language where variables are all immutable, it is incredibly easy not only to debug but to test things. With any function, if you give it the same input, it will generate the same output. There's nothing going on with a prototype chain or pseudo-classical inheritance or the bolted-on inheritance in ES6. You've got the same input, you'll get the same output.
Code Clarity in Phoenix: Pattern Matching and Macros
And that's super liberating. If we just take a quick look at what that looks like in practice, every single function I've got in this Phoenix controller, there's a connection coming in and there are parameters coming in. If they get the same connection and the same parameters, the same page will be generated, same thing will be done. And inside these parameters, we can just pattern match on what's there. Similarly, we can pattern match, you know, what to do in this visible_articles function, which will get all the visible articles. If the admin user is inside the assigns and is set to true, then we'll list all the articles. Otherwise, this pattern match here will just get everything else. Otherwise, we'll just list the published articles.
And pretty much anywhere in the app that we've got this kind of pattern matching, this is essentially a guarantee. We're inside this function at all, that means that this function had matched and we have an ID, we have an article, the article has topics, and so on.
Elixir has full-blown macros, but any module that is using something with macros has to have this use call here at the top. So if I were to go through this module and see something that didn't make sense to me, or see some sort of syntax I didn't understand, I could just look at the top and see exactly what macros are affecting this module. It's both a very high level of abstraction and it's very explicit. The result is it's both faster to write things the first time and faster to debug than using Node.
Production Superpowers: Remote Shell and Fault Tolerance
I'm just gonna pull up my terminal here and show you another thing. So I can SSH into the server, and after I SSH into the server, it's possible to open a remote console into the application that's currently running from a compiled release in production, or more likely in testing. So I can do something like this. campsite is the name of my app, and it's deployed to this releases directory. remote_console into it, and from here I can query the database, I can run functions that are in the application, basically do anything that I would normally do from an interactive shell. And that even includes getting runtime information. use Runtime.Info, and now we can see exactly how much memory is being used on the production server, how much is from what, like this is my ETS cache. You can list all the applications, which is pretty crazy. These are all OTP applications. So each of these things is running as a part of my app.
It's a pretty amazing tool, to be honest. I didn't really put this in the chart, but the stability is amazing. I would say Elixir or Erlang are probably the only runtimes out there that are even more stable than the JVM. Like, it just does not crash. And if any of those sub-applications that you saw were to crash, well, there's this whole fault tolerance thing where things that crash will just get restarted. It's kind of like a really nice Kubernetes orchestration pattern built into your virtual machine.
Final Verdict: An Overwhelming Win for Phoenix
So that's probably about the kind of summary you were expecting, given the fact that this whole channel is building stuff on Elixir, but I struggle to think of any time I would use Node. In fact, I kind of struggle to think of anytime when I think Node is the right option. People talk about productivity, which I think is... I think there are a lot of options where you'll get things built faster than with Node. Or people talk about code sharing between the backend and the front-end. Again, I've never seen that translate into a productivity gain.
I mean, compared to Phoenix, Node is less productive, it's less performant. The one tiny edge it might have would be numerical performance. It's a heavily optimized runtime. But there's still no reason why I would use Node for that. Like if there's some serious number crunching to be done, it's probably going to be dishing off to a native library. In fact, that's what Node apps do as well.
I would say the productivity differential between Elixir and Node actually grows as your app gets bigger just because of the dramatic differences in debugging and testability. Maybe if my main criteria were to choose a popular backend where it would be easy to hire people very cheaply and outsource the development work, although even in that case, I would probably go with Java instead of Node, because Java is easier for larger teams to work with and there are many, many people all over the world who know it very well. Actually, yeah, I would not build a back-end in Node in 2018. I think it's a worse choice pretty much across the board.
And while it is hugely popular, it's also hugely fragmented. So even in terms of just taking a bunch of libraries and plug-and-play, it's going to be painful. This will probably be the very least popular video on this channel ever, but so be it. You wanted my opinion on how Node and Phoenix compare. I think it's a very one-sided competition, and you should go with Phoenix every single time. See you next time.