diff --git a/core/bindings.rs b/core/bindings.rs
index 50308b9318..8ac3082508 100644
--- a/core/bindings.rs
+++ b/core/bindings.rs
@@ -111,12 +111,29 @@ pub fn initialize_context<'s>(
 
   let scope = &mut v8::ContextScope::new(scope, context);
 
+  let deno_str = v8::String::new(scope, "Deno").unwrap();
+  let core_str = v8::String::new(scope, "core").unwrap();
+  let ops_str = v8::String::new(scope, "ops").unwrap();
+
   // Snapshot already registered `Deno.core.ops` but
   // extensions may provide ops that aren't part of the snapshot.
   if snapshot_options.loaded() {
     // Grab the Deno.core.ops object & init it
-    let ops_obj = JsRuntime::eval::<v8::Object>(scope, "Deno.core.ops")
-      .expect("Deno.core.ops to exist");
+    let deno_obj: v8::Local<v8::Object> = global
+      .get(scope, deno_str.into())
+      .unwrap()
+      .try_into()
+      .unwrap();
+    let core_obj: v8::Local<v8::Object> = deno_obj
+      .get(scope, core_str.into())
+      .unwrap()
+      .try_into()
+      .unwrap();
+    let ops_obj: v8::Local<v8::Object> = core_obj
+      .get(scope, ops_str.into())
+      .expect("Deno.core.ops to exist")
+      .try_into()
+      .unwrap();
     initialize_ops(scope, ops_obj, op_ctxs, snapshot_options);
     if snapshot_options != SnapshotOptions::CreateFromExisting {
       initialize_async_ops_info(scope, ops_obj, op_ctxs);
@@ -126,11 +143,9 @@ pub fn initialize_context<'s>(
 
   // global.Deno = { core: { } };
   let deno_obj = v8::Object::new(scope);
-  let deno_str = v8::String::new(scope, "Deno").unwrap();
   global.set(scope, deno_str.into(), deno_obj.into());
 
   let core_obj = v8::Object::new(scope);
-  let core_str = v8::String::new(scope, "core").unwrap();
   deno_obj.set(scope, core_str.into(), core_obj.into());
 
   // Bind functions to Deno.core.*
@@ -144,7 +159,6 @@ pub fn initialize_context<'s>(
 
   // Bind functions to Deno.core.ops.*
   let ops_obj = v8::Object::new(scope);
-  let ops_str = v8::String::new(scope, "ops").unwrap();
   core_obj.set(scope, ops_str.into(), ops_obj.into());
 
   if !snapshot_options.will_snapshot() {
diff --git a/core/runtime.rs b/core/runtime.rs
index 9c6b7afeaf..c028d97c29 100644
--- a/core/runtime.rs
+++ b/core/runtime.rs
@@ -985,11 +985,36 @@ impl JsRuntime {
   fn init_cbs(&mut self, realm: &JsRealm) {
     let (recv_cb, build_custom_error_cb) = {
       let scope = &mut realm.handle_scope(self.v8_isolate());
-      let recv_cb =
-        Self::eval::<v8::Function>(scope, "Deno.core.opresolve").unwrap();
-      let build_custom_error_cb =
-        Self::eval::<v8::Function>(scope, "Deno.core.buildCustomError")
-          .expect("Deno.core.buildCustomError is undefined in the realm");
+      let context = realm.context();
+      let context_local = v8::Local::new(scope, context);
+      let global = context_local.global(scope);
+      let deno_str = v8::String::new(scope, "Deno").unwrap();
+      let core_str = v8::String::new(scope, "core").unwrap();
+      let opresolve_str = v8::String::new(scope, "opresolve").unwrap();
+      let build_custom_error_str =
+        v8::String::new(scope, "buildCustomError").unwrap();
+
+      let deno_obj: v8::Local<v8::Object> = global
+        .get(scope, deno_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
+      let core_obj: v8::Local<v8::Object> = deno_obj
+        .get(scope, core_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
+
+      let recv_cb: v8::Local<v8::Function> = core_obj
+        .get(scope, opresolve_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
+      let build_custom_error_cb: v8::Local<v8::Function> = core_obj
+        .get(scope, build_custom_error_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
       (
         v8::Global::new(scope, recv_cb),
         v8::Global::new(scope, build_custom_error_cb),
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index fa9b0a20d2..da5b5f1b8b 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -231,6 +231,69 @@ function formatException(error) {
   }
 }
 
+core.registerErrorClass("NotFound", errors.NotFound);
+core.registerErrorClass("PermissionDenied", errors.PermissionDenied);
+core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused);
+core.registerErrorClass("ConnectionReset", errors.ConnectionReset);
+core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted);
+core.registerErrorClass("NotConnected", errors.NotConnected);
+core.registerErrorClass("AddrInUse", errors.AddrInUse);
+core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable);
+core.registerErrorClass("BrokenPipe", errors.BrokenPipe);
+core.registerErrorClass("AlreadyExists", errors.AlreadyExists);
+core.registerErrorClass("InvalidData", errors.InvalidData);
+core.registerErrorClass("TimedOut", errors.TimedOut);
+core.registerErrorClass("Interrupted", errors.Interrupted);
+core.registerErrorClass("WouldBlock", errors.WouldBlock);
+core.registerErrorClass("WriteZero", errors.WriteZero);
+core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof);
+core.registerErrorClass("BadResource", errors.BadResource);
+core.registerErrorClass("Http", errors.Http);
+core.registerErrorClass("Busy", errors.Busy);
+core.registerErrorClass("NotSupported", errors.NotSupported);
+core.registerErrorBuilder(
+  "DOMExceptionOperationError",
+  function DOMExceptionOperationError(msg) {
+    return new DOMException(msg, "OperationError");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionQuotaExceededError",
+  function DOMExceptionQuotaExceededError(msg) {
+    return new DOMException(msg, "QuotaExceededError");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionNotSupportedError",
+  function DOMExceptionNotSupportedError(msg) {
+    return new DOMException(msg, "NotSupported");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionNetworkError",
+  function DOMExceptionNetworkError(msg) {
+    return new DOMException(msg, "NetworkError");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionAbortError",
+  function DOMExceptionAbortError(msg) {
+    return new DOMException(msg, "AbortError");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionInvalidCharacterError",
+  function DOMExceptionInvalidCharacterError(msg) {
+    return new DOMException(msg, "InvalidCharacterError");
+  },
+);
+core.registerErrorBuilder(
+  "DOMExceptionDataError",
+  function DOMExceptionDataError(msg) {
+    return new DOMException(msg, "DataError");
+  },
+);
+
 function runtimeStart(runtimeOptions, source) {
   core.setMacrotaskCallback(timers.handleTimerMacrotask);
   core.setMacrotaskCallback(promiseRejectMacrotaskCallback);
@@ -247,72 +310,6 @@ function runtimeStart(runtimeOptions, source) {
   colors.setNoColor(runtimeOptions.noColor || !runtimeOptions.isTty);
   // deno-lint-ignore prefer-primordials
   Error.prepareStackTrace = core.prepareStackTrace;
-  registerErrors();
-}
-
-function registerErrors() {
-  core.registerErrorClass("NotFound", errors.NotFound);
-  core.registerErrorClass("PermissionDenied", errors.PermissionDenied);
-  core.registerErrorClass("ConnectionRefused", errors.ConnectionRefused);
-  core.registerErrorClass("ConnectionReset", errors.ConnectionReset);
-  core.registerErrorClass("ConnectionAborted", errors.ConnectionAborted);
-  core.registerErrorClass("NotConnected", errors.NotConnected);
-  core.registerErrorClass("AddrInUse", errors.AddrInUse);
-  core.registerErrorClass("AddrNotAvailable", errors.AddrNotAvailable);
-  core.registerErrorClass("BrokenPipe", errors.BrokenPipe);
-  core.registerErrorClass("AlreadyExists", errors.AlreadyExists);
-  core.registerErrorClass("InvalidData", errors.InvalidData);
-  core.registerErrorClass("TimedOut", errors.TimedOut);
-  core.registerErrorClass("Interrupted", errors.Interrupted);
-  core.registerErrorClass("WouldBlock", errors.WouldBlock);
-  core.registerErrorClass("WriteZero", errors.WriteZero);
-  core.registerErrorClass("UnexpectedEof", errors.UnexpectedEof);
-  core.registerErrorClass("BadResource", errors.BadResource);
-  core.registerErrorClass("Http", errors.Http);
-  core.registerErrorClass("Busy", errors.Busy);
-  core.registerErrorClass("NotSupported", errors.NotSupported);
-  core.registerErrorBuilder(
-    "DOMExceptionOperationError",
-    function DOMExceptionOperationError(msg) {
-      return new DOMException(msg, "OperationError");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionQuotaExceededError",
-    function DOMExceptionQuotaExceededError(msg) {
-      return new DOMException(msg, "QuotaExceededError");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionNotSupportedError",
-    function DOMExceptionNotSupportedError(msg) {
-      return new DOMException(msg, "NotSupported");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionNetworkError",
-    function DOMExceptionNetworkError(msg) {
-      return new DOMException(msg, "NetworkError");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionAbortError",
-    function DOMExceptionAbortError(msg) {
-      return new DOMException(msg, "AbortError");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionInvalidCharacterError",
-    function DOMExceptionInvalidCharacterError(msg) {
-      return new DOMException(msg, "InvalidCharacterError");
-    },
-  );
-  core.registerErrorBuilder(
-    "DOMExceptionDataError",
-    function DOMExceptionDataError(msg) {
-      return new DOMException(msg, "DataError");
-    },
-  );
 }
 
 const pendingRejections = [];
diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs
index bcf999b3b7..353c4a4429 100644
--- a/runtime/web_worker.rs
+++ b/runtime/web_worker.rs
@@ -318,6 +318,7 @@ pub struct WebWorker {
   pub worker_type: WebWorkerType,
   pub main_module: ModuleSpecifier,
   poll_for_messages_fn: Option<v8::Global<v8::Value>>,
+  bootstrap_fn_global: Option<v8::Global<v8::Function>>,
 }
 
 pub struct WebWorkerOptions {
@@ -496,6 +497,25 @@ impl WebWorker {
       (internal_handle, external_handle)
     };
 
+    let bootstrap_fn_global = {
+      let context = js_runtime.global_context();
+      let scope = &mut js_runtime.handle_scope();
+      let context_local = v8::Local::new(scope, context);
+      let global_obj = context_local.global(scope);
+      let bootstrap_str = v8::String::new(scope, "bootstrap").unwrap();
+      let bootstrap_ns: v8::Local<v8::Object> = global_obj
+        .get(scope, bootstrap_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
+      let main_runtime_str = v8::String::new(scope, "workerRuntime").unwrap();
+      let bootstrap_fn =
+        bootstrap_ns.get(scope, main_runtime_str.into()).unwrap();
+      let bootstrap_fn =
+        v8::Local::<v8::Function>::try_from(bootstrap_fn).unwrap();
+      v8::Global::new(scope, bootstrap_fn)
+    };
+
     (
       Self {
         id: worker_id,
@@ -505,6 +525,7 @@ impl WebWorker {
         worker_type: options.worker_type,
         main_module,
         poll_for_messages_fn: None,
+        bootstrap_fn_global: Some(bootstrap_fn_global),
       },
       external_handle,
     )
@@ -513,15 +534,24 @@ impl WebWorker {
   pub fn bootstrap(&mut self, options: &BootstrapOptions) {
     // Instead of using name for log we use `worker-${id}` because
     // WebWorkers can have empty string as name.
-    let script = format!(
-      "bootstrap.workerRuntime({}, \"{}\", \"{}\")",
-      options.as_json(),
-      self.name,
-      self.id
-    );
-    self
-      .execute_script(&located_script_name!(), &script)
-      .expect("Failed to execute worker bootstrap script");
+    {
+      let scope = &mut self.js_runtime.handle_scope();
+      let options_v8 =
+        deno_core::serde_v8::to_v8(scope, options.as_json()).unwrap();
+      let bootstrap_fn = self.bootstrap_fn_global.take().unwrap();
+      let bootstrap_fn = v8::Local::new(scope, bootstrap_fn);
+      let undefined = v8::undefined(scope);
+      let name_str: v8::Local<v8::Value> =
+        v8::String::new(scope, &self.name).unwrap().into();
+      let id_str: v8::Local<v8::Value> =
+        v8::String::new(scope, &format!("{}", self.id))
+          .unwrap()
+          .into();
+      bootstrap_fn
+        .call(scope, undefined.into(), &[options_v8, name_str, id_str])
+        .unwrap();
+    }
+    // TODO(bartlomieju): this could be done using V8 API, without calling `execute_script`.
     // Save a reference to function that will start polling for messages
     // from a worker host; it will be called after the user code is loaded.
     let script = r#"
diff --git a/runtime/worker.rs b/runtime/worker.rs
index d1998cd881..908f1a7ab0 100644
--- a/runtime/worker.rs
+++ b/runtime/worker.rs
@@ -14,7 +14,6 @@ use deno_cache::SqliteBackedCache;
 use deno_core::error::AnyError;
 use deno_core::error::JsError;
 use deno_core::futures::Future;
-use deno_core::located_script_name;
 use deno_core::v8;
 use deno_core::CompiledWasmModuleStore;
 use deno_core::Extension;
@@ -66,6 +65,7 @@ pub struct MainWorker {
   should_break_on_first_statement: bool,
   should_wait_for_inspector_session: bool,
   exit_code: ExitCode,
+  bootstrap_fn_global: Option<v8::Global<v8::Function>>,
 }
 
 pub struct WorkerOptions {
@@ -318,20 +318,45 @@ impl MainWorker {
       op_state.borrow_mut().put(inspector);
     }
 
+    let bootstrap_fn_global = {
+      let context = js_runtime.global_context();
+      let scope = &mut js_runtime.handle_scope();
+      let context_local = v8::Local::new(scope, context);
+      let global_obj = context_local.global(scope);
+      let bootstrap_str = v8::String::new(scope, "bootstrap").unwrap();
+      let bootstrap_ns: v8::Local<v8::Object> = global_obj
+        .get(scope, bootstrap_str.into())
+        .unwrap()
+        .try_into()
+        .unwrap();
+      let main_runtime_str = v8::String::new(scope, "mainRuntime").unwrap();
+      let bootstrap_fn =
+        bootstrap_ns.get(scope, main_runtime_str.into()).unwrap();
+      let bootstrap_fn =
+        v8::Local::<v8::Function>::try_from(bootstrap_fn).unwrap();
+      v8::Global::new(scope, bootstrap_fn)
+    };
+
     Self {
       js_runtime,
       should_break_on_first_statement: options.should_break_on_first_statement,
       should_wait_for_inspector_session: options
         .should_wait_for_inspector_session,
       exit_code,
+      bootstrap_fn_global: Some(bootstrap_fn_global),
     }
   }
 
   pub fn bootstrap(&mut self, options: &BootstrapOptions) {
-    let script = format!("bootstrap.mainRuntime({})", options.as_json());
-    self
-      .execute_script(&located_script_name!(), &script)
-      .expect("Failed to execute bootstrap script");
+    let scope = &mut self.js_runtime.handle_scope();
+    let options_v8 =
+      deno_core::serde_v8::to_v8(scope, options.as_json()).unwrap();
+    let bootstrap_fn = self.bootstrap_fn_global.take().unwrap();
+    let bootstrap_fn = v8::Local::new(scope, bootstrap_fn);
+    let undefined = v8::undefined(scope);
+    bootstrap_fn
+      .call(scope, undefined.into(), &[options_v8])
+      .unwrap();
   }
 
   /// See [JsRuntime::execute_script](deno_core::JsRuntime::execute_script)
diff --git a/runtime/worker_bootstrap.rs b/runtime/worker_bootstrap.rs
index 5563b6eadb..12abceca65 100644
--- a/runtime/worker_bootstrap.rs
+++ b/runtime/worker_bootstrap.rs
@@ -58,8 +58,8 @@ impl Default for BootstrapOptions {
 }
 
 impl BootstrapOptions {
-  pub fn as_json(&self) -> String {
-    let payload = json!({
+  pub fn as_json(&self) -> serde_json::Value {
+    json!({
       // Shared bootstrap args
       "args": self.args,
       "cpuCount": self.cpu_count,
@@ -80,7 +80,6 @@ impl BootstrapOptions {
       "v8Version": deno_core::v8_version(),
       "userAgent": self.user_agent,
       "inspectFlag": self.inspect,
-    });
-    serde_json::to_string_pretty(&payload).unwrap()
+    })
   }
 }