Аргументы воркеров в Node.js и на что они влияют
- пятница, 31 октября 2025 г. в 00:00:05
В предыдущей статье мы подробно рассмотрели такую фичу JS как веб-воркеры, позволяющей запускать код в фоновом потоке браузера. В этой и последующих статьях, мы рассмотрим воркеры в серверном JavaScript, где они существуют с 10-й версии Node.js В целом механизм идентичен браузерной версии языка. Взаимодействие потоков, всё также основано на обмене сообщениями, за создание воркеров отвечает класс Worker. Конкретно данная статья подробно описывает аргументы принимаемые конструктором класса Worker и то как они влияют на работу воркеров.
Принцип создания воркеров в Node.js не отличается от среды в браузере. Всё также первым аргументом передаётся путь к файлу, код которого требуется запустить в фоновом потоке. Всё также вторым аргументом принимается объект настроек. Разве что этот самый объект настроек обладает куда большим числом опций. Рассмотрим далее их подробнее.
Argument vector - массив аргументов командной строки:
import {Worker, MessageChannel} from 'worker_threads'
const {port1, port2} = new MessageChannel()
const worker = new Worker('./worker.js', {
    argv: [
        '--input', 'large_dataset.csv',
        '--format', 'json',
        '--chunk-size', '1000',
        '--prod'
    ]
})Внутри воркера они хранятся в свойстве process.argv:
console.log(process.argv.slice(2)) // Пропускаем первые 2 элемента, так как в них хранится путь к файлу запущенному в главном потоке и в потоке воркера. Остальное это массив переданных аргументов командной строкиАргументы могу переданные argv не могут сами по себе повлиять на работу воркера и чтобы они возымели какой-то эффект, нужно их распарсить и обработать в коде:
import minimist from 'minimist';
import ConfigBuilder from './ConfigBuilder.js'
const args = minimist(process.argv.slice(2))
const ConfigBuilderInst = new ConfigBuilder()
if (args.prod) { // Формируем конфигурацию в зависимости от того передан аргумент --prod или нет
    ConfigBuilderInst.makeProdCnf()
} else {
    ConfigBuilderInst.makeDevCnf()
}Повлиять на ход работы скрипта через аргументы командной строки можно используя опцию execArgv, но о ней ниже.
Нужно эмулировать CLI;
Требуется интеграция с CLI-библиотеками;
Передать несложные параметры конфигурации;
Обеспечить совместимость с bash-скриптами.
Устанавливает переменные окружения:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    env: {
        ENV: 'prod',
        API_KEY: 'https://api.vasyatka.ru',
        DATABASE_URL: 'postgres://vasyatka:qwerty123@localhost:5432'
    }
})В воркере они доступны в свойстве process.env:
console.log(process.env.ENV) // prod
console.log(process.env.API_KEY) // https://api.vasyatka.ru
console.log(process.env.DATABASE_URL) // postgres://vasyatka:qwerty123@localhost:5432По умолчанию воркеры наследуют все переменные окружения родительского потока. Но если явно указать из в параметре env, то в воркере будут доступны только они, а наследование родительских переменных отменяется. Поэтому если требуется передать новые переменные окружения в воркер + сохранить наследование родительских, для этого стоит использовать деструктуризацию свойства:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    env: {
        ...process.env, // Явно передаём все родительские переменные
        ENV: 'prod',
        API_KEY: 'https://api.vasyatka.ru',
        DATABASE_URL: 'postgres://vasyatka:qwerty123@localhost:5432'
    }
})Параметр env подходит только для передачи простых данных, так как все значения в нём преобразуются в строки. Учтите что большое количество, передаваемых переменных окружения, может повлиять на скорость создания воркера.
Опция делающая переменные окружения общими для родительского потока и потока воркера. Без этой опции воркер наследует переменные окружения родителя, получая их копии, а если её включить то они становятся общими для обоих потоков и изменения в одном потоке, практически сразу станут видны в другом:
import {Worker, SHARE_ENV} from 'worker_threads'
const worker = new Worker('./worker.js', {env: SHARE_ENV})
process.env.key = 'qwerty123' // В воркере это свойство с этим-же значением будет доступно в этом-же свойстве: process.env.keyОперация по изменению переменной окружения, в одном из потоков не является атомарной и нарушает главный принцип многопоточки в JavaScript: жёсткую изоляцию потоков друг от друга. Это может привести потоки в состояние гонки, из-за которого будут возникать сложнодиагностируемые баги. Поэтому хорошо всё взвести, прежде чем устанавливать такое значение для свойства env. Ещё SHARE_ENV негативно влияет на производительность, из-за необходимости синхронизировать доступ к переменным окружения, между потоками.
Если равно true, то в первом аргументе вместо пути к файлу, нужно передавать JS код, в формате строки. Данный код будет выполнен в потоке воркера:
import { Worker} from 'worker_threads'
const worker = new Worker('console.log("Hello from worker!")', {eval: true})Код выполняемый внутри eval, работает в изолированной среде, но при этом он будет иметь доступ к модулям Node.js
import {Worker} from 'worker_threads'
const worker = new Worker(`
    const fs = require('fs')
    let file = fs.readFileSync("my_file.txt", {encoding: "utf-8"})
    console.log(file)`,
    {eval: true}
)По сути данное свойство позволяет реализовать многопоточную версию функции eval(). Но данное свойство, наследует от своей родительской функции, не только логику работы, но и дурную репутацию, связанную с ворохом проблем с безопасностью и производительностью. Поэтому стоит 10 раз подумать, прежде чем использовать это свойство в своём коде.
Для самых простых задач;
Прототипирование;
Динамическая генерация кода.
Список аргументов из командной строки для движка V8. Внутри воркера данный список хранится в свойстве process.execArgv. По умолчанию наследует значение, родительского потока.
execArgv отличается от упомянутой выше опции argv, тем что работает только с аргументами движка Node.js и если попытаться передать ему что-то ещё это приведёт к ошибке. Так-же список того что можно передать в execArgv зависит от версии Node, в то время как флаги передаваемые в argv, ограничены только фантазией разработчика.
Если равно true, то через свойство объекта воркера stdin, отправлять данные ввод дочернего потока, где они будут доступны в свойстве process.stdin:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {stdin: true})
process.stdin.pipe(worker.stdin) // Передаём ввод главного потока в воркерТеперь получим и выведем ввод из главного потока в воркере:
process.stdin.on('data', (data) => {
    console.log('Воркер получил ввод главного потока:', data.toString()) // Ввод получается в виде буффера, поэтому переконвертируем его в строку
})По умолчанию, данная функция отключена, но может пригодиться для создания интерактивных приложений с большой нагрузкой, которую можно будет распределить на воркеры.
Если равно true, вывод дочернего потока, не будет автоматически выводиться, в вывод родительского потока. Значение по умолчанию false. То есть вывод метода console.log(), по умолчанию идёт в консоль, точно так-же как будто бы он работает в главном потоке. Если поменять значение на true, то вывода в консоли мы не увидим, но сообщение будет доступно в главном потоке, через свойство объекта воркера stdout:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {stdout: true})
worker.stdout.on('data', (data) => {
    console.log('Воркер хочет сказать: ' + data.toString()) // Перехватываем вывод из воркера. Вывод поступает в виде буффера
})В воркере сделаем вывод в консоль:
console.log('Привет из воркера!') // Ничего не выведетАналогичное поведение потоков вывода, есть и у дочерних процессов, но о них как-нибудь в другой раз.
Если равно true, то сообщение об ошибке из дочернего потока, не будут автоматически переходить на родительский поток. Значение по умолчанию false. То есть вывод метода console.error(), по умолчанию идёт в консоль, точно так-же как будто бы он работает в главном потоке. Если поменять значение на true, то вывода в консоли мы не увидим:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {stderr: true})Попробуем вывести сообщение об ошибке в консоль:
console.error('Всё сломалось!') // В консоли ничего не увидимПолучить данные в главном потоке можно только через специальный обработчик события:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {stderr: true})
worker.stderr.on('data', (data) => {
    console.log('Воркер хочет сказать: ' + data.toString()) // Воркер хочет сказать: Всё сломалось!
})Так-же stderr влияет на варнинги:
console.warn('Нужно быть осторожнее!')Перехват варнингов ничем не отличается от перехвата ошибок.
Используется для передачи объектов в дочерний поток, через алгоритм структурного клонирования:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    workerData: {
        message: 'Как дела у вас там в воркере?'
    }
})В воркере данные доступны объекте workerData:
import {workerData} from 'worker_threads'
console.log(workerData.message) // Как дела у вас там в воркере?Таким образом можно отправить в воркер данные необходимые для его инициализации, в обход классического механизма обмена сообщениями.
Из-за ограничений связанных с алгоритмом структурного клонирования, через workerData невозможно передавать:
Функции;
Классы;
Циклические ссылки;
Специфические объекты среды Node.js, например сокеты.
Попытка пренебречь этим правилом приведёт к ошибке:
import {Worker} from 'worker_threads'
function greeting() {
    console.log('Привет мир!')
}
const worker = new Worker('./worker.js', {
    workerData: {
        funch: greeting // Получаем ошибку: DOMException [DataCloneError]: function greeting() could not be cloned
    }
})Так как workerData передаётся всего 1 раз при создании воркера он отлично подходит для передачи конфигураций и любых других данных которые требуется передать в воркер всего 1 раз в самом начале его работы:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    workerData: { // Передаём конфигурацию в воркер
        isProd: false,
        priority: 'normal'
    }
})Для передачи больших объектов, так как их обработка через алгоритм структурного клонирования может привести к серьёзному снижению производительности. Для этого гораздо лучше подходит SharedArrayBuffer;
Для данных которые вы планируете изменять и возвращать обратно в главный поток. Для таких вещей гораздо лучше подходит механизм объёмна сообщениями postMessage.
Если равно true, то воркер будет автоматически закрывать все файлы и сокеты, которые были открыты в ходе работы его скрипта.
Файловый дескриптор - это низкоуровневый идентификатор, в ОС для отслеживания открытых файлов, сокетов и других ресурсов ввода/вывода.
Свойство автоматически наследуется всеми дочерними воркерами, открытыми внутри другого воркера. Значение по умолчанию true. Важно понимать что данный параметр влияет только на файловые дескрипторы открытые в воркере.
Значение по умолчанию, когда Node.js автоматически закрывать, все забытые файловые дескрипторы, очень удобно и повышает надёжность приложения, защищая от утечек памяти. Зачем тут вообще что-то менять? Наверное в 95% случаев это действительно так! Но бывают ситуации когда это может пригодиться. В основном они связаны с экономией ресурсов или безопасностью.
Например если начать работу с файлом в воркере, то когда он закончит работу, файловый дескриптор будет закрыт. Но если нужно, чтобы главный поток, продолжил работу с этим файлом, сохранив блокировку и его состояние. Тут пригодится передача файлового дескриптора между потоками:
import {parentPort} from 'worker_threads'
import {writeSync} from 'fs'
import {openSync} from 'fs'
const fileDescriptor = openSync('secondWorker.js', 'w+')
writeSync(fileDescriptor , 'Воркер пишет в файл\n')
parentPort.postMessage({fileDescriptor: fileDescriptor}) // Передаём файловый дискриптор в главный потокПродолжим работу с файлом в главном потоке:
import {Worker} from 'worker_threads'
import {writeSync} from 'fs'
const worker = new Worker('./worker.js', {
    trackUnmanagedFds: false
})
worker.on('message', (message) => {
    writeSync(message.fileDescriptor, 'Главный поток пишет в файл, после воркера\n')
})Если не изменить опцию trackUnmanagedFds на false, то возникнет ошибка: Error: EBADF: bad file descriptor, write.
Если кратко резюмировать то менять trackUnmanagedFds на false можно если:
Нужно контролироваться жизненный цикл дескриптора;
Нужно сохранять на блокировку файла, обеспечивая атомарность записи из нескольких потоков;
Критически важна производительность, можно передавать дескрипторы между потоками, экономя ресурсы на открытии новых.
Позволяет предавать между потоками такие объекты как: сокеты, порты или типизированные массивы, в обход алгоритма структурного клонирования. Данная опция работает только в связке, с упомянутой выше опцией workerData и подходит для работы лишь с некоторыми специфическими типами данных.
Из-за своей архитектуры Node.js не может безопасно передавать все самые распространённые в JavaScript типы данных, такие как обычные строки, объекты и массивы между потоками. Их можно скопировать через алгоритм структурного клонирования, но не передать управление над ними из одного потока в другой.
Объекты готовые к передаче должны быть:
Самостоятельными - не должны содержать ссылок на другие объекты;
Изолированными - существовать независимо от контекста выполнения;
Иметь фиксированный размер в памяти;
Не иметь скрытого состояния и быть предсказуемыми.
Далее приведём примеры передачи различных типов данных через transferList.
ArrayBuffer - это сырая, неструктурированная память, хранящая только бинарные данные. Именно в силу свой не структурированности она отлично подходит для передачи управления её между потоками:
import {Worker} from 'worker_threads'
const buffer = new ArrayBuffer(1024 * 1024 * 100)
const bufferView = new Uint32Array(buffer)
const worker = new Worker('./worker.js', {
    workerData: {buffer: buffer},
    transferList: [buffer] // Передаем упраление над ArrayBuffer воркеру
})
console.log(bufferView) // Пусто, мы полность передали данные в воркер, в главном потоке они больше недоступныТеперь получим ArrayBuffer в воркере:
import {workerData} from 'worker_threads'
const bufferView = new Uint32Array(workerData.buffer)
console.log(bufferView) // Теперь ArrayBuffer тутПередача проходит максимально быстро и с минимальным затратам ресурсов на данную операцию;
Сразу после передачи исходный буфер становится пустым;
Передавать можно сразу несколько буферов;
ArrayBuffer идеально подходит для передачи больших объёмов информации в бинарном виде, например мультимедиа или научных данных.
Передача MessagePort через transferList может пригодиться для организации каналов обмена сообщениями между потоками:
import {Worker, MessageChannel} from 'worker_threads'
const {port1, port2} = new MessageChannel()
const worker = new Worker('./worker.js', {
    workerData: {port: port2},
    transferList: [port2]
})
port1.on('message', (msg) => {
    console.log('Главный поток получил сообщение:', msg)
})В воркере формируем сообщение и отправляем его в главный поток:
import {workerData, parentPort} from 'worker_threads'
const port = workerData.port
port.postMessage('Привет из воркера!')Производительность - так как данные не копируются из одного потока в другой, а просто происходит передача управления над портом, данная операция расходует минимум ресурсов, что делает её крайне привлекательной в ситуациях, когда реализация приложения ограничена по ресурсам;
Высокая скорость соединения - канал созданный через MessageChannel() позволяет быстро обмениваться данными между потоками;
Масштабируемость  и гибкость - MessageChannel() позволяет легко создавать новые каналы связи, когда это нужно, в том числе и динамически;
Изоляция - каналы сообщений позволяют контролировать доступ к компонентам.
FileHandle если переводить на русский, это уже упоминавшийся выше файловый дескриптор и его тоже можно передавать через transferList:
import {Worker} from 'worker_threads'
import {open} from 'fs/promises'
const fileHandle = await open('secondWorker.js', 'w+')
await fileHandle.writeFile('Сначала главынй поток что-то пишет в воркер\n') // Обязательно делаем запись синхронной, Node не даст передать файловый дискриптор в воркер, пока он используется
const worker = new Worker('./worker.js', {
   workerData: {fileHandle: fileHandle},
   transferList: [fileHandle]
})Теперь примем файловый дескриптор в воркере и запишем в него что-нибудь:
import {workerData, parentPort} from 'worker_threads'
const fileHandle = workerData.fileHandle
fileHandle.writeFile('Ворке что-то пишет в файл!\n')В примере выше важно использовать модуль fs/promises, методы более стандартного fs, возвращают файловый дескриптор в виде числа, а числа невозможно передать через transferList.
Есть ограничения по ресурсам, то передавая файловые дескрипторы между потоками, можно их сэкономить;
Если необходимо сохранить позицию записи чтения/записи в файле, для воркера;
Необходимо реализовать сложный конвейер обработки файлов.
Устанавливает лимит ресурсов, потребляемых воркером. При превышении лимита, работа воркера завершается. Данный параметр полезен для избежание утечек памяти, при работе с воркерами. Но даже если он не установлен, то воркер всё равно может завершиться из-за глобальной нехватки памяти на сервере.
Движок V8 делит объекты в зависимости от срока их жизни в памяти на:
Старое поколение - долгоживущие объекты, переживающие несколько сборок мусора;
Молодое поколение - объекты с коротким жизненным циклом, удаляемые быстрым сборщиком мусора.
Примером молодого объекта может быть, объект созданный в цикле, который будет удалён сборщиком мусора, сразу после того как цикл завершит последнюю итерацию:
function getEveNumObj() {
    let arEvenNumObj = []
    for (let i = 0; i < 1000; i++) {
        let yongObj = {index: i, square: i ** 2} // Молодой объект, так как он существует только в этом цикле
        let isEvenNum = !(yongObj.square % 2)
        if (isEvenNum) {
            arEvenNumObj.push(yongObj)
        }
    }
    return arEvenNumObj
}На молодое поколение V8 выделяет совсем немного памяти от 1 до 16 мегабайт;
Сборщик мусора максимально быстро и часто очищает данную память;
Здесь хранятся только короткоживущие объекты.
Примером старого поколения может послужить, класс инициирующий запуск нашего JavaScript приложения:
import MyApp from 'myApp'
const globalApp = new MyApp() // Долгоживущий объектНа него выделяется много памяти, в плоть до нескольких гигабайт. Тут то нам и пригодиться параметр maxOldGenerationSizeMb , способный предотвратить утечку памяти в воркере. Речь о данном свойстве пойдёт ниже.
Сборщик мусора, работает с ним в медленном режиме, очищая его как можно реже.
Теперь когда мы кратко рассмотрели что такое молодое и старое поколения объектов в движке V8, можно более осмыслено рассмотреть некоторые параметры объекта resourceLimits. maxOldGenerationSizeMb, как можно догадаться из названия, ограничивает размер памяти выделяемой на долгоживущие объекты. Именно через такие объекты чаще всего происходят утечки памяти:
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    resourceLimits: {
        maxOldGenerationSizeMb: 10 // Максимум 10MB для долгоживущих объектов
    }
})
// Создаём сообщения с данными вызывающими утечку
setInterval(() => {
    worker.postMessage('Подержи моё пиво!'.repeat(10000))
}, 100)
worker.on('error', (err) => {
    if (err.code === 'ERR_WORKER_OUT_OF_MEMORY') {
        console.log('Воркер превысил лимит старого поколения! Утечка памяти предотвращена')
    }
})Теперь сымитируем утечку памяти в воркере:
import {parentPort} from 'worker_threads'
let memoryLeak = [] // Создаём долгоживущий объект
parentPort.on('message', (data) => {
    memoryLeak.push(data) // Каждое сообщение добвляем в массив создавая утечку памяти
    console.log(`Памяти сожрано: ${process.memoryUsage().heapUsed / 1024 / 1024} MB`)
})В примере выше воркер автоматически завершит работу, с ошибкой ERR_WORKER_OUT_OF_MEMORY, как только расход памяти превысит, указанные нами в maxOldGenerationSizeMb 10 мегабайт.
Ограничивает размер молодого поколения объектов в V8. В отличии от maxOldGenerationSizeMb, данный параметр не вызывает ошибок при превышении. Он является целевым значением, а не жёстким лимитом. Его превышение всего лишь запускает сборку мусора.
Для демонстрации влияния на работу воркера maxYoungGenerationSizeMb придётся написать достаточно сложный по меркам данной статьи бенчмарк. До этого я старался не нагромождать сложные примеры в коде, но тут без этого никак. Главный поток:
import {Worker} from 'worker_threads'
import {performance} from 'perf_hooks'
async function benchmark(youngGenLimit, testName) {
    return new Promise((resolve) => {
        const startTime = performance.now()
        let operations = 0
        const worker = new Worker('./worker.js', {
            resourceLimits: {
                maxYoungGenerationSizeMb: youngGenLimit
            }
        })
        worker.on('message', (msg) => {
            if (msg.type === 'progress') {
                operations = msg.operations;
            } else if (msg.type === 'completed') {
                const endTime = performance.now()
                const duration = endTime - startTime
                const opsPerSecond = operations / (duration / 1000)
                worker.terminate()
                resolve({
                    testName,
                    youngGenLimit,
                    operations,
                    duration: Math.round(duration),
                    opsPerSecond: Math.round(opsPerSecond)
                })
            }
        })
        worker.postMessage('start')
    })
}
async function runBenchmarks() {
    const tests = [ // Разныле лимиты для разных поколений
        {limit: 1, name: 'Очень маленькое (1MB)'},
        {limit: 16, name: 'Маленькое (16MB)'},
        {limit: 64, name: 'Среднее (64MB)'},
        {limit: 256, name: 'Большое (256MB)'}
    ]
    const results = [];
    for (const test of tests) {
        const result = await benchmark(test.limit, test.name)
        results.push(result)
        console.log(`${result.testName}:`)
        console.log(`Операций: ${result.operations}`)
        console.log(`Время: ${result.duration}ms`)
        console.log(`Оп/сек: ${result.opsPerSecond}`)
        await new Promise(resolve => setTimeout(resolve, 1000)) // Пауза между тестами
    }
    console.log('=== ИТОГИ ===')
    results.forEach(result => {
        console.log(`${result.testName}: ${result.opsPerSecond} оп/сек`)
    })
}
runBenchmarks()Воркер нагружающий молодое поколение:
import {parentPort} from 'worker_threads'
function performanceTest() {
    let operations = 0
    const startTime = Date.now()
    const mediumLivedObjects = []
    function processBatch() {
        operations++
        if (mediumLivedObjects.length > 5000) { // Ограничиваем размер массива
            mediumLivedObjects.splice(0, 1000)
        }
        for (let i = 0; i < 100; i++) {
            for (let j = 0; j < 10; j++) {
                const obj = {
                    index: operations * 10 + j,
                    data: 'x'.repeat(100),
                    numbers: new Array(50).fill(Math.random()),
                    timestamp: Date.now()
                }
                mediumLivedObjects.push(obj)
            }
            if (mediumLivedObjects.length > 0) { // Имитируем обработку
                const processed = mediumLivedObjects.slice(-10).map(item => ({
                    ...item,
                    processed: true,
                    result: item.numbers.slice(0, 10).reduce((a, b) => a + b, 0)
                }))
            }
        }
        if (operations % 10 === 0) {
            const memory = process.memoryUsage();
            console.log(
                `Количество операций: ${operations}, Расход памяти: ${Math.round(memory.heapUsed / 1024 / 1024)} MB, Количество объектов: ${mediumLivedObjects.length}`
            )
            parentPort.postMessage({
                type: 'progress',
                operations
            })
        }
        // Условие выхода
        if (Date.now() - startTime < 10000) {
            setImmediate(processBatch);
        } else {
            console.log('Завершаем работу...');
            parentPort.postMessage({
                type: 'completed',
                operations
            })
        }
    }
    console.log('Запускаем тест...');
    processBatch()
}
parentPort.on('message', (msg) => {
    if (msg === 'start') {
        performanceTest()
    }
})Данный бенчмарка поочерёдно создаёт воркеры, с различными лимитами для maxYoungGenerationSizeMb и подсчитывает количество операций в секунду, которые воркер успевает выполнить Результат работы данного бенчмарка следующие:
1MB:  279 оп/сек  (первый тест)
16MB: 346 оп/сек  (+24% от 1MB)
64MB: 396 оп/сек  (+42% от 1MB) 
256MB: 418 оп/сек (+50% от 1MB)Из результатов теста следует: чем больше памяти выделяется на maxYoungGenerationSizeMb, тем больше операций в секунду, воркер выполняет. Но увеличение лимита на maxYoungGenerationSizeMb это не серебряная пуля. Мой бенчмарк, написан чтобы создавать высокое давление, на молодое поколение в памяти. В некоторых случаях может потребоваться наоборот снижение лимита maxYoungGenerationSizeMb, для более интенсивной работе сборщика мусора. На практике 16 мегабайт, выделяемых на молодое поколение в воркере, более чем достаточно для большинства задач.
Быстрое выделение памяти и быстро освобождение;
Предназначена для временных объектов;
Частая сборка мусора;
Небольшой размер. В основанном потоке по умолчанию 64 мегабайта, а в воркере всего 16 мегабайт.
codeRangeSizeMb - это размер памяти выделяемой для JIT-компиляции. Большинству приложений, нет необходимости менять этот параметр, но если нужно запустить код которому нет 100% доверия, то песочница сделанная на основе воркера с ограниченным codeRangeSizeMb станет отличным решением.
Если в ходе работы воркера данный лимит будет превышен, то воркер завершит работу с ошибкой ERR_WORKER_OUT_OF_MEMORY, как и в случае превышения лимита maxOldGenerationSizeMb
import {Worker} from 'worker_threads'
const worker = new Worker('./worker.js', {
    resourceLimits: {
        codeRangeSizeMb: 1
    }
})
worker.postMessage('start')
worker.on('error', (err) => {
    console.error('В воркере произошла ошибка:', err.message)
    if (err.code === 'ERR_WORKER_OUT_OF_MEMORY') {
        console.log('Ошибка вызвана превышением лимита codeRangeSizeMb!')
    }
})В воркере создадим большую нагрузку на JIT:
import {parentPort} from 'worker_threads'
function generateJITCode() {
    const functions = []
    let count = 0
    while (true) {
        count++
        const hugeFunction = createHugeFunction(count) // Создаем ОЧЕНЬ большие функции для JIT-компиляции, но не создаем данных - только код!
        functions.push(hugeFunction)
        hugeFunction()
    }
}
function createHugeFunction(id) {
    // Создаем большую функцию
    let functionBody = 'let result = 0'
    // Добавляем много математических операций для увеличения размера JIT-кода
    for (let i = 0; i < 1000; i++) {
        functionBody += `result += Math.sin(${id} * ${i} + ${Math.random()})
                         result += Math.cos(${id} * ${i * 2} - ${Math.random()})
                         result += Math.tan(${id} * ${i * 3} * ${Math.random()})
                         result += Math.sqrt(Math.abs(${id} * ${i * 4} + ${Math.random()}))
                         result += Math.log(Math.abs(${id} * ${i * 5} + 1))
                         result += Math.exp(${id} * ${i * 6} * 0.001)
                         result += Math.pow(${id}, ${i % 10}) * ${Math.random()}
                         result += Math.atan2(${id} * ${i * 7}, ${id} * ${i * 8} + 1)`
    }
    // Добавляем много условных операторов для усложнения кода
    functionBody += `if (result > 0) {
        ${generateComplexBranching(id, 'positive')}
    } else {
        ${generateComplexBranching(id, 'negative')}
    }
    return result`
    return new Function(functionBody)
}
function generateComplexBranching(id, type) {
    let code = ''
    for (let i = 0; i < 100; i++) {
        code += `if (${id} % ${i + 2} === 0) {
                    result += Math.${type === 'positive' ? 'acos' : 'asin'}(Math.min(1, Math.abs(result)))
                 } else {
                    result -= Math.${type === 'positive' ? 'acosh' : 'asinh'}(Math.max(1, Math.abs(result)))
                 }`
    }
    return code
}
parentPort.on('message', (msg) => {
    if (msg === 'start') {
        generateJITCode()
    }
})Повышает безопасность при выполнении стороннего кода;
Позволяет избежать утечек памяти;
Полезно если ресурсы памяти сильно ограничены;
Устанавливает размер стека воркера в мегабайтах. Слишком маленькое значение сделает воркер, неработоспособным, вызвав ошибку переполнения стека. Значение по умолчанию: 4 мегабайта, а минимальный размер это 1 мегабайт.
Создадим пул воркеров с различными размерами стека:
import {Worker} from 'worker_threads'
const arStackSizes = [1, 2, 4, 8]
arStackSizes.forEach(stackSizeMb => {
    const worker = new Worker('./worker.js', {
        resourceLimits: {stackSizeMb}
    })
    worker.on('message', (msg) => {
        console.log(`Размер стека: ${stackSizeMb}MB`)
        console.log(`Глубина рекурсии: ${msg.depth}`)
        console.log(`В воркере произошла ошибка: ${msg.error.toString()}`)
    })
})В воркере создадим бесконечную рекурсию, вызывающую ошибку:
import {parentPort} from 'worker_threads'
let depth = 0
function recursiveFunction() {
    depth++
    
    recursiveFunction()
}
try {
    recursiveFunction()
} catch (error) {
    parentPort.postMessage({depth, error})
}Запустив данный бенчмарк я получил линейную зависимость! С увеличением объёма памяти выделяемой на стек в 2 раза (2, 4, 8), в 2 раза увеличивалась и глубина рекурсии:
1MB  → 11,716 вызовов
2MB  → 26,280 вызовов  (+14,564)
4MB  → 55,407 вызовов  (+29,127)  
8MB  → 113,661 вызовов (+58,254)Каждый раз работа воркера в примерах выше завершалась ошибкой: Maximum call stack size exceeded.
Нужно экономить памяти из-за большого числа воркеров;
Нужна защита от утечек вызываемых рекурсией;
Требуется контроль за расходом ресурсов, в условиях когда они сильно ограничены.
Имя потока, которое пригодится при отладке или дебагинге. Данное свойство доступно в Node.js начиная с 20.x версии и выше. Доступная имени зависит от ОС, в которой работает скрипт:
 1.  Windows - 32 767 символов;
 2. macOS - 64 символа;
 3. Linux - 16 символов;
 4. NetBSD - лимит устанавливается в свойстве PTHREAD_MAX_NAMELEN_NP;
 5. FreeBSD или OpenBSD - лимит указывается в свойстве MAXCOMLEN. Значение по умолчанию 'WorkerThread'.
Воркеры в Node.js имеют массу настроек, позволяющих их гибко использовать, в зависимости от конкретной ситуации. Большинство из этих настроек не пригодится 99% процентам разработчиков, тем не менее я решил максимально детально описать, так как на русском языке, ничего подобного нет, а официальная документация, на мой взгляд скучна и скупа на примеры и пояснения.