Node Or Deno, That Is The Question!? π€
August 23rd, 2020 | 13 mins readPublished on dev.to and medium.com
I'm gonna fix the world I broke, and put it back together better than it was before
During the 2018 JS Conf, which took place in Berlin, Ryan Dahl talked about 10 things he regrets about NodeJS. Some time later (on May 13th, 2020 exactly), Deno version 1.0.0 was born, as well as several new features. The previous quote (taken from Mr. Robot's season 3 episode 2), wouldn't have best translated Ryan Dahl's mindset about NodeJS, at that time.
Theory
If you ask yourself... What is NodeJS? Who is Ryan Dahl? What is Deno? This post is made for you! π
NodeJS is a runtime environment for the JavaScript language, based on the Chrome V8 engine. If you are already familiar with this programming language, you must have NodeJS (and NPM) installed on your computer. Historically, the Chrome V8 engine (developed by the Chromium team) was created in 2008, and with it the ability to directly compile JavaScript code into native machine code, before running it. Nowadays, it's embedded in several essential solutions, such as Chrome, MongoDB or NodeJS.
Ryan Dahl is, no more, no less, than the creator of NodeJS. Developed since 2008 with the C ++ language (and based on the Chrome V8 engine), NodeJS will integrate its own package manager (NPM) some time later, and will quickly become an essential part of the JavaScript ecosystem.
NB: I may take a few shortcuts when I explain. Indeed, the JavaScript ecosystem is so vast today, that these few lines / paragraphs aren't enough to fully describe this topic...
Since 2010, JavaScript technologies continue to grow. The proof: it's one of the most used programming languages by developers, with Java and Python. These technologies include frontend frameworks, such as Angular, React or VueJS; but also backend frameworks, including ExpressJS, Polka, Koa, etc... In 2018, when everyone was focused on the concept of JAMStack, Ryan Dahl began working on the "successor" of NodeJS, entitled: Deno!
Like NodeJS, Deno is also based on the Chrome V8 engine, but unlike its counterpart, it's developed with the Rust language. In the same way, the asynchronism management differs, since this time again, Deno refers to Tokio for the treatment of events.
NB: Remember, JavaScript is a synchronous language. That is, it performs only one operation at a time (inside what is called, the CallStack). Asynchronous operations, such as XHR calls, or timers, are supported by the environment in which the code is executed (either the browser, or NodeJS / Deno). In general, we talk about Web APIs.
Let's get back to the topic: it's May 13th, 2020, Deno version 1.0.0 is released. Among its many new features, there is above all the native execution of TypeScript code. Unlike NodeJS which "only" supports the CommonJS syntax (or ES Modules through the .mjs
extension), Deno fully supports the Microsoft's typed superset, namely TypeScript.
Second new feature: dependency management. The too heavy relationship with NPM (and package.json
) is one of NodeJS mistakes, according to Ryan Dahl. To handle this, Deno retrieves what it needs directly from the Web. So, just import modules from an URL into the code (rather than referring to the node_modules
). This feature will give rise to the "deps.ts" convention, which (like its counterpart, the package.json
) allows to group all external dependencies into a single file.
// Standard Library
export * as colors from 'https://deno.land/std@0.66.0/fmt/colors.ts';
export { readJson } from 'https://deno.land/std@0.66.0/fs/mod.ts';
export { serve } from 'https://deno.land/std@0.66.0/http/server.ts';
// Third Party Modules
export { genSalt, hash, compare } from 'https://deno.land/x/bcrypt@v0.2.4/mod.ts';
export { makeJwt, setExpiration } from 'https://deno.land/x/djwt@v1.2/create.ts';
export { validateJwt } from 'https://deno.land/x/djwt@v1.2/validate.ts';
export { MongoClient, Database, Collection } from 'https://deno.land/x/mongo@v0.10.0/mod.ts';
Another notable change: Deno forces developers to worry about security when running scripts, and that thanks to / because of Rust. Indeed, this runtime will not allow you to read and / or write a file without being previously authorized. To do this, you must specify permissions when interpreting the code. The same applies to external calls. For example, if you want to build an API that will write into a remote database, you'll need to allow network access. This simply means adding "flags" when using the command line tool: deno run --allow-net main.ts
. Nowadays, NodeJS doesn't care about this dimension, which is worth some criticism...
About the cost of implementing Deno, as for NodeJS, everything has been thought out. Whether you're on Linux, Windows or Mac OS; whether it's with Curl, PowerShell or HomeBrew; there are many ways to install the command line tool. This last one is also very practical, since it offers a REPL mode, the possibility to lint and / or format the code, as well as to update Deno, quite simply.
Deno's features are numerous! I could also mention its ability to compile the WebAssembly natively, but not having tested it yet, I invite you to take a look at the official documentation.
In Practice...
Enough theory, it's time for practice. It seems that Deno is more efficient than NodeJS (since coded in Rust), let's see if it's really true... Here, I chose to compare this two JavaScript runtimes with three use cases:
- Running a simple script
- Running a script with file system interactions
- Running a script with network access
NB: NodeJS and Deno versions used are 14.8.0 and 1.3.0 respectively.
#1 - Fibonacci
function iterativeFibonacci(x) {
let arr = [0, 1];
for (let i = 2; i < x + 1; i++) {
arr = [...arr, arr[i - 2] + arr[i - 1]];
}
return arr[x];
}
function recursiveFibonacci(x) {
if (x < 2) {
return x;
}
return recursiveFibonacci(x - 1) + recursiveFibonacci(x - 2);
}
function showTime(func) {
let start, end;
start = new Date();
func();
end = new Date();
console.log(`${end.getTime() - start.getTime()}ms`);
}
showTime(() => {
// iterativeFibonacci(1000);
recursiveFibonacci(10);
});
You'll have recognized it, this first script allows to recover the n-th number of the Fibonacci sequence. I deliberately performed two functions, an iterative (for a linear course) and a recursive (for a tree course), to reveal if there is a difference in the treatment of these functions, between NodeJS and Deno. By adding a time wrapper (here showTime()
), I get the following results:
x | Node | Deno |
---|---|---|
1000 | 3ms | 3ms |
2000 | 6ms | 8ms |
3000 | 12ms | 14ms |
4000 | 24ms | 27ms |
5000 | 36ms | 39ms |
x | Node | Deno |
---|---|---|
10 | < 1ms | 1ms |
20 | 2ms | 3ms |
30 | 9ms | 13ms |
40 | 1050ms | 1320ms |
50 | 141.680s | 183.450s |
We quickly notice that the linear course (iterative) is drastically more efficient than the tree course (recursive). Even more interesting, the figures are regular! Regardless of the environment, the behaviors are similar:
- Linear execution time with
iterativeFibonacci
- Exponential execution time with
recursiveFibonacci
Unfortunately, the statistics speak for themselves. We're forced to note that Deno is a little behind NodeJS. Recursively, this last one recovers the 5000th occurrence of the Fibonacci sequence in 2 minutes and 20 seconds, while Deno needs about 40 additional seconds for this same operation. Despite this slight delay, I noticed during my tests, that the CallStack was filling faster with NodeJS (a difference of about 150 to 200 operations), for the same allocation of resources.
Interesting Fact:
Speaking of "tests", I take the opportunity to point out that Deno comes with an integrated unit test API. So, it's very easy to quickly test the code, where with NodeJS, I'd have needed NPM to recover Karma / Mocha (or better Jest), to launch my unit tests. Here is a concrete example, with Fibonacci functions:
import { assertEquals } from 'https://deno.land/std/testing/asserts.ts';
import { iterativeFibonacci, recursiveFibonacci } from './fibonacci.ts';
Deno.test('iterativeFibonacci', () => {
assertEquals(iterativeFibonacci(10), 55);
});
Deno.test('recursiveFibonacci', () => {
assertEquals(recursiveFibonacci(10), 55);
});
#2 - Files Renamer
Now let's move on to a more practical use case, with a massive file renaming script.
const fsPromises = require('fs').promises;
const { constants } = require('fs');
async function filesRenamer(dirPath = '.', prefix = 'renamed_file') {
let i = 0;
try {
const allFiles = await fsPromises.readdir(dirPath);
for (const fileName of allFiles) {
const filePath = `${dirPath}/${fileName}`;
try {
const metaData = await fsPromises.stat(filePath);
if (metaData.isDirectory()) {
continue;
}
const fileExt = fileName.split('.').pop();
const newFileName = `${prefix}_${i + 1}.${fileExt}`;
try {
await fsPromises.access(`${dirPath}/${newFileName}`, constants.F_OK);
} catch {
try {
await fsPromises.rename(filePath, `${dirPath}/${newFileName}`);
i++;
} catch (e) {
console.log(e);
}
}
} catch (e) {
console.log(e);
}
}
} catch (e) {
console.log(e);
}
return i;
}
async function showTime(callback) {
let start, end;
start = new Date();
await callback();
end = new Date();
console.log(`${end.getTime() - start.getTime()}ms`);
}
showTime(async () => {
await filesRenamer(process.argv[2], process.argv[3]);
});
async function filesRenamer(dirPath = '.', prefix = 'renamed_file') {
let i = 0;
try {
for await (const dirEntry of Deno.readDir(dirPath)) {
const filePath = `${dirPath}/${dirEntry.name}`;
if (dirEntry.isDirectory) {
continue;
}
const fileExt = dirEntry.name.split('.').pop();
const newFileName = `${prefix}_${i + 1}.${fileExt}`;
try {
await Deno.stat(`${dirPath}/${newFileName}`);
} catch {
try {
await Deno.rename(filePath, `${dirPath}/${newFileName}`);
i++;
} catch (e) {
console.log(e);
}
}
}
} catch (e) {
console.log(e);
}
return i;
}
async function showTime(callback: Function) {
let start, end: Date;
start = new Date();
await callback();
end = new Date();
console.log(`${end.getTime() - start.getTime()}ms`);
}
showTime(async () => {
await filesRenamer(Deno.args[0], Deno.args[1]);
});
You'll have noticed, I switched to TypeScript in this second script. Moreover, if you try to run it, you'll very quickly surprised... From now on, security comes into play! Indeed, when we want to interact with the files (read or write), you'll have to allow Deno to do so, using this following command: deno run --allow-read --allow-write filesRenamer.ts
. Pretty simple, right!? π Just think about it...
What is interesting here (performance excluded) are the differences and similarities that exist between Deno's API and that of NodeJS. Even if scripts are built the same way (launching with arguments, reading the directory, reading the file, writing the file), we see that we save some lines of code with Deno. By focusing on the readDir()
functions, we notice that they don't return the same data structure. One returns only file names contained in the browsed directory, while the other returns an object list, which include the file name, but especially the file type. Therefore, this avoids calling the stat()
function to find out if it's a directory (or not), since the data is directly accessible.
I think that Ryan Dahl was able to take advantage of NodeJS good and bad things, and filled the gap with Deno. The most concrete example of this hypothesis is the native use of promises rather than callback functions usage. Furthermore, Deno was able to keep the synchronous and asynchronous versions for some functions: chmod
/ chmodSync
, mkdir
/ mkdirSync
, remove
/ removeSync
, etc... Which is a pretty good approach if you want to satisfy a large audience.
NB: Version 10 of NodeJS marks the arrival of the "fs" module promises. Before that, it was necessary to "promisify" all the functions with the "util" module of NodeJS.
Files | Node | Deno |
---|---|---|
1000 | 220ms | 455ms |
2000 | 425ms | 885ms |
3000 | 625ms | 1340ms |
4000 | 980ms | 2050ms |
5000 | 1025ms | 2255ms |
In terms of performance, again, the above data corroborates the execution times obtained on Fibonacci functions. NodeJS remains faster than Deno at present. According to this test, that last one is also at least 2 times slower to execute JavaScript / TypeScript code than its counterpart.
#3 - Web Server
The last thing I want to highlight is the implementation of an HTTP server. In these last two scripts, whether for NodeJS or Deno, setting up a Web server is very simple (as the JavaScript philosophy suggests). Both use their "http" module: NodeJS imports it from node_modules
, while Deno retrieves it from its standard libraries.
NB: Retrieving modules from URLs doesn't mean that the Web is constantly solicited. On the first call, Deno caches the module version specified during the import for future uses.
About their response delay, I noticed that they take 2ms to respond to the /whoami
request in GET. Obviously, the example below is trivial and if we want to implement a powerful backend service, we will immediately look for a suitable framework that offers more features. However, these two pieces of code represent the basis of some Web frameworks (especially ExpressJS for NodeJS, or Alosaur for Deno).
const http = require('http');
http
.createServer((req, res) => {
if (req.url === '/whoami') {
res.write("I'm Node!");
res.end();
} else {
res.write('Hello World!');
res.end();
}
})
.listen(8080);
console.log('http://localhost:8080');
webServer.js
import { serve } from 'https://deno.land/std/http/server.ts';
const server = serve({ port: 8080 });
console.log('http://localhost:8080');
for await (const req of server) {
if (req.url === '/whoami') {
req.respond({ body: "I'm Deno!" });
} else {
req.respond({ body: 'Hello World!' });
}
}
webServer.ts
Another Interesting Fact:
Deno implements most of Web APIs. Which means, functions such as setTimeout
, clearTimeout
, setInterval
, clearInterval
are accessible, but also fetch
! So, if you want to get a resource from a URL, it's natively possible without having to use Axios (although it already exists as a third-party library), or any other similar library. Since a demo is better than words, here is what I suggest: deno run --allow-net getArticles.ts dmnchzl
interface Article {
title: string;
url: string;
}
const getArticles = async (username: string): Promise<Article[]> => {
const response = await fetch(`https://dev.to/api/articles?username=${username}`);
const data = await response.json();
return data.map(({ title, url }: Article) => ({ title, url }));
};
(async () => {
const articles = await getArticles(Deno.args[0]);
console.log(articles);
})();
Against all odds, these two runtime environments for the JavaScript language aren't so different from each other. What hit me in first place with Deno, is the use of dependencies through import that refer directly to the Web. Doing without NPM (and package.json
) is quite confusing, but it's done quickly thanks to the "deps.ts" convention.
Then, the native use of TypeScript is highly appreciated. I insist on the word "native", because with NodeJS it would have been necessary to configure its environment and transpile the code to finally run it. Of course, these tasks are usually supported by a bundler (Webpack / RollupJS), but nevertheless, it's an additional layer that could be removed.
Finally, the concept of permissions immediately seduced me. Indeed, the fact of authorizing (or not) reading, writing, network access, etc... Allows you to be fully in control of the code that you are launching. Any security risks are managed in this way, where NodeJS is currently unable to protect itself...
NB: I'm happy to have to specify reading and writing (distinctly) when working on the file system with an absolute path. A mistake can happen very quickly... Of course, nobody does that. π
As I write these few lines / paragraphs, Deno is on the rise! Compared to NodeJS, it's more secure and lighter. Although it can't (yet) match this last one in terms of execution speed, it represents a strong (and single) competitor as a JavaScript environment.
By its mode of operation, as well as its many features, Ryan Dahl has clearly succeeded in filling the gap of his previous creation by developing this new technology. Today, Deno's is part of a modern Web context (especially with regard to dependency calls). The support of TypeScript, "fix" the weakly typed appearance of JavaScript, and so, makes Deno a complete solution. Moreover, the presence of Rust inside its code promises many things in terms of performance.
The community is strong! So much so that we see more and more third-party libraries appear every day, I want to talk about MongoDB, Prettier, GraphQL, Moment, etc... Some NPM must-haves are already ready for Deno. Similarly, if you want to play with authentication / encryption within your APIs; BCrypt, JWT and OAuth2 (to name a few) also respond to the call! By the way, I want to point out that there are a multitude of backend frameworks with Deno, the choice is yours (but I advise you to take a look at Alosaur).
The Final Word
For now, I won't give up on NodeJS. This is a mature solution in the Web ecosystem, which is beginning to spread into the business world. In France, small / medium-sized companies have already opted for this solution, and large companies are putting more into it (instead of Spring / Django). However, I'm very excited about Deno. Like GraphQL with REST, I currently consider it as an alternative, but I think it will change manners. The security appearance should encourage professionals to migrate some of their applications to the JavaScript environment. Although the standard dependencies of Deno are stable, they aren't (for the most part) yet available in "final" version / 1.0.0, but when it is, I think we should see a major change / a migration inside the developer community... Will they be tempted by the dark side!? π