If that sounds similar to you, then buckle up cowboys and cowgirls because you’re about to go “oh, yeah, cool” a lot at some pretty basic distinctions that you probably intuitively knew but didn’t have the words for.
I think there’s innate value in understanding more about where and how your code actually runs. It’s rarely directly applicable, but at some point in writing a large-enough app in JS, especially if it’s full-stack, you’re going to need to know these things. It’ll also help you understand why Node.JS and client-side JS are different, and appreciate the overall portability (if not the performance) of JS in the wider context of software engineering. Or throw it out there in your next engineering interview and sound smart and informed, and hope they don’t ask any deeper questions 🤷♀️
The JS runtime is the place where JS (or its machine-readable counterpart) is actually run. The browser is a runtime and it’s how JS has access to things like the
window object. Server-side JS runtimes also exist, like Node.JS.
Engines are the bit of software which take our source code, e.g. the human-readable JS code, and transform them to computer-executable instructions. There are a number of ways that engines can do this, some of them use Just in Time (JIT) compilation, but JS is an interpreted (i.e. non-compiled) language.
I just assumed that the JS runtime was the browser or server where the JS actually ran, i.e. where the commands (like
console.log('hello')) were executed. And I was right, but there’s a little more detail to this answer.
JS runtimes get a bit of flack for causing bad performance, whatever that generic term means. Take this critique from Sebastian De Deyne:
We’re adding heavy runtimes to support multiple platforms instead of staying close to the metal, and we pay the price in performance.
This criticism brings us a little closer to a useful definition of a Runtime: it includes the libraries and frameworks which act as the intermediary between the code you’ve written, and the operating system it is running on. So if we take this mental model a little further, a runtime is the abstractions that sit between the bare metal and the running program (StackOverflow source).
Programming languages haven’t always needed, or come with, a runtime. Some of the earlier languages, like FORTRAN and COBOL have such a limited set of features and syntax that the language is the runtime (at least conceptually). The difference between the source code (i.e. the code written by an author) and the machine-executable compiled code which are created from the source code, is pretty minimal in these languages. The code you write details explicit manual control over memory allocation, garbage collection (and a bunch of other features) that the source code doesn’t need to bring in anything else. The computer can run that file just fine by itself. Obviously this shifts the role of a program from not only its intended computation, to include also the memory allocation and logistics around these computations.
The C programming language is a good example of when we bought in runtimes. C came with the ability to call other libraries or functions to achieve certain things. For example, the
malloc function allows an author to allocate memory and get a pointer to its location.
malloc is defined in
stdlib of C - in a separate C file to the one the author is currently writing. The
stdlib library defines exactly how memory is allocated through and the correct address is returned. This allows us, the authors, to focus on the logic of our code specifically, and not to have to re-write a memory allocation function in pretty much every bit of C code we write. This speeds us up, reduces errors, creates conventions… all good things in software engineering at scale.
That means that if we were to write a simple C script which invoked
malloc, when we came to execute our code it would need to also have some way of gaining it access to the
malloc is defined there, not in our code. The
malloc function is not syntax in the language, in the way that
static are. The runtime of our C script would include access to the
malloc function. These libraries would need to be linked.
Subsequent languages, famously Java and C#, came with a very heavy runtime - their Virtual Machine (VM). This is what allows both languages to boast portability between architectures and OSs - the source code runs inside of a VM, and the VM abstracts away all the complexity of interacting with the bare metal.
A JS engine is what gets us from the human-readable JS code into a computer-interpretable set of instructions. E.g.
const name = 'Thomas' isn’t an instruction that an operating system or browser would understand. Instead, we need some way of letting our program find and manage memory, link things together, etc.
Let’s take a moment to just cover some technical terms:
- Source Code is the original code in JS, e.g. written by a human author. It may also be valid JS which has been minified by a build step.
- Machine Code is a set of instructions for CPUs for loading memory, performing specific tasks etc. Machine code is not human readable (wikipedia)
Different JS engines go from source code to machine code in different ways, which vary with their target environment and can affect the performance of a script. For examples, the V8 engine compiles to machine code, whereas Rhino compiles JS to Java classes. Let’s not talk about the implications of this - just assume that they’re comparable but different… ya know?
The JS engine is what lets our JS code just… run. Engines are responsible for several low-level machine operations which are necessary on the journey from source code to machine code. They load the script into the computer’s memory, link all the necessary libraries together so all parts of the program have access to each other (remember runtimes?), and then find the entry point (i.e. the instructions in the
index.js file) and start executing those.
This is the source of one of the most defining “features” for novice and experienced JS authors alike, the
cannot read property X of undefined error, caused by the fact that we have tried to access a property (
X) of something which does not exist (
undefined). This is a classic kind of error which a compiled language may be able to catch - by checking for potentially undefined variables. But we’re getting off track.
With that said, the highly performant V8 engine uses Just in Time (JIT) compilation of JS to machine code. This means the JS source code is compiled, but as close to its execution as possible. I’m not going to go into JIT compilation here because it’s a little long, but Mozilla have a great summary on it.
So yeah - the engine is what takes our
.js files, and allows a computer to do the relevant computer-y things with them. And here we all are, trying to remember which way round the Boolean logic works on