In this long read we take a look at Deno, a simple, modern, and secure runtime for JavaScript and TypeScript applications built with the Chromium V8 JavaScript engine and Rust, created to avoid several pain points and regrets with Node.js. This long read is sampled from Alexandre Portela dos Santos’ book Deno Web Development
Deno: A Runtime Ready for the Prime Time?
Deno is a secure runtime for JavaScript and TypeScript from the original developer of Node.js. Since its launch in May 2020 interest in Deno has been growing rapidly. In this introduction, we explain what Deno is, explore its architecture and capabilities, and discuss its present limitations and major use cases.
Background
Ryan Dahl first mentioned Deno in a talk at JSConf EU 2018 entitled 10 Things I Regret About Node.js. Over the past decade or so Node.js has, it hardly needs saying, played a key role in bringing JavaScript to the server for a wide variety of use cases, including web development tooling, web servers, scripts, and many others. Given that Dahl was Node.js’s originator, his words carry considerable weight.
Node’s popularity owes much to the fact that it is asynchronous and single-threaded by design. It lends itself to event-driven programming, having an event loop at its core that makes it a scalable way to develop back-end applications that handle thousands of concurrent requests.
In using an event loop to provide a clean way to deal with concurrency, Node.js contrasts with tools like PHP or Ruby which use the thread-per-request model. The single-threaded environment grants Node.js’s users the simplicity and safety of not caring about thread safety problems. It succeeds in abstracting the event loop and shielding the user from all the issues associated with synchronous tools, requiring little to no knowledge about the event loop itself. Node does this by leveraging callbacks and, more recently, by the use of promises.
Many things have changed since Node.js was created in 2009. Node brought ideas to JavaScript that didn’t exist before prior to Node’s advent. These concepts were later taken by the standards organizations and added to the language differently, making parts of Node incompatible with its mother language.
CommonJS modules are no longer the standard; JavaScript has ES modules now. TypedArrays are now a thing, and finally JavaScript can directly handle binary data. Promises and async/await are the go-to way with asynchronous operations.
These features are available on Node.js, but they have to coexist with the non-standard features created back in 2009 that still need to be maintained. These features, and the large number of users that Node.js has, made it difficult and slow to evolve the system.
To solve some of these problems, and to keep up with the evolution of the JavaScript language, many community projects were created. These projects made it possible to use the latest features of the language but added things like a build system to many Node.js projects, heavily complicating them. In Ryan Dahl’s words, it ‘took away from the fun of dynamic language scripting’.
Over a decade of heavy usage has also made it clear that some of the runtime’s fundamental constructs needed improvement, with the lack for a security sandbox being one of the major issues. Node.js made it possible for JavaScript to access the ‘outside world’ by creating bindings in V8 – the JavaScript engine behind it. Even though these bindings enabled I/O features like reading from the file system, they also broke the purpose of the JavaScript sandbox. This decision made it hard to let the developer control what a Node.js script has access to. In its current state, for instance, there’s nothing to prevent a third-party package in a Node.js script reading all the files the user has access to, among other nefarious actions.
So, a decade on, Ryan Dahl and the team behind Deno perceived once again the lack of a fun and productive scripting environment that could be used for a wide range of tasks. They also felt like the landscape surrounding JavaScript had changed enough that it was worthwhile simplifying things. Deno was the result.
Presenting Deno
Deno is a simple, modern, and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
https://deno.land/
The name alone makes it clear just how much Deno owes to its illustrious ancestor. Building on Node’s conceptual foundations, Deno’s chief attributes include the following:
- Secure by default
- First-class TypeScript support
- A single executable file
- Provides fundamental tools to write applications
- Complete and audited standard-library
- Compatibility with ECMAScript and browser environment
Deno is secure by default, and by design. It leverages the V8 sandbox and provides a strict permission model that enables developers to fine-control what the code has access to.
TypeScript is also first-class supported, meaning developers can choose to use TypeScript without any extra configuration. All the Deno APIs are written in TypeScript and thus have correct and precise types and documentation. The same is true for the standard-library.
Deno ships a single executable with all the basic tools needed to write applications, and it will always be that way. The team makes an effort to keep the executable small (c.15MB), so that it can be used in a variety of situations and environments, ranging from simple scripts to full-fledged applications.
More than just executing code, the Deno binary provides a complete set of developer utilities, namely a linter, a formatter, and a test runner.
Golang’s carefully polished standard library inspired the Deno’s standard library. It is deliberately bigger and more complete as compared with Node. This decision was made to address the enormous dependency trees that used to arise in some Node.js projects. Deno’s core team believes that, by providing a stable and complete standard-library, it can help address this problem. By removing the need to create third-party packages to handle common use cases that the platform supports by default, the need to use numerous third-party packages is reduced.
To maintain compatibility with ES6 and the browser, Deno makes an effort to mimic the browser APIs. Tasks like performing HTTP requests, dealing with URLs, or encoding text, among others, are made by using the same APIs you’d use in the browser. The Deno team has tried to keep these APIs in sync with the browser.
The aim is for Deno to offer the best of three worlds: the prototype-ability and developer experience of JavaScript, the type-safety and security offered by Typescript, and Rust’s performance and simplicity.
Ideally, as Ryan also mentioned in one of his talks, developers start writing JS, migrate to TS, and end up with Rust code. At the moment, is it only possible to run JS and TS; Rust may be introduced in the not-too-distant future.
A web browser for command-line scripts
Over time the Node.js module system has evolved into something that is now overly complex and painful to maintain. It takes into consideration edge cases like importing folders, searching for dependencies, importing relative files, searching for index.js, third party packages, and reading the package.json, among others.
It has also become heavily coupled with NPM, the Node Package Manager, which was initially part of Node itself but which separated in 2014. Having a centralized package manager is not very webby, to use Ryan’s words. The fact that millions of applications depend on a single registry to survive is a liability.
Deno solves this problem by using URLs. It has an approach very similar to a browser for scripts, requiring only an absolute URL to a file to execute or import code. This absolute URL can be local, remote, or HTTP based and includes the file extension.
import { serve } from https://deno.land/[email protected]/http/server.ts
The preceding code happens to be the same code you would write on a browser inside a script tag if you wanted to require an ES Module.
In relation to installation and offline usage, Deno makes it possible for the user not to worry about that by using a local cache. When the program runs, it installs all required dependencies, removing the need for an install step.
Now that we’ve seen what Deno is and what the problems are that it solves, we’re in good shape to go beyond the surface. By knowing what is happening behind the scenes we can get a better understanding of Deno itself.
Architecture and technologies supporting Deno
Architecture-wise, much thought has been put into establishing a clean and performant way of communicating with the underlying OS without leaking details to the JavaScript side. To enable that, Deno uses message-passing to communicate from inside the V8 to the Deno backend. The backend is the component written in Rust that interacts with the event loop and thus with the OS.
Deno is made possible by four pieces of technology:
- V8
- TypeScript
- Tokio (Event Loop)
- Rust
It is the connection of all those four parts that make it possible to provide the developer with a great experience and development speed while keeping code safe and sandboxed. If you aren’t yet familiar with these technologies, they can be summarized like this:
V8 is a JavaScript engine developed by Google. It is written in C++ and runs across all major operating systems. It is also the engine behind Chrome, Node.js, and others.
TypeScript is a superset of JavaScript developed by Microsoft that adds optional static typing to the language and transpiles to JS.
Tokio is an asynchronous runtime for Rust that provides utilities to write network applications of any scale.
Rust is a server-side language designed by Mozilla focused on performance and safety.
Using the fast-growing Rust language to write Deno’s core made it more approachable for developers than Node. Node’s core was written in C++, which is not known for being (how can we put this?) exceptionally easy to deal with. With many pitfalls, and with a less than straightforward developer experience, C++ revealed itself as an obstacle to the evolution of Node.js core.
Deno_core is shipped as a Rust crate (package). This connection with Rust is not a coincidence. Rust provides many features that facilitate this connection with JavaScript, and that add capabilities to Deno itself. Asynchronous operations in Rust typically use Futures that map very well with JavaScript Promises. Rust is also an embeddable language, and that provides direct embedding capabilities to Deno. Rust being one of the first languages used to create a compiler for WebAssembly makes it closer to the browser where JavaScript was born.
Inspiration from POSIX systems
In one of his talks, Ryan states that Deno handles some of its tasks ‘as an Operating System’. The following table shows how some of the standard terms from POSIX/Linux systems map to Deno concepts:
Some of the concepts from the Linux world might be familiar to you. Take, for instance, processes. They represent an instance of a running program that might execute using one or multiple threads. Deno uses WebWorkers to do the same job inside the runtime.
In the second row, we have syscalls. They are a way for programs to perform requests to the kernel. In Deno, those requests do not go directly to the kernel, they go from the Rust core to the underlying operating system, but they work similarly. We’ll have the opportunity to see it in the coming architecture diagram.
Architecture
Deno’s core was initially written in golang, but it later changed to Rust. This decision was made because golang is a garbage-collected language, and it was felt that its combination with V8’s GC could lead to problems in the future.
To understand how the underlying technologies interact with each other to form the Deno core, let’s look at the architecture diagram:
Deno uses message passing to communicate with the Rust backend. As a decision to privilege isolation, Deno never exposes JS object handles to Rust. All communication in and out of V8 uses Uint8Array instances.
For the event loop, Deno uses Tokio, a Rust thread pool. Tokio is responsible for handling I/O work and calling back the Rust backend, making it possible to handle all operations asynchronously. Operations (ops) is the name given to the messages passed back and forth from the Rust to the event loop.
All the asynchronous messages dispatched from Deno’s code into its core (written in Rust) return Promises back to Deno. To be more precise, asynchronous operations in Rust usually return Futures, which Deno maps to JS Promises. Whenever these Futures are resolved, the JavaScript Promises are also resolved.
To enable communication from V8 to the Rust backend, Deno uses rusty_v8, a Rust crate created by the Deno team that provides V8 bindings to Rust.
Deno also includes the TypeScript compiler right inside V8. It uses V8 snapshots as a startup time optimization. A snapshot saves the JavaScript heap at a specific execution time so that it can be restored when needed.
Since it was first presented, Deno has been subject to an iterative, evolutionary process. If you are curious about how much it changed, you can look at one of the initial roadmap documents written back in 2018.
Now we know not only what Deno is, but also what’s happening behind the scenes. This knowledge will help us when running and debugging our applications. Deno’s creators have made many technological and architectural decisions to bring Deno to its present state. These decisions pushed the runtime forward and made sure that Deno excels in certain situations. However, to make it work well for some use cases, trade-offs inevitably had to be made. Those trade-offs resulted in the limitations we’ll examine next.
Deno’s limitations
As with anything else, choosing solutions is a matter of dealing with trade-offs. The ones that better adapt to the projects and applications we’re writing are what we end up using. Currently, Deno has some limitations, some due to its comparative youth, others stemming from design decisions taken. As with most solutions, Deno is not a one-size-fits-all tool. We’ll now consider some of Deno’s current limitations and explore the motivations behind them.
Not as stable as Node
In its current state, Deno can’t be compared to Node.js in terms of stability. Node has more than ten years of development while Deno is only nearing its second year.
Even though most of the core features are already considered stable and correctly versioned, there are still features that are subject to change, and which live under the unstable flag.
Node.js has years of usage under its belt, ensuring that it is battle-tested and that it works in the most diversified environments. That’s a state we’re hopeful Deno will get to, but time and adoption are essential factors.
Better HTTP latency but worse throughput
Deno keeps performance on track from the beginning. However, as seen on the benchmarks page (https://deno.land/benchmarks), there are areas where it is still not at Node’s level.
Its ancestor leverages the direct bindings with C++ on the HTTP server to amplify this performance score. As Deno resisted adding native HTTP bindings and builds on top of native TCP sockets, it still suffers from a performance penalty. This decision is something that the team plans to tackle after optimizing the TCP sockets communication.
Deno HTTP server handles about 25k requests per second with a max latency of 1.3 milliseconds, while node handles 34k requests but has a latency that varies between 2 and 300 milliseconds.
We can’t say 25k requests per second is not enough, especially since we’re using JavaScript. If your app/website needs more than that then probably JavaScript is not the correct tool for the job, and hence neither is Deno.
Compatibility with Node.js
Due to the many changes introduced, Deno doesn’t provide compatibility with existing JavaScript packages and tooling. A compatibility layer is starting to appear on the standard library but is not close to being finished.
As Node and Deno are very similar systems with shared goals, we expect the latter to execute more and more Node programs out of the box as time goes on. However, and even though some Node code is currently runnable, that is not the case currently.
TypeScript compiler speed
As mentioned previously, Deno uses the TypeScript compiler. It reveals itself as one of the slowest parts of the runtime, especially when compared to the time V8 takes to interpret JavaScript. Snapshots do help with this, but it is not enough. Deno’s core team believes that they will have to migrate the TypeScript compiler to Rust to fix it.
Due to the extensive work required to complete this task, this will most likely not be done anytime soon, even though it’s likely to be one of the things that would make startup time orders of magnitude faster.
Lack of plugins/extensions
Even though Deno has a plugin system to support custom operations, it is not yet finished and is considered unstable. Lacking this means that extending native functionality to more than Deno makes available is virtually impossible.
We should now understand the current Deno limitations and why they exist. Some of them might be resolved soon, as Deno matures and evolves. Others are the result of design decisions, or roadmap priorities. Understanding limitations is fundamental knowledge when it comes to deciding whether to use Deno in a project.
Exploring use cases
Most of the changes Deno introduces were brought in to make the runtime safer and more straightforward as compared with Node, but as it leverages most of the same pieces of technology, and shares the same engine as well as many of the same goals, use cases don’t differ much.
However, even though the differences are not that big, small nuances are likely to make one a slightly better fit than the other in specific situations.
A flexible scripting language
Scripting is one of those features where interpreted languages always shine. JavaScript is perfect when we want to prototype something fast. That can be renaming files, migrating data, or consuming something from an API. It just feels like the right tool for these use cases.
With Deno scripting has been the focus of much effort. The runtime itself makes it very easy for users to write scripts, and provides many benefits when compared to Node.js. Those benefits include: being able to execute code with just a URL, not having to manage dependencies, and the possibility of creating an executable based on Deno.
On top of all this, the fact that you can now import remote code while controlling the permissions it uses is a significant step up in terms of trust and security.
Deno’s REPL (Read Eval Print Loop) is a great place to do scripting work. Adding to what we’ve mentioned previously, the small size of the binary and the fact it includes all the necessary tools are the icing on the cake.
Safer desktop applications
Although the plugin system is not yet stable, and the packages that allow developers to create desktop applications depend heavily on that, it is very promising.
The last few years have witnessed the rise of Desktop Web Applications. The electron framework enabled applications like VS Code and Slack to be created. Those are web pages running inside a WebView with access to native features, and they have become part of many people’s daily lives.
However, for users to install these applications, they have to trust them blindly. We’ve previously discussed security and how JavaScript code used to have access to all the systems where it ran. Deno is fundamentally different here. With its sandbox and all its security features, it is much safer, and the potential unlocked is enormous.
We can expect to see lots of advances in the use of JavaScript to build Desktop applications with Deno.
Quick and complete environment for writing tools
Deno’s features position it as a very complete, simple, and fast environment for writing tooling. When we say tooling, we don’t just mean tooling for JavaScript or TypeScript projects. As the single binary contains everything needed to develop an application, we can use Deno in ecosystems outside the JS world.
The clarity, automatic documentation via TypeScript, ease of running, and popularity of JavaScript, make Deno the right cocktail to write tools like code generators, linters, etc, for other languages.
Runs on embedded devices
Using Rust and distributing the core as a Rust crate, Deno automatically enables usage in embedded devices. It might go from IoT devices to wearables and ARM devices. Again, the fact that it is small and includes all the tools in the binary is a big potential win.
The fact that the crate is made available standalone allows people to embed Deno in the most varied of places. For instance, when writing a database in Rust, and wanting to add a Map-Reduce logic, one can use JavaScript and Deno to do it.
Generates browser-compatible code
If you haven’t had a look at Deno before, this one probably comes as a surprise. Aren’t we talking about a server-side runtime? We are. But this same server-side runtime has been making efforts to keep the APIs browser compatible. It provides features in its toolchain that enable code to be written in Deno and executed in the browser.
All this care taken by the Deno team to keep APIs browser-compatible and capable of generating browser code opens a new set of possibilities yet to be explored.
Full-fledged APIs
Deno, like Node, puts considerable effort into dealing with HTTP servers. With a complete standard library providing great primitives for frameworks to write on top of, there is no doubt that APIs are among the strongest Deno use cases. TypeScript is a great value addition here, in terms of documentation, code generation, and static-type checking, helping mature codebases scale.
These are just a few examples of the use cases for which we believe Deno is a great fit. As happened with Node, there are undoubtedly many new uses waiting to be discovered. We’re excited to be joining this adventure and look forward to seeing what it has still to unveil.
Concluding remarks
Node brought an asynchronous, event-based approach to bear on the problem of concurrency, and capitalized on JavaScript’s popularity with web developers to become one of the leading web development environments. Now Deno builds on those foundations and promises to add greater security and simplicity to the mix. Its youth means there are still wrinkles to be ironed out, but increasingly Deno has the appearance of a technology with a very bright future.
Related Titles
Deno Web Development
Learn how to use Deno, the secure JavaScript and TypeScript runtime written in Rust, to write reliable web applications
Features
○ Understand Deno’s essential concepts and features
○ Learn how to use Deno in real-world scenarios
○ Use Deno to develop, test, and deploy web applications and tools
Node.js Web Development – Fifth Edition
Build scalable web applications using Node.js, Express.js, and the latest ECMAScript techniques, along with deploying applications with AWS and Docker with this updated fifth edition
Features
○ Learn backend web programming with the JavaScript stack
○ Explore best practices, right from configuring and building web servers to deploying them on a production cloud hosting system: AWS using Docker and Terraform
○ Work through the different stages of developing robust and scalable apps using Node.js 14
Svelte 3 Up and Running
Build your first web project using the Svelte framework and deploy it in the cloud with automated testing and CI/CD
Features
○ A practical guide to building production-ready static web apps with Svelte 3
○ Build faster and leaner frontend and static web apps using the JAMstack
○ Deploy your Svelte 3 app to production using cloud services and DevOps principles such as automated testing and CI/CD
Leave A Comment