# How Deno's Runtime Extension System Works: A Deep Dive into deno_core

> Explore Deno's runtime extension system by diving into deno_core. Learn how Rust ops and JS source bundle into modular, permission-aware V8 extensions for native code integration.

- Repository: [Deno/deno](https://github.com/denoland/deno)
- Tags: deep-dive
- Published: 2026-02-26

---

**Deno's runtime extension system uses the `Extension` struct in `deno_core` to bundle Rust ops and JavaScript source, registering them with V8 during `JsRuntime` initialization to create a modular, permission-aware bridge between native code and JavaScript.**

The `denoland/deno` repository implements its JavaScript/TypeScript runtime through a layered architecture where native Rust capabilities are exposed to JavaScript via a structured extension system. This design allows Deno to ship core APIs as discrete, optional modules while maintaining high performance through V8 snapshotting.

## Core Architecture and Types

The extension system centers on four key types defined across the `deno_core` and `runtime` crates.

**`deno_core::Extension`** — Defined in [[`deno_core/src/ext.rs`](https://github.com/denoland/deno/blob/main/deno_core/src/ext.rs)](https://github.com/denoland/deno/blob/main/deno_core/src/ext.rs), this struct holds op registrations, JavaScript/ESM source files, and metadata required to bridge Rust and V8.

**`deno_core::ExtensionFileSource`** — Located in the same file, this type represents JavaScript source that can be inline, static, or computed dynamically.

**`runtime::worker::JsRuntime`** — Found in [[`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs)](https://github.com/denoland/deno/blob/main/runtime/worker.rs), this core runtime accepts a `Vec<Extension>` during instantiation and manages the V8 isolate lifecycle.

**`runtime::snapshot::Snapshot`** — Implemented in [[`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs)](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs), this type pre-creates a V8 snapshot containing core extensions to eliminate cold-start latency.

## The Extension Registration Flow

Deno registers native capabilities through a five-phase pipeline that transforms Rust functions into callable JavaScript APIs.

### Defining Ops with the `#[op]` Macro

Operations (ops) are Rust functions decorated with the `#[op]` procedural macro. Each op becomes a V8 function accessible from JavaScript.

```rust
// ext/web/lib.rs
use deno_core::op;

#[op]
async fn op_fetch(req: FetchRequest) -> Result<FetchResponse, AnyError> {
    // Native implementation...
}

```

The macro automatically generates the V8 binding code required to marshal data between JavaScript and Rust.

### Building Extensions with `Extension::builder()`

Each extension crate exposes an initialization function that constructs an `Extension` using the builder pattern. This bundles ops with their associated JavaScript runtimes.

```rust
// ext/web/lib.rs
pub fn init_ops_and_esm() -> Extension {
    Extension::builder()
        .ops(vec![
            ("op_fetch", op_fetch::decl()),
            // Additional ops...
        ])
        .esm_files(vec![
            ExtensionFileSource::js_script(
                "ext:deno_web/99_main.js",
                include_str!("js/99_main.js"),
            ),
        ])
        .build()
}

```

The `Extension::builder()` API lives in `deno_core::Extension` and supports both ESM files and traditional scripts.

### Aggregating Extensions in the Runtime

The Deno binary collects all core extensions into a vector before creating the runtime. In [[`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs)](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs), the system aggregates standard library extensions:

```rust
// runtime/snapshot.rs
let mut extensions: Vec<Extension> = vec![
    deno_web::init_ops_and_esm(),
    deno_net::init_ops_and_esm(),
    deno_fs::init_ops_and_esm(),
    // ...other core extensions
];

```

This vector is then passed to `JsRuntime::new` in [[`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs)](https://github.com/denoland/deno/blob/main/runtime/worker.rs):

```rust
let js_runtime = JsRuntime::new(RuntimeOptions {
    extensions,
    // Additional options...
});

```

At this point, V8 registers each op and compiles the provided JavaScript source into the isolate.

### Customizing Extensions Before Runtime Creation

Some consumers require dynamic modification of extensions before runtime initialization. The CLI's TypeScript compiler in [[`cli/tsc/js.rs`](https://github.com/denoland/deno/blob/main/cli/tsc/js.rs)](https://github.com/denoland/deno/blob/main/cli/tsc/js.rs) demonstrates this pattern using a customizer closure:

```rust
let mut ext = deno_cli_tsc::init_ops_and_esm();
let customizer = |ext: &mut Extension| {
    use deno_core::ExtensionFileSource;
    ext.esm_files.to_mut().push(
        ExtensionFileSource::new_computed(
            "ext:deno_cli_tsc/99_main_compiler.js",
            crate::tsc::MAIN_COMPILER_SOURCE.as_str().into(),
        ),
    );
};
customizer(&mut ext);

```

This technique allows injection of context-specific scripts without modifying the base extension crate.

## V8 Snapshotting for Performance

To eliminate startup overhead, Deno bakes core extensions into a **V8 snapshot** during the build process. The [[`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs)](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs) module creates this snapshot by calling [`get_extensions_in_snapshot`](https://github.com/denoland/deno/blob/main/runtime/snapshot_info.rs) from [[`runtime/snapshot_info.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot_info.rs)](https://github.com/denoland/deno/blob/main/runtime/snapshot_info.rs):

```rust
// runtime/snapshot.rs (simplified)
let mut extensions = get_extensions_in_snapshot();
let mut snapshot = Snapshot::new(&mut extensions, include_js_files);

```

When `JsRuntime` is instantiated with `snapshot: Some(snapshot_blob)`, V8 restores the pre-initialized isolate. This bypasses the cost of re-registering ops and re-parsing core JavaScript modules on every process start.

## Creating a Custom Extension

Third-party developers and embedders can create custom extensions using the same primitives as Deno's core APIs.

```rust
use deno_core::{Extension, op, JsRuntime, RuntimeOptions, ExtensionFileSource};

// 1. Define the native operation.
#[op]
fn op_hello(name: String) -> String {
    format!("Hello, {name}!")
}

// 2. Construct the extension.
fn hello_extension() -> Extension {
    Extension::builder()
        .ops(vec![("op_hello", op_hello::decl())])
        .esm_files(vec![
            ExtensionFileSource::js_script(
                "ext:my_hello/hello.js",
                r#"
                export function hello(name) {
                  return Deno.core.ops.op_hello(name);
                }
                "#,
            ),
        ])
        .build()
}

// 3. Initialize the runtime with the custom extension.
fn main() {
    let extensions = vec![hello_extension()];
    let mut runtime = JsRuntime::new(RuntimeOptions {
        extensions,
        ..Default::default()
    });

    // 4. Execute JavaScript utilizing the new API.
    runtime
        .execute_script(
            "<anon>",
            r#"
            import { hello } from "ext:my_hello/hello.js";
            console.log(hello("Deno"));
            "#,
        )
        .unwrap();
}

```

This pipeline demonstrates the complete lifecycle: Rust op definition, extension construction, runtime injection, and JavaScript consumption via the `Deno.core.ops` namespace.

## Summary

- **`deno_core::Extension`** in [`deno_core/src/ext.rs`](https://github.com/denoland/deno/blob/main/deno_core/src/ext.rs) serves as the fundamental abstraction for bundling Rust ops and JavaScript source.
- The **`#[op]`** macro transforms Rust functions into V8-callable operations, while **`Extension::builder()`** assembles them into deployable units.
- **Registration** occurs when `JsRuntime::new` receives a `Vec<Extension>`, at which point ops are bound to the V8 isolate.
- **Snapshotting** in [`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs) pre-compiles core extensions into V8 snapshots, reducing cold-start latency by avoiding re-registration and re-parsing.
- **Customization closures** allow dynamic script injection before runtime initialization, as seen in the TypeScript compiler integration.
- Extensions live in dedicated crates (e.g., `ext/web`, `ext/net`, `ext/fs`), enabling modular API development and permission isolation.

## Frequently Asked Questions

### What is the difference between `deno_core::Extension` and `JsRuntime`?

`deno_core::Extension` is a static definition that bundles Rust ops and JavaScript source code, essentially a blueprint for capabilities. `JsRuntime`, defined in [`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs), is the active V8 isolate that consumes extensions during instantiation. While an `Extension` describes what APIs are available, `JsRuntime` creates the execution environment where those APIs become callable JavaScript functions.

### How does Deno's extension system improve startup performance?

Deno leverages V8 snapshots to eliminate initialization overhead. During the build process, [`runtime/snapshot.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot.rs) creates a snapshot containing pre-registered core extensions from [`runtime/snapshot_info.rs`](https://github.com/denoland/deno/blob/main/runtime/snapshot_info.rs). When a new process starts, `JsRuntime` restores this snapshot rather than re-registering ops and re-parsing JavaScript, reducing cold-start time from hundreds of milliseconds to near-instant.

### Can I remove core extensions to create a minimal Deno runtime?

Yes, because Deno organizes each API domain into separate extension crates (`ext/web`, `ext/net`, `ext/crypto`), you can construct a custom `Vec<Extension>` containing only required modules. When creating `JsRuntime` in [`runtime/worker.rs`](https://github.com/denoland/deno/blob/main/runtime/worker.rs), simply omit unnecessary extensions from the vector. This is particularly useful for embedded scenarios or security-constrained environments where network or filesystem access must be prohibited at the runtime level.

### How do permissions integrate with the extension system?

Permissions are enforced at the op layer. When an extension registers an op like `op_fetch` in [`ext/fetch/lib.rs`](https://github.com/denoland/deno/blob/main/ext/fetch/lib.rs) or `op_read_file` in `ext/fs`, the Rust implementation checks permission states before executing privileged operations. Because extensions are modular, Deno can enable or disable entire capability domains by including or excluding the respective extension, creating a coarse-grained permission boundary complemented by fine-grained runtime checks.