Джаваскриптеры давно мечтают о безопасной изоляции кода, чтобы можно было выполнить сторонний скрипт или библиотеку в своём песочном замке, без риска повредить глобальные объекты или залезть друг другу в прототипы. Сейчас для этого есть костыли, либо создавать скрытый <iframe> (у которого свой глобальный контекст), либо городить сложные рентаймы. В Node.js есть модуль vm и контексты, но и они далеки от идеала. Но на горизонте замаячило штатное решение от TC39, ShadowRealm API. От названия веет чем-то мистическим, но по сути это просто способ создать новый глобальный JavaScript-контекст в рамках текущего потока, и исполнить в нём код изолированно от основного.
Сразу оговорюсь, что ShadowRealm пока ещё проект из будущего. Это предложение для стандарта ECMAScript, которое находится примерно на Stage 3 (близко к финалу, но формально не утверждено). Реализации ещё нет в большинстве движков, возможно, за флагом в V8 или SpiderMonkey что-то пробовали, но массово в браузерах API недоступно. Тем не менее, спецификация уже достаточно чёткая, и некоторые платформы готовятся его внедрить. Например, Node.js обсуждает интеграцию ShadowRealm как альтернативу vm.Module. Также существует полифилл на npm.
Зачем нужен ShadowRealm? Подключены десятки библиотек, плюс грузятся плагины или скрипты от сторонних команд. Все они работают в одном окне, разделяя единственный глобальный объект window. Один скрипт может нечаянно (а то и намеренно) изменить что-то глобальное, например, добавить window.$ или замонкепатчить Array.prototype.sort. В итоге компоненты начинают мешать друг другу, нарушая целостность системы. ShadowRealm призван решить эту проблему.
Он позволяет создать новый контекст выполнения JavaScript с собственным глобальным объектом и встроенными классами, полностью отделённый от основного окружения. Код в ShadowRealm не пачкает глобальные объекты внешнего мира и не может наблюдать посторонние изменения. Каждая такая тень имеет свой набор примитивов (Array, Object, и т.д.), никак не связанный с реальными объектами из окна браузера. Это значит, что, например, Array внутри ShadowRealm совсем другой конструктор, не равный Array с вашей страницы.
ShadowRealm не запускает код в отдельном потоке. В отличие от Web Worker, тут нет многопоточности, всё выполняется синхронно в том же потоке, что и основной скрипт. ShadowRealm скорее похож на eval, можно выполнить фрагмент кода как бы в другом измерении, и он мгновенно вернёт результат. Почему же тогда не использовать просто eval? Фича в том, что ShadowRealm даёт чистый глобальный контекст. Обычный eval выполняет код либо в глобале странице, либо в локальной области с какими нибудь ограничениями. А ShadowRealm это совершенно новый глобал, без ваших переменных, без модифицированных прототипов, без DOM. И никакого доступа к объектам основного мира, кроме специально переданных.
API минималистичен. В спецификации всего два метода у ShadowRealm: синхронный .evaluate() и асинхронный .importValue(). Ну и сам конструктор new ShadowRealm().
new ShadowRealm()создаёт новый реалм. Вызывается как конструктор без аргументов. Если попробовать вызвать безnew, будет ошибка. При создании движок поднимает новый Realm Record, по сути, инициализирует новый глобальный объект со всеми стандартными свойствами. Никаких веб API там нет, только чистый JS. В браузере внутрь ShadowRealm не протекают даже такие вещи какwindowилиdocumen,у него нет DOM, нетwindow.locationи других неудаляемых свойств, которые есть в обычных iframe. Глобал ShadowRealm сам по себе простой объект, и все его свойства можно переопределять или удалять.-
shadowRealm.evaluate(sourceText)выполняет переданный строковый код в новом реалме и возвращает результат синхронно. Код выполняется как скрипт,но все глобальные переменные и функции из этого кода остаются внутри ShadowRealm. Снаружи получим только возвращённое значение. Пример:const realm = new ShadowRealm(); let result = realm.evaluate('1 + 2'); console.log(result); // 3Если внутри
evaluateопределим глобальную переменную, скажемglobalThis.x = 42, то снаружи в основном мире никакогоxне появится, ведьglobalThisвнутри realm другой. Аналогично, если вы присвоите в основном миреglobalThis.y = 100, то внутри ShadowRealmglobalThis.yбудетundefined. Изоляция полная в обе стороны.Возвращаемое значение. ShadowRealm умеет отдавать наружу только примитивы или функции. Если код внутри
evaluateвернул объект, например, массив или{}, то случится ошибка, броситсяTypeErrorс сообщением вроде “value passing between realms must be callable or primitive”. Все это из-за того, что объекты неразрывно связаны со своим глобальным контекстом. Передав наружу объект из ShadowRealm, мы бы дали доступ к чужим прототипам, а через них потенциально к глобалу того реалма. Так что пришли к тому, что не нужно никаких объектов через границу, только простые данные. Зато функции можно, о них позже. А вот если внутри произошла ошибка и она не перехвачена, снаружи вы тоже не получите оригинальный объект ошибки. Вместо этого.evaluateвыбросит новыйTypeErrorв вашем (внешнем) мире.Метод
.evaluateподчиняется Content Security Policy так же, как обычныйeval. То есть если у страницы стоит жёсткий CSP, запрещающий, то иshadowRealm.evaluateработать не будет, браузер его заблокирует. shadowRealm.importValue(specifier, exportName)загружает ES-модуль по указанному пути (как динамическийimport()), но в контексте ShadowRealm, и возвращает из него определённый экспорт. Возвращается промис, который резолвится примитивом или функцией. Грубо говоря,.importValue('module.js', 'foo')делает, то что в ShadowRealm подгружается модульmodule.js, там берётся экспортfoo, и этот экспорт передаётся наружу. Еслиfooне найден, получим ошибку. Это единственный способ импортировать в ShadowRealm сложный код, не лепя огромную строку дляevaluate. можно загрузить туда большую библиотеку, которая написана как модуль, и получить оттуда функцию для вызова. В отличие от дефолтного динамическогоimport(), здесь вам не дают сам модуль или его объект-namespace,ведь объекты не передаются. Надо запросить конкретный экспорт. Причём если модуль экспортирует функцию, вы получите обертку-функцию, вызывая которую из основного мира, вы на самом деле запускаете оригинальную функцию внутри ShadowRealm. Если экспорт примитив, он просто копируется. МетодimportValueработает асинхронно, так как может потребоваться загрузить ресурс с сервера (он подчиняется тем же CSP и политикеdefault-src, т.е нельзя подгрузить модуль с домена, не разрешённого контент-политикой страницы.
Теперь о передаче данных и функций между реалмами. Как уже сказал, простые типы передаются как есть. А вот объекты запрещены, попытка вернуть объект или прокинуть объект в качестве аргумента функции через границу вызовет исключение. Если вдруг ну очень надо поделиться структурированными данными, можно передать строку или, например, массив чисел сериализовать, но напрямую нет. Зато функции передаются! Но не напрямую, а через обёртки.
Если код внутри ShadowRealm возвращает функцию, то .evaluate вернёт вам специальный объект, обёрнутую функцию. Можно вызывать его, как обычную джс-функцию, но физически её тело выполнится внутри ShadowRealm, где она была определена. Аналогично, если вы передаёте функцию как аргумент внутрь, движок завернёт вашу функцию в обёртку и передаст в ShadowRealm. Реалмы обмениваются не самими функциями, а такими прокси-обёртками, которые безопасно маршрутизируют вызовы.
Что значит безопасно в данном случае? Во-первых, никаких других свойств или прототипов обёрнутой функции снаружи не видно. Во-вторых, при вызове такой функции-посредника все аргументы автоматом копируются по тем же правилам. В-третьих, если обёрнутая функция кинет исключение там внутри, то снаружи это выльется в новый TypeError, как и для .evaluate.
Поначалу кажется, что это очень большие ограничения. Но на практике этого достаточно для многих случаев. Кейсы, которые обсуждаются для ShadowRealm:
Изолированное выполнение сторонних скриптов.
Виртуализация DOM и внешних API.
Изоляция логики, требующей безопасности.
Изоляция тестов. Есть идея запускать тесты в ShadowRealm, чтобы каждый тест получал чистое окружение, сброшенное до начального состояния JS, но без создания новых процессов. Можно было бы импортировать модуль в ShadowRealm, потестировать его в вакууме, потом уничтожить realm и никаких глобальных побочек.
Отдельно стоит сравнить ShadowRealm с другими подходами:
Ифрейм. Обмен с iframe гораздо тяжелее: пост-месседжи (или SharedArrayBuffer, если надо быстро) да ещё и в виде сериализуемых структур, сложности с загрузкой. ShadowRealm создаётся мгновенно и общается синхронно, но в том же потоке.
Web Worker. Если нужно именно распределить нагрузку по ядрам, вам к воркерам. Кстати, никто не мешает создать ShadowRealm внутри Worker-а, это разрешено. Можно иметь много уровней.
Node VM / compartments. В Node.js модуль
vmпозволяет создать контекст с отдельнымglobal, но при этом стандартные объекты (Arrayetc.) он разделяет. ShadowRealm более жёстко отделён, у него всегда свои intrinsics. Также Node VM может разделять объекты, ShadowRealm нет, только функции.
Напоследок посмотрим простой пример использования. Представим, у нас есть модуль helper.js с функцией, которую мы хотим выполнять изолированно. Мы можем сделать так:
// helper.js (ES Module)
export function secretCalc(a, b) {
// эта функция, допустим, полагается на какой-то глобал,
// но мы хотим контролировать окружение...
if (globalThis.myFlag) {
throw new Error("Should not see main page global!");
}
return a + b;
}
Теперь основной код:
const realm = new ShadowRealm();
// Импортируем функцию secretCalc из модуля в ShadowRealm
const calc = await realm.importValue('./helper.js', 'secretCalc');
// Вызываем функцию, она выполняется в ShadowRealm
let result = calc(10, 20);
console.log(result); // 30
// Попробуем показать, что глобалы изолированы:
globalThis.myFlag = true;
try {
calc(1, 2);
} catch (e) {
console.log("Ошибка:", e.message);
}
// -> Ошибка: Error encountered during evaluation
Загрузили модуль внутри realm. Переменная calc обёрнутая функция, связанная с secretCalc из изолированного мира. Первый вызов calc(10, 20) успешно вернул 30. Затем мы установили некий глобал myFlag в основном мире. Внутри ShadowRealm его по-прежнему нет, поэтому при втором вызове calc функция secretCalc не увидит globalThis.myFlag и не упадёт по той причине, что предусматривала (у нас упал по другой, я для примера бросил Error внутри, чтобы выловить поведение). Брошенная внутри ошибка превратится снаружи в TypeError без оригинального текста (мы видим лишь стандартное сообщение). ShadowRealm спрятал детали внутренней ошибки, чтобы не засветить реалмовские объекты (в том числе сам объект ошибки, у которого свой прототип).
Как видите, интерфейс довольно декларативный. Чаще всего вы будете делать realm.evaluate для простых случаев (например, выполнить маленький скрипт) или realm.importValue для загрузки больших модулей.
Конечно же ShadowRealm не заменит Worker там, где нужен параллелизм, но для задач безопасности и модульности это шикарный инструмент.
Остаётся дождаться, когда эта спецификация станет стандартом (надеемся, скоро) и появятся реализации. Возможно, в 2026 году мы увидим поддержку в V8 и других движков. Пока же можно поэкспериментировать с полифиллом.
ShadowRealm ещё развивается, так что следите за обновлениями стандартов ECMAScript , впереди много интересного!
ShadowRealm — хороший пример того, как JS-экосистема уходит от «магии» к управляемым средам исполнения. Курс JavaScript Developer. Basic как раз про такую осознанную разработку: вы с нуля разберетесь в модулях, окружении и инструментальной цепочке, освоите React, TypeScript, Node.js и научитесь уверенно работать с современным фронтендом.
Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:
27 ноября: «Drag&Drop-конструктор интерфейсов на чистом JavaScript». Записаться
9 декабря: «Каталог данных с фильтрацией и поиском на чистом JavaScript». Записаться
22 декабря: «Создаем свой html-тег». Записаться