DEV Community

Cover image for Compilation vs. Interpretation: The Question That Turned Out to Be More Complicated Than Expected
Shajibul Alam Shihab
Shajibul Alam Shihab

Posted on

Compilation vs. Interpretation: The Question That Turned Out to Be More Complicated Than Expected

Compilation vs. Interpretation: The Question That Turned Out to Be More Complicated Than Expected

For a long time, the answer seemed obvious.

"Is JavaScript compiled or interpreted?"

The response came almost automatically:

"Interpreted."

Then another question appeared.

"If JavaScript is interpreted, why does V8 have a compiler?"

That single question unraveled a misconception that many developers including experienced ones carry for years.

The truth is that modern programming languages rarely fit into neat labels like compiled or interpreted. Once the execution pipeline is understood, those labels start to feel overly simplistic.


Every program begins life as source code.

Whether it's JavaScript, Python, Rust, Go, or TypeScript, none of that code can be executed directly by a processor.

A CPU understands only one thing:

Machine code.

Somewhere between writing code and seeing a program run, translation has to happen.

The only real difference between languages is when and how that translation takes place.


The First Approach: Translate Everything Up Front

Imagine translating an entire novel before anyone starts reading it.

The work takes time initially, but once it's finished, every future read is fast because no translation is needed anymore.

That's exactly how Ahead-of-Time (AOT) compilation works.

Languages like C, Rust, and Go compile the entire program into native machine code before execution begins.

gcc program.c -o program
./program
Enter fullscreen mode Exit fullscreen mode

By the time ./program runs, every instruction has already been translated into machine code.

The CPU simply executes it.

The advantages are obvious:

  • Excellent runtime performance
  • Errors are caught before execution
  • No translation overhead while the program is running

The trade-off?

A separate executable is needed for every target platform and architecture.


The Second Approach: Translate While Running

Now imagine a translator sitting beside the reader.

Instead of translating the whole book beforehand, each sentence is translated only when it's reached.

No preparation.

No finished translated copy.

Just translation and execution happening together.

That's the traditional idea of interpretation.

A command like:

python script.py
Enter fullscreen mode Exit fullscreen mode

doesn't produce a standalone executable first.

Instead, the interpreter reads the program and executes it as it goes.

This approach offers incredible portability.

The same source code works anywhere the interpreter exists.

The downside is that translation work happens every time the program runs.


Then Came the Plot Twist

For years, programming books often divided languages into two categories:

  • Compiled
  • Interpreted

That classification made sense decades ago.

Today?

Not so much.

Modern language runtimes are far more sophisticated.

Instead of choosing one approach, many combine several.


Enter Bytecode

Before reaching machine code, many languages first compile into something called bytecode.

Bytecode isn't machine code.

It's an intermediate instruction set designed to be portable across different operating systems and processors.

Instead of looking like this:

Source Code
      ↓
Machine Code
Enter fullscreen mode Exit fullscreen mode

the pipeline becomes:

Source Code
      ↓
Bytecode
      ↓
Virtual Machine
      ↓
Execution
Enter fullscreen mode Exit fullscreen mode

This is the first clue that the old "compiled vs interpreted" debate isn't telling the whole story.

Python does this.

Java does this.

Even though both are commonly described as interpreted languages, they still perform a compilation step.

It just doesn't produce machine code directly.


And Then Things Get Even More Interesting

Modern runtimes don't stop at bytecode.

They watch how a program behaves while it's running.

Suppose a function gets called once.

Nothing special happens.

Suppose another function gets called a million times inside a loop.

Now the runtime notices.

Instead of interpreting that same code forever, it decides something.

"This function is important."

That frequently executed section—often called a hot path—gets compiled into native machine code while the program is still running.

This process is called Just-In-Time (JIT) compilation.

It's one of the reasons modern JavaScript engines are incredibly fast.

Rather than compiling everything upfront or interpreting everything forever, they optimize only the code that actually matters.

The result is a balance between fast startup and excellent long-running performance.

JavaScript's V8, Java's HotSpot JVM, and PyPy all use this strategy.


So... What About TypeScript?

This was the part that completely changed the mental model.

At first glance, TypeScript seems similar to C.

After all, there's a compiler.

Running tsc certainly feels like compilation.

But tsc isn't producing machine code.

Its job is much simpler.

Given this code:

function double(n: number): number {
    return n * 2;
}
Enter fullscreen mode Exit fullscreen mode

The TypeScript compiler checks the types...

removes every type annotation...

and emits plain JavaScript.

function double(n) {
    return n * 2;
}
Enter fullscreen mode Exit fullscreen mode

That's where tsc stops.

The resulting JavaScript is then handed to a JavaScript engine like V8.

Only then does the familiar execution pipeline begin:

  • Parse JavaScript
  • Generate bytecode
  • Start execution
  • Identify hot paths
  • JIT compile them into native machine code

In other words, a TypeScript program goes through two completely different compilation stages.

First:

TypeScript → JavaScript

Later:

JavaScript → Native Machine Code (via V8's JIT compiler)

That's why answering

"Is TypeScript compiled or interpreted?"

with a single word doesn't really work.

It's both.

Just at different stages.


The Biggest Misconceptions

One misconception appears almost everywhere:

"Interpreted languages aren't compiled."

Modern Python and Java both compile into bytecode before execution.

Compilation still exists.

It simply happens at a different stage.

Another common misconception:

"tsc works like the C compiler."

It doesn't.

The TypeScript compiler never produces machine code.

It only performs type checking and transpiles TypeScript into JavaScript.

The JavaScript engine still carries the responsibility of producing executable machine instructions.


One Last Thing About Performance

Benchmarks for JIT-powered languages can be surprisingly misleading.

The first few executions often represent the runtime before optimization.

Only after the runtime identifies hot paths and compiles them into native machine code does peak performance begin to appear.

That's why serious benchmarks usually include a warm-up phase before measuring execution speed.


Perhaps the biggest lesson wasn't learning what compilation or interpretation means.

It was realizing that the question itself has become outdated.

Modern programming languages aren't strictly compiled.

They aren't strictly interpreted either.

Most combine multiple techniques compilation, bytecode, interpretation, and JIT optimization to balance portability, startup time, developer experience, and runtime performance.

So the next time the question appears

"Is JavaScript compiled or interpreted?"

The most accurate answer might simply be:

"It's more interesting than that."

Top comments (0)