Roll your own JavaScript runtime, pt. 3
This post is a continuation of Roll your own JavaScript runtime and Roll your own JavaScript runtime, pt. 2.
We’ve been delighted by the positive response to this series on rolling your own custom JavaScript runtime. One area that some expressed interest in is how to use snapshots to get faster startup times. Snapshots can provide improved performance at a (typically) negligible increase in filesize.
In this blog post, we’ll build on the
first and
second part by creating a snapshot
of runtime.js
in a build script, then loading that snapshot in main.rs
to
speed up start time for our custom runtime.
Watch the video demo or view source code here.
Getting setup
If you followed the first and second blog post, your project should have three files:
example.ts
: the JavaScript file we intend to execute with the custom runtimesrc/main.rs
: the asynchronous Rust function that creates an instance ofJsRuntime
, which is responsible for JavaScript executionsrc/runtime.js
: the runtime interface that defines and provides the API that will interop with theJsRuntime
frommain.rs
Let’s write a build.rs
file that will create a snapshot of the custom runtime,
runjs
.
build.rs
Creating a snapshot in Before create a build.rs
file, let’s first add deno_core
as a build
dependency in Cargo.toml
:
[package]
name = "runjs"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
deno_ast = { version = "0.22.0", features = ["transpiling"] }
deno_core = "0.174.0"
reqwest = "0.11.14"
tokio = { version = "1.25.0", features = ["full"] }
+ [build-dependencies]
+ deno_core = "0.174.0"
Next, let’s create a build.rs
file in the root of your project. In this file,
we’ll have to do the following steps:
- Create a small extension of
src/runtime.js
- Build a file path to the snapshot
- Create the snapshot
Putting the above steps into code, your build.rs
script should look like:
use std::env;
use std::path::PathBuf;
use deno_core::{Extension, include_js_files};
fn main() {
// Create the runjs extension.
let runjs_extension = Extensions::builder("runjs")
.esm(include_js_files!(
"src/runtime.js",
))
.build();
// Build the file path to the snapshot.
let o = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let snapshot_path = o.join("RUNJS_SNAPSHOT.bin");
// Create the snapshot.
deno_core::snapshot_util::create_snapshot(deno_core::snapshot_util::CreateSnapshotOptions {
cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"),
snapshot_path: snapshot_path,
startup_snapshot: None,
extensions: vec!(runjs_extension),
compression_cb, None,
Snapshot_module_load_cb: None,
})
}
The main function is create_snapshot
, which accepts several options. Let’s go
over them in the next section.
CreateSnapshotOptions
Diving into The create_snapshot
function is a nice abstraction layer that uses an options
struct to determine how the snapshot is created. Let’s go over the different
fields that are available in CreateSnapshotOptions
.
pub struct CreateSnapshotOptions {
pub cargo_manifest_dir: &'static str,
pub snapshot_path: PathBuf,
pub startup_snapshot: Option<Snapshot>,
pub extensions: Vec<Extensions>,
pub compression_cb: Option<Box<CompressionCb>>,
pub snapshot_module_load_cb: Option<ExtModuleLoaderCb>,
}
cargo_manifest_dir
: the directory in which Cargo will be compiling everything into. This should always be theCARGO_MANIFEST_DIR
environment variable.snapshot_path
: the path where we write the snapshot to.startup_snapshot
: you can pass in a snapshot, if you want to build a snapshot from another snapshot.extensions
: any extensions that you would like to add on top of the snapshotcompression_cb
: you can set which compression method to use to further reduce the filesize.snapshot_module_load_cb
: you can pass a module loader that will transform files included in the snapshot. For example, you can transpile TypeScript to JavaScript.
In this example, we only use:
snapshot_path
: we define the snapshot path by resolvingOUT_DIR
andRUNJS_SNAPSHOT.bin
.extensions
: we pass therunjs_extension
, which is built fromsrc/runtime.js
.
main.rs
Loading the snapshot in Currently, the main.rs
file’s run_js
function loads the runjs_extension
.
We’ll modify this function to instead load the snapshot we created in
build.rs
:
- use deno_core::include_js_files;
+ use deno_core::Snapshot;
// Other stuff…
+ static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/RUNJS_SNAPSHOT.bin"));
async fn run_js(file_path: &str) -> Result<(), AnyError> {
let main_module = deno_core::resolve_path(file_path)?;
let runjs_extension = Extension::builder("runjs")
- .esm(include_js_files!(
- "runtime.js",
- ))
.ops(vec![
op_read_file::decl(),
op_write_file::decl(),
op_remove_file::decl(),
op_fetch::decl(),
op_set_timeout::decl(),
])
.build();
let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
module_loader: Some(Rc::new(TsModuleLoader)),
+ startup_snapshot: Some(Snapshot::Static(RUNTIME_SNAPSHOT)),
extensions: vec![runjs_extension],
..Default::default()
});
let mod_id = js_runtime.load_main_module(&main_module, None).await?;
let result = js_runtime.mod_evaluate(mod_id);
js_runtime.run_event_loop(false).await?;
result.await?
}
We’ll remove the .esm()
function, since that was moved to build.rs
script.
Then, we’ll add a line to load the snapshot.
Finally, to load the snapshot, we’ll add startup_snapshot
in RuntimeOptions
that points to the RUNTIME_SNAPSHOT
, which is defined above run_js
as a
static slice of bytes of the snapshot we created in build.rs
.
And that’s it! Let’s try running with:
cargo run -- example.ts
It should work!
What’s next?
Snapshotting is an excellent tool to help improve startup speeds for a custom runtime. This is an extremely simple example, but we hope that it sheds some light into how Deno uses snapshots to optimize performance.
Through this series, we’ve shown how to build your own custom JavaScript
runtime, add APIs like fetch
, and now speed up startup times via snapshotting.
We love hearing from you so if there’s anything you want us to cover, please let
us know on Twitter,
YouTube, or
Discord.
Don’t miss any updates — follow us on Twitter.