🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Зачем понимать, как работает JavaScript на самом деле
Когда вы пишете JavaScript, вы не просто «заставляете страницу что-то делать». Вы запускаете цепочку процессов, которые начинаются с загрузки исходного текста и заканчиваются изменением DOM, сетевым запросом, записью в память, отрисовкой кадра и иногда неожиданной ошибкой в консоли. Понимание того, как работает JavaScript, превращает язык из набора команд в предсказуемый механизм, который можно диагностировать и ускорять.
Самая частая причина «магии» в JavaScript — разрыв между тем, что программист думает о выполнении кода, и тем, как реально работает движок и среда выполнения. Этот разрыв особенно заметен, когда вы сталкиваетесь с асинхронностью, очередями задач, оптимизациями JIT, сборкой мусора и особенностями браузерного рендеринга.
Что значит «как работает JavaScript» — от строки кода до результата
Фраза «как работает JavaScript» включает несколько уровней:
- Как устроен язык и его правила — синтаксис, типы, область видимости, семантика выражений.
- Как движок превращает текст в выполняемые инструкции — парсинг, байткод, JIT-компиляция.
- Как среда выполнения связывает код с миром — DOM, сеть, таймеры, файлы, потоки, события.
- Как выстраивается порядок выполнения — стек вызовов, очереди задач, микрозадачи.
- Как управляется память — выделение, удержание ссылок, сборка мусора, утечки.
Какие проблемы решает понимание внутренностей
Когда вы понимаете внутреннюю механику, вы перестаёте «гадать» и начинаете вычислять причины. Типовые проблемы, которые становятся понятными:
- Неожиданный порядок выполнения — почему один лог печатается раньше другого, хотя «написано ниже».
- Подвисания интерфейса — почему на слабых устройствах кнопки реагируют с задержкой 200–800 мс.
- Тормоза анимаций — почему цель 60 FPS требует укладываться примерно в 16,7 мс на кадр, и где JavaScript ворует время.
- Сложные баги из-за состояния — гонки данных, потеря актуальности, дубли запросов, повторные обработчики.
- Утечки памяти — рост потребления RAM на 50–300 МБ за несколько минут из-за «висящих» ссылок и слушателей событий.
- Нестабильная производительность — код то быстрый, то внезапно медленный из-за деоптимизации.
- Неожиданные ограничения браузера — безопасность, CORS, доступ к файлам, политика источников.
Кому это особенно полезно
Эта тема одинаково важна для разных ролей, потому что JavaScript сегодня — не только фронтенд.
- Новичкам — чтобы не заучивать «магические правила», а понимать причину поведения кода.
- Фронтенд-разработчикам — чтобы писать быстрые интерфейсы, не блокировать главный поток и правильно работать с DOM.
- Бэкенд-разработчикам на Node.js — чтобы понимать I/O, событийную модель, нагрузку и масштабирование.
- Тимлидам и архитекторам — чтобы выбирать решения по производительности и надежности, а не по привычке.
- Тем, кто готовится к собеседованию — чтобы уверенно отвечать про движок, рантайм, event loop и оптимизацию.
Короткая ментальная модель JavaScript — что за чем стоит
У JavaScript есть важная особенность: язык один, а поведение вокруг него зависит от окружения. Чтобы не путаться, удобно держать в голове простую модель из трёх уровней — язык, движок и рантайм. Плюс отдельным «дирижёром» выступает event loop, который управляет очередями задач.
JavaScript как язык — синтаксис и семантика стандарта ECMAScript
JavaScript как язык определяется стандартом ECMAScript. Стандарт описывает, как должны работать выражения, типы, операторы, функции, объекты, прототипы, классы, модули, промисы и многое другое. Это важно: стандарт описывает язык, но не описывает конкретные браузерные функции вроде работы с DOM или сети.
Отсюда возникает ключевое разделение:
- ECMAScript — «чистый язык» и его базовые встроенные объекты вроде Array, Map, Promise, Date, RegExp.
- Web API в браузере — DOM, fetch, setTimeout, localStorage, Canvas, WebSocket и другие интерфейсы.
- Системные API в Node.js — fs, net, http, streams, process и так далее.
Если коротко, ECMAScript отвечает на вопрос «как работает язык», а рантайм отвечает на вопрос «что этот язык может делать в конкретной среде».
Движок JavaScript — парсинг, байткод, JIT, оптимизация и сборка мусора
Движок JavaScript — это программа, которая выполняет ваш код. У него есть несколько задач, которые часто идут по цепочке:
- Разобрать исходный текст и построить внутреннее представление.
- Превратить это представление в инструкции, которые можно исполнять быстро.
- Оптимизировать горячие участки кода, которые выполняются много раз.
- Управлять памятью и очищать мусор — объекты, которые больше не используются.
Важно понимать, что современные движки обычно используют комбинацию подходов: часть кода выполняется через интерпретацию байткода, а наиболее «горячие» места компилируются JIT в более быстрые формы. Поэтому фраза «JavaScript — интерпретируемый язык» в реальности слишком упрощает картину.
Рантайм — окружение вокруг движка, которое даёт API и связывает код с системой
Рантайм — это не «синоним движка». Рантайм включает движок, но добавляет среду: события, очереди, таймеры, сетевые и файловые операции, интеграцию с браузером или операционной системой.
Простой пример: Promise — часть ECMAScript, а fetch — обычно часть браузерного рантайма. В Node.js fetch тоже может быть доступен, но это уже реализация рантайма, а не языка как стандарта.
Однопоточность выполнения кода и многопоточность окружения
Одна из главных причин путаницы: JavaScript-код в большинстве сценариев выполняется в одном потоке. Это означает, что в каждый конкретный момент времени движок исполняет одну инструкцию за другой, последовательно, без настоящего параллелизма внутри самого выполнения JS.
Но окружение вокруг может быть многопоточным. Сеть, файловая система, декодирование изображений, компоновка страницы, некоторые вычисления — всё это может выполняться не в том же потоке, где крутится JS-код. Поэтому JavaScript ощущается «асинхронным», хотя выполнение кода остаётся последовательным.
Event loop как диспетчер задач — почему асинхронность не равна параллельности
Event loop — это цикл, который выбирает, какую задачу выполнять следующей. Он связывает завершение асинхронных операций с выполнением JavaScript-кода: когда сеть получила ответ или таймер «дозрел», рантайм ставит задачу в очередь, а event loop в подходящий момент отдаёт её движку.
Ключевая мысль: асинхронность чаще всего означает «не блокировать выполнение, а продолжить позже», а не «выполнять одновременно в том же потоке». Параллельность возможна через воркеры, потоки или отдельные процессы, но это отдельные механизмы.
Где и как запускается JavaScript — от вкладки браузера до сервера
Один и тот же JavaScript-код может жить в разных окружениях. При этом язык остаётся тем же, а доступные возможности и ограничения могут существенно отличаться. Для практики это важно: вы должны понимать, где находится ваш код, какие API доступны, что блокирует выполнение и как измерять производительность.
JavaScript в браузере — вкладка, документ, рендеринг и Web API
В браузере JavaScript тесно связан с пользовательским интерфейсом. Он может читать и менять DOM, реагировать на события, отправлять запросы, работать с локальным хранилищем и управлять отрисовкой. При этом есть принципиальные ограничения безопасности, потому что браузер защищает пользователя.
Практические особенности браузерного JavaScript:
- Главный поток отвечает и за выполнение JS, и за часть работы по интерфейсу — если вы блокируете его на 200–500 мс, пользователь это увидит.
- Рендеринг кадров связан с нагрузкой — для плавности анимации полезно помнить бюджет в 16,7 мс на кадр при 60 FPS.
- Доступ к системе ограничен — нет прямого чтения файлов без участия пользователя, нет произвольного доступа к другим доменам без правил безопасности.
JavaScript на сервере — Node.js и системные API, файловая система и сеть
На сервере JavaScript чаще всего ассоциируют с Node.js. Здесь нет DOM, зато есть доступ к файловой системе, сетевым соединениям, процессам и потокам ввода-вывода. Архитектура Node.js ориентирована на большое количество I/O-операций: тысячи сетевых соединений, логирование, очереди, брокеры сообщений.
Практические особенности серверного JavaScript:
- Фокус на I/O — важно не блокировать event loop тяжёлыми вычислениями, иначе «замрёт» обработка запросов.
- Масштабирование часто делают процессами и потоками — кластеризация, worker threads, очереди задач.
- Профилирование и утечки памяти критичны — на сервере рост памяти на 100–300 МБ может привести к рестартам и деградации сервиса.
Другие рантаймы — Deno, Bun и встраиваемые движки
Кроме Node.js существуют другие рантаймы. Они по-разному организуют стандартную библиотеку, безопасность, модули и инструменты разработки. Для понимания «как работает JavaScript» важно не название рантайма, а принципы: движок исполняет язык, рантайм даёт API, event loop управляет порядком выполнения.
Также JavaScript-движок может быть встроен в другие программы: игры, приложения, корпоративные продукты, устройства. Тогда набор API полностью зависит от того, что предоставил разработчик платформы.
JavaScript в приложениях — Electron, React Native и расширения браузера
В прикладных средах JavaScript часто выступает как универсальный «клей» для интерфейса и логики:
- В Electron вы получаете доступ к браузерному UI и к системным возможностям через Node.js-подобные механизмы, но должны особенно внимательно относиться к безопасности.
- В React Native JavaScript управляет логикой, а интерфейс рисуется нативными компонентами, что создаёт свои нюансы производительности и коммуникации между слоями.
- В расширениях браузера доступна часть специальных API, но есть строгие ограничения и правила публикации.
JavaScript рядом с WebAssembly — когда выгодно подключать Wasm
WebAssembly не заменяет JavaScript, а дополняет его. Если у вас есть вычисления, которые требуют высокой производительности и предсказуемости, Wasm может дать прирост за счёт другого профиля исполнения. Но выигрывает не каждый сценарий: перенос маленьких функций ради «скорости» часто не окупается из-за накладных расходов на загрузку и передачу данных.
Типовые случаи, когда Wasm действительно уместен:
- Тяжёлые вычисления — обработка изображений, аудио, видео, кодеки.
- Криптография и компрессия — где важны скорость и контроль над памятью.
- Портирование существующих библиотек на C, C++ или Rust.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Что происходит, когда вы запускаете JS-код — цепочка шагов без мистики
Любой запуск JavaScript можно представить как маршрут от «текста» к «действиям». Если упростить, вы всегда проходите одни и те же этапы: получение кода, разбор, подготовка к выполнению, выполнение, оптимизация, возможная деоптимизация и взаимодействие с внешним миром через API.
Получение исходника — из файла, сети, бандла или inline-скрипта
Код попадает в движок разными путями:
- Как отдельный файл JavaScript, загруженный по сети.
- Как часть бандла, где в одном файле собраны десятки модулей.
- Как inline-скрипт прямо в HTML.
- Как динамически созданный код, например через Function или import().
На фронтенде качество загрузки напрямую влияет на пользовательский опыт. Файл на 300 КБ без сжатия и без кеширования может добавлять сотни миллисекунд к времени интерактивности, особенно на мобильных сетях. Поэтому стратегии загрузки скриптов и разделение кода по страницам — не «SEO-магия», а инженерная необходимость.
Токенизация и парсинг — построение AST
Движок не исполняет текст напрямую. Сначала он превращает поток символов в токены, а затем строит AST — абстрактное синтаксическое дерево. Это дерево описывает структуру программы: где объявлены функции, какие выражения используются, как устроены блоки, условия, циклы, импорты.
Практическая ценность понимания AST:
- Синтаксические ошибки обнаруживаются до выполнения — это причина, почему ошибка в одном файле может «сломать» весь запуск скрипта.
- Инструменты сборки и линтеры работают с деревом — поэтому они могут находить проблемы, даже не исполняя код.
- Некоторые конструкции парсятся и оптимизируются по-разному — например, модули позволяют более агрессивный анализ, чем динамические импорты.
Компиляция в байткод — зачем он нужен
После AST движок часто генерирует байткод — промежуточные инструкции для виртуальной машины движка. Байткод удобен тем, что он компактнее, чем AST, быстрее исполняется, и его проще анализировать для оптимизации.
Если вы привыкли к мысли «компиляция = машинный код», здесь важно уточнить: байткод не обязан быть машинным кодом процессора. Это формат, который движок умеет быстро интерпретировать и, при необходимости, превращать в более оптимальную форму.
Интерпретация байткода — первый запуск и прогрев
Первое выполнение кода часто идёт через интерпретацию байткода. В этот момент движок собирает статистику: какие функции вызываются чаще, какие ветки условий популярнее, какие типы данных реально проходят через параметры и свойства объектов.
Этот этап называют «прогревом». Пока код прогревается, производительность может быть ниже, чем после оптимизации. Именно поэтому микробенчмарки на 10–50 итераций часто вводят в заблуждение, а реальные улучшения проявляются на 10 000–1 000 000 повторений или в длительных сессиях пользователя.
JIT-компиляция — оптимизация горячих участков
JIT означает, что движок компилирует и оптимизирует код «по ходу дела». Когда он видит, что функция стала горячей, он может создать более быстрый вариант выполнения. Типичная идея оптимизации — опираться на наблюдения: если параметр всегда число, можно применить быстрый путь, если объект всегда одинаковой формы, можно ускорить доступ к свойствам.
Для разработчика это означает две вещи:
- Стабильность типов и структуры объектов часто делает код быстрее без единой «магической» оптимизации.
- Резкие изменения формы объектов и типов могут ломать предположения движка и сбрасывать оптимизацию.
Деоптимизация — почему быстрый код внезапно становится медленным
Оптимизация строится на предположениях. Когда предположение перестаёт быть верным, движок может выполнить деоптимизацию — откатиться к более универсальному, но медленному пути исполнения. Это нормально: универсальный путь работает для всех случаев, но он дороже.
Типичные причины деоптимизации в прикладном коде:
- Функция внезапно начинает получать значения другого типа — например, число и строку в одном месте.
- У объектов меняется «форма» — добавляются или удаляются свойства в разном порядке.
- Массив становится разреженным — вместо плотного набора элементов появляются «дыры».
- Смешиваются сценарии использования — один и тот же участок кода пытается обслуживать слишком разные формы данных.
Результат для пользователя может выглядеть как «периодические лаги», когда всё было плавно, а потом на 200–400 мс интерфейс замер. Часто причина — не сеть и не «медленный телефон», а деоптимизация плюс сборка мусора плюс тяжёлый обработчик в одном кадре.
Выполнение и побочные эффекты — DOM, сеть, таймеры, файлы и логирование
Сам по себе движок работает с вычислениями, объектами и памятью. Но как только ваш код меняет DOM, делает запросы, ставит таймеры, пишет в файл или лог, вы выходите в рантайм. Там включаются правила очередей, событий, рендеринга и безопасности.
Практически важно различать:
- Чистые вычисления — сортировки, преобразования, фильтрации, расчёты без взаимодействия с внешним миром.
- Побочные эффекты — любые операции, которые меняют внешний мир или зависят от него.
- Переходы между уровнями — каждый вызов API рантайма может иметь накладные расходы и ограничения.
Движок JavaScript под капотом — как он ускоряет ваш код
Понимание движка не нужно, чтобы писать «просто работающий» код. Но оно нужно, чтобы писать код, который работает быстро и стабильно в реальных условиях: на слабых смартфонах, в долгих сессиях, под нагрузкой, с большим объёмом данных.
Основные компоненты движка — парсер, интерпретатор и оптимизирующий компилятор
Современный движок обычно состоит из нескольких крупных частей:
- Парсер — превращает исходный текст в структуру, понятную движку.
- Генератор байткода — создаёт промежуточные инструкции для выполнения.
- Интерпретатор — быстро запускает код и собирает статистику выполнения.
- Оптимизирующий компилятор — ускоряет горячие функции на основе профиля.
- Система управления памятью — выделение, пометки, сборка мусора.
Такой конвейер позволяет быстро стартовать и постепенно ускоряться. Это важно для браузера: пользователь хочет увидеть результат быстро, а не ждать долгой компиляции до первого пикселя.
Профилирование исполнения — как движок решает, что оптимизировать
Оптимизация должна окупаться. Если функция выполняется 2 раза, нет смысла тратить ресурсы на сложную компиляцию. Поэтому движок собирает профиль — частоту вызовов, типы аргументов, формы объектов, ветвления условий.
На практике это приводит к понятным последствиям:
- Горячие пути ускоряются — код, который выполняется 10 000–100 000 раз, становится кандидатом на оптимизацию.
- Редкие ветки остаются медленнее — и это нормально, потому что ими почти не пользуются.
- Код может «разогнаться» через секунды после запуска — особенно в сложных интерфейсах и играх.
Специализация типов в динамическом языке — как это ускоряет выполнение
JavaScript динамически типизирован: переменная может хранить число, строку, объект, массив. Универсальность удобна, но потенциально медленнее. Движок компенсирует это специализацией: если он видит, что переменная почти всегда число, он может выбрать быстрый путь работы с числами.
Отсюда рождается практическое правило: если вы постоянно смешиваете типы, движку сложнее ускорять код. Если же вы поддерживаете предсказуемость типов на горячих участках, производительность часто улучшается без дополнительных усилий.
Скрытые классы и формы объектов — почему важна стабильность структуры
У объектов в JavaScript можно добавлять свойства в любой момент. Для движка это проблема: универсальный «словарный» доступ к свойствам медленнее. Поэтому движки используют концепцию скрытых классов или «форм объектов».
Идея простая: если много объектов создаются одинаково, например с одинаковым набором свойств, движок может представить их как структуры одной формы. Тогда доступ к свойствам похож на быстрый доступ по фиксированному смещению, а не на поиск в словаре.
Практические выводы для прикладного кода:
- Создавайте объекты с одинаковым набором свойств в одном порядке, если это горячий путь.
- Избегайте хаотичного добавления свойств после создания объекта в критичных местах.
- Не заставляйте один и тот же объект менять «профессию» — сегодня он хранит одни поля, завтра другие.
Inline caching — ускорение доступа к свойствам
Inline caching — техника ускорения повторяющихся операций, например чтения свойства объекта. Если код много раз обращается к obj.value, движок запоминает, как именно доставать это свойство для объектов определённой формы. При повторении он может выполнять доступ почти мгновенно.
Но это работает лучше, когда формы объектов стабильны. Если в одном месте кода вы передаёте десятки разных форм, кеш становится менее эффективным. Тогда движок вынужден выбирать более универсальный путь, что добавляет накладные расходы.
Встроенные оптимизации массивов — плотные и разреженные массивы
Массивы в JavaScript могут быть очень быстрыми, но не всегда. Для движка важна структура массива:
- Плотный массив — элементы лежат подряд, индексы идут без дыр, например 0, 1, 2, 3.
- Разреженный массив — есть пропуски, например элементы только по индексам 0 и 10 000.
Плотные массивы проще оптимизировать и быстрее проходить циклом. Разреженные массивы часто требуют более сложной внутренней структуры, что ухудшает скорость перебора и доступ по индексу.
Также влияет однородность данных: массив чисел может быть оптимизирован иначе, чем массив, где перемешаны числа, строки, объекты и undefined. Для горячих вычислений полезно держать данные максимально однотипными.
Пределы оптимизаций — мегаморфизм, полиморфизм и сломанные предположения
Оптимизации работают, пока код предсказуем. Когда одна и та же операция сталкивается со слишком большим разнообразием форм и типов, движок теряет возможность эффективно кешировать и специализировать выполнение.
Типовые ситуации, когда оптимизация ухудшается:
- Полиморфизм — операция видит несколько разных форм объектов и вынуждена выбирать между ними.
- Мегаморфизм — форм становится слишком много, кеш перестаёт быть полезным и всё превращается в универсальный путь.
- Резкие изменения данных — типы и структуры постоянно «плавают», ломая предположения компилятора.
Для прикладного разработчика это превращается в правило инженерного здравого смысла: горячий код должен быть простым и предсказуемым. Если вы пытаетесь одним участком кода обслужить 20 разных форм данных, вы платите производительностью и сложностью отладки.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Рантайм JavaScript — что добавляется поверх языка
Когда говорят «JavaScript работает в браузере» или «JavaScript работает на сервере», часто подразумевают не только язык, но и то, что окружает язык. В реальности ECMAScript описывает правила самого языка, а всё, что связано с сетью, таймерами, файлами, интерфейсом, событиями и интеграцией с операционной системой, приходит из runtime environment — среды выполнения.
Именно рантайм объясняет, почему одна и та же конструкция JavaScript может вести себя по-разному в Chrome и Safari, почему код, написанный для браузера, не запускается в Node.js без изменений, и почему серверный код может работать «без окон и кнопок», но при этом обслуживать тысячи соединений.
Что такое runtime environment — движок плюс API и интеграция с системой
Runtime environment — это «обвязка» вокруг движка, которая делает язык полезным в конкретной платформе. Если упростить до формулы, получается так:
движок JavaScript + набор API + механизм событий и очередей + интеграция с системой = рантайм
Что обычно входит в рантайм:
- Доступ к внешним возможностям — сеть, таймеры, хранилища, файловая система, потоки данных.
- Модель событий — обработка кликов, сообщений, сетевых ответов, таймеров.
- Очереди задач и правила выполнения — чтобы асинхронные операции завершались «позже», а не блокировали код.
- Безопасность и ограничения — песочница браузера, политики доступа, разрешения.
Важное практическое следствие: язык один, но «мир» вокруг него меняется. Поэтому ваш код нужно понимать в контексте того рантайма, где он исполняется.
Глобальные объекты и окружение — window, globalThis, global
Чтобы взаимодействовать с рантаймом, JavaScript использует глобальные объекты. На уровне стандарта есть универсальная точка входа globalThis — это кросс-платформенный способ обратиться к глобальному объекту вне зависимости от среды.
Однако исторически и практично встречаются разные варианты:
- window — глобальный объект в браузере в контексте страницы. Через него доступны DOM, location, history, localStorage и другие браузерные интерфейсы.
- global — глобальный объект в Node.js (в CommonJS-среде). Через него доступны process, Buffer и прочие системные сущности.
- globalThis — стандартный способ получить глобальный объект и в браузере, и на сервере, и в воркерах.
Почему это важно новичкам: многие ошибки начинаются с неверного предположения, что «window есть везде». В Node.js window отсутствует, а в Web Worker нет window, но есть globalThis и self. Правильная привычка — думать «какой рантайм у меня сейчас».
Браузерные Web API — DOM, fetch, timers, storage, history
В браузере рантайм даёт набор Web API. Это не часть ECMAScript, поэтому их наличие и поведение регулируется спецификациями веб-платформы и реализациями браузеров.
DOM и связанные интерфейсы
DOM — это объектная модель документа. Через DOM JavaScript читает и меняет структуру HTML, стили, атрибуты, текст и состояние элементов. Это мощно, но дорого: доступ к DOM часто пересекается с механизмами рендеринга, компоновки и перерисовки.
Ключевые DOM-сущности:
- document — корневой объект документа, через него ищут элементы и создают узлы.
- Element и Node — базовые типы для узлов дерева, с методами управления структурой.
- EventTarget — интерфейс событий, на нём работают addEventListener и removeEventListener.
fetch и работа с сетью
fetch — интерфейс для сетевых запросов, построенный вокруг промисов. Он позволяет делать запросы и получать ответы без блокировки главного потока. Но важно помнить про ограничения: CORS, политика источников, кеширование, таймауты, отмена через AbortController.
Таймеры и планирование
setTimeout и setInterval позволяют отложить выполнение, но они не гарантируют точное время. Таймеры зависят от загрузки event loop, приоритета вкладки, энергосбережения, политики браузера. В реальных условиях задержка может быть больше заданной на 20–200 мс, а в фоне — намного больше.
Хранилища и история
localStorage и sessionStorage дают простое ключ-значение хранилище. Для более серьёзных сценариев есть IndexedDB, но она сложнее. history и location позволяют управлять адресной строкой и навигацией, что важно для SPA и маршрутизации.
Практически важно помнить: localStorage часто синхронный и может блокировать главный поток, поэтому его злоупотребление в критичных местах может вызывать лаги, особенно если вы записываете большие строки на 200 000–2 000 000 символов.
Серверные API — fs, net, http, process, streams
В Node.js рантайм ориентирован на систему и сеть. Здесь нет DOM, зато есть инструменты для построения серверов, работы с файловой системой и потоками данных.
fs и файловая система
fs даёт доступ к чтению и записи файлов. Особенность Node.js в том, что многие операции доступны в синхронном и асинхронном вариантах. Синхронные варианты блокируют event loop, поэтому на сервере их стараются избегать в обработчиках запросов.
net и низкоуровневая сеть
net позволяет работать с TCP-соединениями и строить свои протоколы. Это инструмент для тех случаев, когда HTTP недостаточно или вы строите специализированную сетевую архитектуру.
http и веб-серверы
http используется для создания серверов и клиента HTTP. На его основе построено множество фреймворков. Понимание того, что HTTP-обработчик выполняется в event loop, помогает избегать ошибок: тяжелая синхронная работа в обработчике может замедлить ответы всем пользователям.
process и управление запуском
process содержит информацию о процессе: аргументы командной строки, переменные окружения, сигналы, текущий статус. Через него управляют завершением программы, обработкой ошибок и поведением приложения в разных средах.
streams и потоки данных
Streams — модель работы с данными частями, а не целиком. Это особенно полезно, когда файл или ответ большой, например 500 МБ или 2 ГБ. Потоки позволяют обрабатывать данные постепенно и контролировать нагрузку через backpressure.
Почему один и тот же код ведёт себя по-разному — различия окружений
Различия в поведении возникают по нескольким причинам:
- Разные API — в браузере есть DOM и window, в Node.js есть fs и process.
- Разные ограничения безопасности — браузер защищает пользователя и изолирует вкладки, сервер имеет доступ к системе.
- Разный жизненный цикл — в браузере код живёт в контексте вкладки и событий пользователя, на сервере код часто живёт неделями.
- Разная модель производительности — в браузере важен FPS и отклик интерфейса, на сервере важны пропускная способность, latency и стабильность памяти.
- Разные детали event loop — очереди задач и приоритеты могут отличаться по реализации.
Правильный подход — писать код с ясным пониманием окружения и использовать адаптеры, где нужно: полифиллы, условные импорты, отдельные модули для браузера и сервера.
Контекст выполнения — Execution Context и области видимости
Чтобы уверенно понимать порядок выполнения и работу переменных, нужно разобраться с тем, как движок создаёт контексты выполнения и как формируются области видимости. Это базовые понятия, которые объясняют hoisting, TDZ, замыкания и поведение this.
Глобальный контекст и контекст функции
Execution Context можно представить как «пакет информации», который нужен движку для выполнения кода. Когда программа стартует, создаётся глобальный контекст. Когда вызывается функция, создаётся контекст функции. В некоторых случаях создаются контексты модулей и eval, но в прикладном коде чаще всего важны глобальный и функциональный.
В каждом контексте движок фиксирует:
- Какие переменные и функции доступны в текущей области видимости.
- Ссылку на внешнее лексическое окружение.
- Значение this для текущего выполнения.
- Состояние выполнения — где именно находится «указатель» в коде.
Когда функция завершает работу, её контекст обычно уничтожается. Но если есть замыкание, часть данных может продолжать жить, потому что на них остаются ссылки.
Лексическое окружение и цепочка областей видимости
Лексическое окружение — это структура, которая хранит объявления переменных и ссылку на внешнее окружение. Она формируется по месту написания кода, а не по месту вызова. Именно поэтому говорят «лексическая область видимости».
Цепочка областей видимости работает так: когда движок ищет переменную, он сначала смотрит в текущем окружении, затем идёт наружу по ссылкам, пока не дойдёт до глобального уровня. Если не найдёт — возникнет ReferenceError.
Практические эффекты цепочки областей видимости:
- Внутренняя функция может использовать переменные внешней функции — это и есть основа замыканий.
- Слишком глубокие замыкания могут удерживать крупные объекты в памяти дольше, чем вы ожидаете.
- Имена переменных во внутренних блоках могут «перекрывать» внешние, что иногда приводит к путанице.
Поднятие объявлений — hoisting и реальный порядок инициализации
Hoisting — это поведение, при котором объявления переменных и функций «как будто поднимаются» вверх области видимости. Но важно уточнить: поднимаются не значения, а сами объявления. Движок сначала проходит фазу создания контекста и регистрирует имена, а уже потом выполняет код построчно.
Типовое поведение:
- Function Declaration поднимаются полностью и доступны до места объявления.
- var поднимается как объявление, но инициализируется значением undefined до присваивания.
- let и const поднимаются, но не инициализируются сразу, из-за чего возникает TDZ.
Почему это важно: многие «странные» ошибки в JavaScript на самом деле объясняются тем, что движок сначала создаёт окружение, а уже потом выполняет строки. Если вы запомните этот принцип, вы перестанете воспринимать hoisting как магию.
Temporal Dead Zone — почему let и const не как var
Temporal Dead Zone — это промежуток времени между входом в область видимости и фактической инициализацией переменной let или const. В этот момент имя уже существует в окружении, но обращаться к нему нельзя, будет ошибка ReferenceError.
Зачем это сделано: чтобы предотвратить класс ошибок, характерный для var, когда переменная неожиданно равна undefined в начале блока. TDZ делает поведение более строгим и предсказуемым.
Практические правила для новичков:
- Используйте let для переменных, значение которых меняется.
- Используйте const по умолчанию, если переменная не переназначается.
- Избегайте var в новом коде, если нет строгой причины.
Важно уточнение: const запрещает переназначение переменной, но не делает объект неизменяемым. Если const хранит объект, его свойства можно менять, потому что меняется содержимое объекта, а не ссылка.
this и привязка контекста — call, apply, bind и стрелочные функции
Значение this — один из самых частых источников путаницы. Главный принцип: this определяется не тем, где функция написана, а тем, как она вызвана. Исключение — стрелочные функции, у которых this берётся из внешнего контекста.
Как формируется this при вызове
Типовые сценарии:
- Вызов как метод объекта — this указывает на объект слева от точки.
- Обычный вызов функции — this зависит от режима strict и окружения, часто это undefined.
- Вызов через new — this будет новым объектом.
- Явная привязка — call и apply задают this явно.
call, apply и bind
call вызывает функцию сразу и позволяет передать this и аргументы по списку. applybind
Практические случаи использования bind:
- Передача метода как коллбэка, чтобы не потерять контекст.
- Создание обработчиков событий с нужным this.
- Частичное применение аргументов, если это оправдано.
Стрелочные функции и this
Стрелочные функции не имеют собственного this. Они берут this из внешней области видимости. Это удобно в коллбэках и обработчиках, потому что вы избегаете потери контекста. Но важно помнить: стрелочные функции нельзя использовать как конструкторы с new, и иногда вам нужен именно «динамический this» обычной функции.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Стек вызовов и модель выполнения
Чтобы понять последовательность выполнения, нужно понимать call stack — стек вызовов. Это структура данных, в которой движок хранит текущие активные вызовы функций. Он объясняет, почему код выполняется последовательно, как возникают ошибки переполнения стека и почему синхронные операции блокируют интерфейс.
Call stack — как интерпретируется последовательность выполнения
Когда выполняется функция, она добавляется в стек. Когда функция завершается, она снимается со стека. Текущая выполняемая функция всегда находится на вершине стека. Поэтому JavaScript выглядит как «поток исполнения».
Практически это значит:
- Пока функция не завершится, следующая строка «снаружи» не выполнится.
- Если функция выполняется 300 мс, интерфейс и другие задачи могут ждать эти 300 мс.
- Ошибки показывают стек вызовов, и это главный инструмент поиска источника проблемы.
Стек и рекурсия — переполнение и способы избежать
Рекурсия — это вызов функции самой себя. Она удобна для обхода деревьев и графов, но в JavaScript глубина стека ограничена. Точная глубина зависит от движка и окружения, но на практике уже несколько тысяч вложенных вызовов могут привести к ошибке переполнения стека.
Способы избежать переполнения:
- Переписать рекурсию в итерацию с циклом и собственным массивом-стеком.
- Разбивать задачу на части через очереди задач и планирование, если подходит по логике.
- Использовать алгоритмы обхода с явной структурой данных, а не со стеком вызовов.
Новичкам важно понимать: переполнение стека — это не «ошибка языка», это физическое ограничение памяти стека и глубины вложенных вызовов.
Синхронный код и блокировки — почему подвисает UI
В браузере главный поток часто отвечает и за выполнение JavaScript, и за обработку пользовательских событий, и за часть рендеринга. Если вы выполняете синхронно тяжёлую работу, например сортировку массива на 1 000 000 элементов или цикл, который занимает 500 мс, вы блокируете всё остальное: клики не обрабатываются, прокрутка дергается, анимации рвутся.
Типовые источники блокировок:
- Долгие циклы и тяжелые вычисления без пауз.
- Частые операции с DOM в одном обработчике.
- Синхронные обращения к хранилищам и тяжёлое сериализование JSON на больших объектах.
- Чрезмерное логирование в цикле, особенно в DevTools.
Хорошая привычка — мыслить бюджетами: если вы хотите 60 FPS, на «всё про всё» в одном кадре у вас около 16,7 мс, а часть этого времени уйдёт на рендеринг. Поэтому в идеале JavaScript-работа в кадре должна быть заметно меньше этого порога.
Визуализация выполнения — как читать стек в ошибках и DevTools
Когда происходит ошибка, вы видите стек вызовов. Это список функций, которые привели к проблеме. Читать стек нужно сверху вниз, но искать первопричину чаще полезнее снизу вверх, начиная с вашего кода, а не с внутренних библиотечных вызовов.
Полезные навыки:
- Понимать, где ваша функция в стеке и кто её вызвал.
- Отличать синхронный стек от асинхронного контекста, где стек «обрывается».
- Использовать breakpoints и шаги отладки, чтобы увидеть порядок выполнения.
- Смотреть вкладки Performance и Memory, чтобы находить блокировки и утечки.
Память — heap, объекты и сборка мусора
JavaScript управляет памятью автоматически: вы создаёте объекты, массивы, функции, а освобождением занимается сборщик мусора. Это удобно, но у этого есть цена: сборка мусора может создавать паузы, а неправильное удержание ссылок приводит к утечкам памяти.
Heap и стек — что где живет
В упрощенной модели:
- Стек хранит информацию о вызовах функций и некоторые значения, привязанные к контексту выполнения.
- Heap хранит объекты, массивы, функции и другие структуры, которые живут дольше одной строки кода.
Когда вы создаёте объект, вы обычно создаёте его в heap, а в стеке хранится ссылка на этот объект. Поэтому копирование объекта и копирование ссылки — это разные вещи. Копия ссылки не создаёт новый объект, она просто указывает на тот же участок heap.
Как выделяется память под объекты и массивы
Когда вы создаёте объект или массив, движок выделяет память в heap. В зависимости от формы объекта и структуры массива движок может выбирать разные внутренние представления. Плотные массивы обычно хранятся компактнее и перебираются быстрее. Объекты со стабильной структурой могут храниться как «структуры», а объекты с хаотическими свойствами могут превратиться в «словарный режим», который медленнее.
Практический вывод: предсказуемые структуры данных помогают и производительности, и памяти. Это особенно важно в приложениях, которые живут долго и обрабатывают большие объёмы данных.
Garbage Collection — почему память освобождается не сразу
Garbage Collection работает по принципу достижимости. Объект считается «живым», если до него можно добраться по цепочке ссылок от корней, таких как глобальные объекты, текущие контексты выполнения и активные структуры рантайма.
Память не освобождается «сразу», потому что GC запускается периодически. Он должен балансировать: слишком частая сборка мусора тратит CPU и мешает работе, слишком редкая увеличивает потребление памяти. Поэтому движок выбирает момент, когда сборка выгодна.
Поколенческая модель GC — молодое и старое поколение
Многие движки используют поколенческую сборку мусора. Идея основана на наблюдении: большинство объектов живут недолго. Поэтому heap делят на зоны:
- Молодое поколение — новые объекты, которые часто быстро становятся ненужными.
- Старое поколение — объекты, которые пережили несколько циклов GC и живут долго.
Сборка мусора в молодом поколении обычно быстрая и частая. Сборка в старом поколении реже, но может быть тяжелее. Именно она чаще связана с заметными паузами и «микрофризами» в интерфейсе.
Паузы GC и производительность — откуда берутся микрофризы
В браузере пользователь чувствует паузы GC как короткие подвисания: прокрутка «подрагивает», анимация теряет кадры, клики обрабатываются с задержкой. Даже пауза на 20–40 мс заметна, потому что это уже больше бюджета одного кадра в 16,7 мс. Пауза на 100 мс — это ощущается как явный лаг.
Что усиливает риск пауз:
- Создание большого количества краткоживущих объектов в цикле.
- Большие массивы и крупные графы объектов, которые надо обойти при пометке.
- Удержание ненужных ссылок, из-за которых heap растёт и GC работает тяжелее.
Типичные утечки памяти — глобальные ссылки, таймеры, подписки, замыкания и DOM
Утечка памяти в JavaScript почти всегда означает одно: объект больше не нужен логически, но на него осталась ссылка, и сборщик мусора не может его освободить.
Типовые источники утечек:
- Глобальные ссылки — кэш в globalThis или window, который никогда не очищается.
- Таймеры — setInterval, который продолжает жить, хотя компонент уже не нужен.
- Подписки на события — addEventListener без removeEventListener при уничтожении логики.
- Замыкания — внутренние функции удерживают внешние переменные и большие объекты.
- DOM-узлы — ссылки на элементы, которые удалили из DOM, но на них остались ссылки в коде.
Практический признак: при переходах между страницами или открытии модалок память растёт и не возвращается. Например, было 150 МБ, стало 260 МБ, затем 340 МБ и дальше. Это сигнал, что что-то удерживается.
WeakMap и WeakRef — когда они спасают от утечек
WeakMap позволяет хранить данные, ключами которых являются объекты, но не мешает сборке мусора этих объектов. Если объект-ключ становится недостижимым, запись может исчезнуть автоматически. Это полезно для кешей, метаданных и привязок данных к объектам без риска удержания.
WeakRef даёт слабую ссылку на объект. Она может стать «пустой», если объект собран. WeakRef — инструмент для продвинутых сценариев, где вы хотите кешировать, но не обязаны удерживать объект в памяти. Для новичков важно: WeakRef не заменяет нормальное управление жизненным циклом, а лишь дополняет его.
Однопоточность, параллельность и почему JavaScript всё успевает
JavaScript часто называют однопоточным, и это верно для выполнения кода. Но при этом приложения выполняют сеть, таймеры, обработку медиа и множество фоновых задач. Здесь важно различать: что выполняет движок, и что выполняет окружение вокруг.
Один поток выполнения JS-кода — что именно однопоточно
Однопоточность означает, что в рамках одного контекста исполнения движок выполняет JavaScript-инструкции последовательно. В каждый момент времени выполняется только один фрагмент кода. Это упрощает модель данных: нет конкурентного доступа к объектам из нескольких потоков.
Практические последствия:
- Нет настоящих гонок по памяти внутри одного потока, но есть гонки по времени и порядку событий.
- Любая тяжёлая синхронная работа блокирует всё остальное.
- Асинхронность — главный способ «не блокировать» и возвращаться к задаче позже.
Параллельные подсистемы — сеть, таймеры, диск, рендеринг и декодирование
Окружение выполняет много работы параллельно:
- Сеть получает данные и уведомляет рантайм, когда ответ готов.
- Таймеры отсчитывают время и создают задачи в очередях.
- Файловая система на сервере выполняет I/O операции.
- Браузер рендерит страницу, считает layout, делает repaint и композитинг.
- Декодирование изображений и видео может идти отдельно от JS-потока.
JavaScript получает результат через событие, коллбэк или промис. Поэтому кажется, что JavaScript «делает всё сразу», хотя он просто получает готовые результаты и обрабатывает их по очереди.
Соревнование задач — гонки данных и состояние приложения
Хотя в одном потоке нет гонок памяти, есть гонки событий. Например, пользователь нажал кнопку 3 раза подряд, сеть вернула ответы в неожиданном порядке, а вы обновили состояние так, что поздний ответ перезаписал более свежие данные.
Типовые проблемы:
- Состязание запросов — второй запрос завершился раньше первого и перезаписал результат.
- Дубли обработчиков — компонент смонтирован несколько раз и повесил несколько слушателей.
- Состояние устаревает — коллбэк использует старые значения из замыкания.
Решения обычно связаны с управлением жизненным циклом: отмена, дедупликация, маркеры актуальности, очереди, единый источник истины.
Потокобезопасность и SharedArrayBuffer — когда появляется настоящая конкуренция
Настоящая конкуренция по памяти появляется, когда вы используете воркеры и общий буфер данных. SharedArrayBuffer позволяет нескольким потокам видеть одну и ту же память. Тогда вам нужны Atomics и правила синхронизации, иначе вы получите классические проблемы многопоточности.
Для большинства приложений это избыточно. Но в задачах с высокой производительностью, обработкой медиа и сложными вычислениями SharedArrayBuffer позволяет строить эффективные архитектуры.
Event loop — сердце асинхронного JavaScript
Чтобы понимать порядок выполнения, нужно уверенно владеть моделью event loop. Он объясняет, почему setTimeout не выполняется «в точное время», почему Promise.then часто запускается раньше таймера, почему интерфейс иногда не перерисовывается сразу и почему логирование выводится «не так, как ожидается».
Что такое цикл событий — правила выбора следующей задачи
Цикл событий — это механизм, который повторяет один и тот же процесс: берёт задачу из очереди, выполняет её в движке, затем даёт шанс обработать микрозадачи и, в браузере, провести этапы рендеринга. Важно: пока выполняется задача, движок не переключается на другую задачу. Переключение происходит только между задачами.
Очередь задач — macrotask queue и типичные источники
Macrotask queue обычно содержит задачи вроде:
- setTimeout и setInterval.
- События UI — клики, ввод, прокрутка.
- Сообщения — postMessage, MessageChannel.
- Некоторые события сети и браузерные события.
Каждая macrotask выполняется целиком, затем движок проверяет микрозадачи и продолжает цикл. Поэтому длинная macrotask на 100–300 мс приводит к лагам: пока она работает, всё остальное ждёт.
Очередь микрозадач — microtask queue и почему Promise срочнее
Microtask queue — очередь задач более высокого приоритета. Она используется, например, для обработчиков промисов. Микрозадачи выполняются после завершения текущей macrotask, но до того, как event loop возьмёт следующую macrotask.
Это и объясняет типичную ситуацию: вы ставите setTimeout и Promise.then, и then выполняется раньше, даже если таймер 0 мс. Таймер попадёт в очередь задач, а микрозадача промиса выполнится раньше перехода к следующей задаче.
Важный практический риск: если вы создаёте бесконечный поток микрозадач, вы можете «задушить» цикл событий, потому что до следующей macrotask дело не дойдёт. Это редкость, но может случаться при неосторожных рекурсивных цепочках промисов.
Рендеринг кадра и event loop — когда браузер рисует изменения
В браузере рендеринг обычно не происходит «после каждой строки кода». Браузер старается группировать изменения. Если вы изменили DOM 50 раз подряд в одном обработчике, браузер может отрисовать результат только после завершения текущей задачи и выполнения обязательных этапов цикла.
Практическая польза этого знания:
- Если вы хотите гарантировать, что кадр нарисуется перед следующим шагом, используйте подходящие точки планирования, например requestAnimationFrame.
- Если вы постоянно читаете layout после записи DOM, вы можете заставить браузер делать лишние синхронные вычисления.
- Перерисовка и компоновка имеют стоимость, и она зависит от сложности DOM и стилей.
Таймеры setTimeout и setInterval — точность, задержки и дрейф
Таймеры дают нижнюю границу задержки, а не точное время. Если вы указали 10 мс, это означает «не раньше 10 мс». Реальное выполнение может быть позже, если event loop занят. В фоне вкладки браузеры часто увеличивают минимальную задержку таймеров, чтобы экономить ресурсы.
Особенность setInterval — накопление дрейфа. Если коллбэк работает дольше интервала или event loop перегружен, интервалы смещаются, и вы получаете нерегулярные вызовы. Поэтому для точных сценариев чаще используют схему с setTimeout и вычислением следующего запуска по времени.
requestAnimationFrame — правильное место для анимаций
requestAnimationFrame предназначен для анимаций и обновлений, привязанных к кадрам. Он вызывает коллбэк перед перерисовкой. Это позволяет делать анимации плавнее и экономнее, потому что вы работаете в том же ритме, что и рендеринг. Если вкладка не активна, браузер может уменьшить частоту вызовов, что тоже полезно для батареи.
Наглядные сценарии порядка выполнения — почему лог выводится не так
Путаница обычно возникает в таких случаях:
- Синхронный код выполняется сразу, в текущей задаче.
- Микрозадачи промисов выполняются после завершения текущего синхронного участка, до перехода к следующей задаче.
- Macrotask таймеров и событий выполняются позже, когда event loop возьмёт их из очереди.
Если вы держите в голове этот порядок, большинство «парадоксов» исчезает, а поведение кода становится предсказуемым.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Асинхронные паттерны — от коллбэков до async await
Асинхронность в JavaScript выросла эволюционно. Сначала были коллбэки, затем промисы, затем синтаксис async await. Каждый шаг делал код более читаемым и управляемым, но добавлял свои нюансы и типичные ошибки.
Коллбэки — плюсы, минусы и ад коллбэков
Коллбэк — функция, которую вы передаёте другой функции, чтобы она вызвала её позже. Это основа событийной модели. Коллбэки просты и эффективны, но при сложной логике создают проблемы.
Плюсы коллбэков:
- Минимальные накладные расходы и прямолинейная модель.
- Хорошо подходят для событий и подписок.
- Легко передавать параметры и контекст.
Минусы коллбэков:
- Вложенность и ухудшение читаемости при цепочках действий.
- Сложнее обрабатывать ошибки единообразно.
- Риск вызвать коллбэк несколько раз или не вызвать вовсе.
- Сложнее отменять операции и управлять жизненным циклом.
Термин «ад коллбэков» описывает ситуацию, когда логика превращается в лестницу вложенных функций, где трудно понимать порядок и точки выхода.
Промисы — состояния, цепочки и обработка ошибок
Promise представляет асинхронный результат, который будет доступен позже. У промиса есть состояния: ожидание, выполнено успешно, выполнено с ошибкой. Промисы позволяют строить цепочки, а ошибки можно ловить единым образом.
Практические преимущества промисов:
- Читаемые цепочки then и catch.
- Единая модель ошибок через reject и catch.
- Композиция — можно объединять промисы и управлять параллелизмом.
Типичные ошибки с промисами:
- Забыть вернуть промис из then, из-за чего цепочка ломается.
- Поймать ошибку слишком рано и скрыть её, потеряв диагностику.
- Создать промис, который никогда не завершается, и тем самым удерживать ресурсы.
async await — синтаксический сахар и важные нюансы
async await делает асинхронный код похожим на синхронный по форме, но не по сути. await «приостанавливает» выполнение функции до завершения промиса, при этом не блокирует главный поток. Вместо этого функция возвращает управление event loop, а продолжение выполняется позже как часть микрозадач.
Нюансы, которые важно знать:
- await работает только внутри async функции или на верхнем уровне в модуле, если поддерживается top-level await.
- Ошибки надо ловить try catch, но помнить, что try catch ловит только то, что await-ится внутри блока.
- Последовательные await могут быть медленнее параллельного запуска, если операции независимы.
Параллелизм задач — Promise.all, allSettled, any и race
Когда у вас несколько независимых асинхронных операций, вы выбираете стратегию:
- Promise.all — все должны выполниться успешно, иначе ошибка.
- Promise.allSettled — дождаться всех независимо от результата.
- Promise.any — первый успешный результат, ошибки игнорируются пока есть шанс успеха.
- Promise.race — первый завершившийся, успешный или с ошибкой.
Важно понимать: эти методы управляют завершением, но не отменяют операции сами по себе. Если вы запустили 10 fetch запросов, Promise.race завершится на первом, но остальные запросы могут продолжить выполнение, если вы их не отмените.
Отмена операций — AbortController и управление жизненным циклом
AbortController позволяет отменять операции, которые это поддерживают, например fetch. Это важно для интерфейсов: пользователь ушёл со страницы, сменил фильтр, закрыл модалку, и старые запросы больше не нужны. Без отмены вы получаете лишнюю нагрузку и риск устаревших данных.
Типовые случаи, когда отмена критична:
- Поиск по мере ввода — пользователь меняет запрос 5–20 раз за несколько секунд.
- Переключение вкладок и маршрутов — старые запросы должны прекращаться.
- Длинные загрузки — пользователь отменяет действие и ожидает мгновенной реакции.
Очереди событий и промисы — как не получить бесконечную микрозадачу
Промисы выполняют продолжение в микрозадачах. Если вы создаёте цепочки, которые снова планируют микрозадачи без пауз, вы можете «захватить» event loop. Практически это выглядит как зависший UI: macrotask с событиями и рендерингом не получают шанс выполниться.
Как снижать риск:
- Не строить бесконечные рекурсивные цепочки then.
- Для длительных процессов использовать планирование с уступкой, например разбивку на части.
- Следить за нагрузкой в Performance профиле, если есть подозрение на «микрозадачный шторм».
Streams и асинхронная итерация — когда данные приходят частями
Потоки полезны, когда данные большие или приходят постепенно. В браузере и на сервере появляются API потоков, которые позволяют читать данные кусками и не держать всё в памяти.
Асинхронная итерация помогает обрабатывать поток как последовательность частей: вы получаете следующую порцию данных, обрабатываете и переходите дальше. Это снижает пиковое потребление памяти и улучшает отзывчивость, особенно если вы комбинируете это с планированием.
DOM и события — как JavaScript оживляет страницу
На стороне браузера большая часть «жизни» интерфейса — это DOM и события. JavaScript связывает действия пользователя, сетевые ответы и изменения в интерфейсе через обработчики событий, манипуляции DOM и планирование перерисовок.
Что такое DOM — дерево документа и узлы
DOM — это дерево, где каждый элемент HTML представлен узлом. У узлов есть тип, родители, дети, атрибуты и текст. Когда вы вызываете document.querySelector, вы ищете узел. Когда вы меняете textContent или classList, вы меняете состояние узла.
Почему модель дерева важна:
- Любое изменение узла может повлиять на компоновку соседей.
- Поиск по дереву имеет стоимость, особенно при больших документах.
- Частые изменения могут приводить к дорогим перерисовкам.
Чтение и запись DOM — почему это может быть дорогим
Операции DOM часто дороже, чем операции с обычными объектами JavaScript. Причина в том, что DOM связан с рендерингом. Когда вы пишете в DOM, браузер должен понять, как это повлияет на страницу. Когда вы читаете определённые свойства, браузер может быть вынужден синхронно пересчитать layout.
Практический принцип:
- Старайтесь группировать записи DOM и минимизировать чередование «прочитал-изменил-прочитал-изменил».
- Избегайте лишних измерений layout в горячих обработчиках, например при прокрутке.
- Используйте requestAnimationFrame для обновлений, завязанных на кадры.
Layout, reflow, repaint — что вызывает перерисовку
Когда меняется DOM или стили, браузер может выполнять несколько этапов:
- Layout — расчёт размеров и позиций элементов.
- Reflow — часто используют как синоним перерасчёта компоновки.
- Repaint — перерисовка пикселей, например при смене цвета.
Некоторые изменения влияют только на внешний вид и требуют repaint, другие влияют на геометрию и требуют layout, что обычно дороже. Поэтому выбор CSS-свойств и стратегия обновлений реально влияет на производительность.
События — подписка, обработчики и снятие обработчиков
События — это основной способ реагировать на действия пользователя и изменения в среде. Вы подписываетесь через addEventListener и получаете объект события в обработчике. Важно помнить про снятие обработчиков, когда логика больше не нужна, иначе появляются утечки памяти и дублирование действий.
Типовые правила:
- Ставьте обработчик там, где он логически принадлежит.
- Снимайте обработчик при уничтожении компонента или прекращении сценария.
- Не создавайте новые функции-обработчики в цикле без необходимости, если потом их нужно снимать.
Фазы события — capturing, target и bubbling
Событие проходит несколько фаз:
- Capturing — событие идёт сверху вниз по дереву к целевому элементу.
- Target — событие достигает целевого узла.
- Bubbling — событие всплывает вверх по дереву.
Понимание фаз помогает строить делегирование, управлять stopPropagation и проектировать архитектуру событий без хаоса.
Делегирование событий — производительность и простота кода
Делегирование означает, что вместо того, чтобы вешать обработчик на 1 000 кнопок, вы вешаете один обработчик на общий контейнер и определяете цель по event.target. Это уменьшает количество слушателей, упрощает управление жизненным циклом и часто улучшает производительность.
Когда делегирование особенно полезно:
- Длинные списки и таблицы, где элементы постоянно создаются и удаляются.
- Динамические интерфейсы, где контент генерируется на лету.
- Сценарии, где важна простота снятия обработчиков.
Пассивные слушатели — прокрутка без лагов
При прокрутке браузеру важно быстро понимать, можно ли продолжать прокрутку без ожидания обработчика. Пассивный слушатель сообщает браузеру, что обработчик не будет вызывать preventDefault. Тогда браузер может прокручивать плавнее и не блокировать кадр ожиданием.
Пассивные слушатели особенно важны для touch-событий и сценариев, где прокрутка должна быть максимально гладкой на мобильных устройствах. Это один из тех случаев, когда маленькая настройка влияет на реальный пользовательский опыт.
Загрузка и выполнение скриптов в браузере
Даже идеально написанный JavaScript может давать плохой пользовательский опыт, если он загружается и исполняется неудачно. В браузере важны не только алгоритмы, но и «логистика» доставки: когда именно скрипт скачан, когда разобран, в какой момент выполнен, блокирует ли он разбор HTML и отрисовку, и как он влияет на метрики вроде LCP и INP.
Понимание последовательности загрузки помогает объяснить типичные проблемы: «почему страница белая 2 секунды», «почему кнопка не кликается сразу», «почему скрипт работает локально, но ломается на проде», «почему в одном браузере всё ок, а в другом нет».
Как браузер загружает HTML, CSS и JS — общая последовательность
Упрощённая, но полезная для практики последовательность выглядит так:
- Браузер получает HTML и начинает парсить его сверху вниз, строя DOM-дерево.
- Находит ссылки на CSS и начинает скачивать стили, строя CSSOM.
- Находит скрипты и решает, когда и как их загрузить и выполнить, в зависимости от атрибутов.
- Когда DOM и CSSOM готовы в достаточной мере, браузер строит дерево рендеринга и выполняет layout и paint.
- После появления контента и выполнения скриптов браузер продолжает обрабатывать события, таймеры и сетевые ответы через event loop.
Практическая деталь: CSS влияет на отрисовку, а JavaScript может влиять на DOM. Если скрипт запускается до того, как нужные элементы созданы, вы получите null и ошибки. Если скрипт запускается слишком рано и блокирует поток, вы получите задержки интерактивности.
Тег script — блокирующее поведение по умолчанию
По умолчанию тег script ведёт себя блокирующе:
- Парсер HTML останавливается на месте тега script.
- Браузер загружает скрипт, если он внешний.
- Браузер выполняет скрипт.
- Только после выполнения продолжает парсить HTML дальше.
Почему так устроено: скрипт может менять документ через document.write или другие механизмы, поэтому браузер должен обеспечить предсказуемость порядка. Однако сегодня это поведение часто вредно для производительности, потому что задерживает построение DOM и показ контента.
Типичный симптом блокировки: вы видите долгую паузу до появления контента, а в DevTools вкладке Network и Performance видно, что выполнение скрипта совпадает с задержкой рендеринга.
defer и async — разница, когда что выбирать
Два ключевых атрибута внешних скриптов — defer и async. Они меняют порядок загрузки и выполнения.
defer — загрузить параллельно и выполнить после построения DOM
Скрипт с defer:
- Скачивается параллельно, не блокируя парсинг HTML.
- Выполняется после того, как HTML распарсен, то есть когда DOM построен.
- Сохраняет порядок выполнения относительно других defer-скриптов.
Когда выбирать defer: если скрипт зависит от DOM и должен выполняться после того, как элементы уже существуют. Это типичный вариант для большинства клиентских приложений.
async — загрузить параллельно и выполнить сразу после загрузки
Скрипт с async:
- Скачивается параллельно, не блокируя парсинг HTML.
- Как только загрузился, выполняется сразу, даже если парсинг HTML ещё идёт.
- Не гарантирует порядок выполнения относительно других async-скриптов.
Когда выбирать async: для независимых скриптов, которые не зависят от DOM и не должны выполняться в строго определённой последовательности. Типовые примеры — аналитика, счётчики, некоторые рекламные скрипты, где порядок между ними не критичен.
Риск async: если скрипт обращается к элементам, которых ещё нет, или зависит от другого скрипта, вы получите нестабильные ошибки, которые «то есть, то нет».
type=module — модули в браузере и их особенности
Скрипты с type=module включают модульную систему ESM прямо в браузере. У модулей есть несколько важных отличий от классических скриптов:
- Модули по умолчанию ведут себя похоже на defer — не блокируют парсинг и выполняются после разбора документа.
- У модуля своя область видимости — переменные не «падают» в глобальный window.
- Поддерживается import и export, а браузер строит граф зависимостей.
- Модули выполняются в строгом режиме, что уменьшает количество неявных ошибок.
Практический плюс модулей — предсказуемость и возможность статического анализа. Практический минус — необходимость учитывать особенности загрузки зависимостей и CORS, потому что импорты фактически инициируют сетевые запросы.
Динамический импорт — подгрузка по требованию
Динамический импорт через import() позволяет загружать код по необходимости. Это основа code splitting: вы не тянете весь код сразу, а подгружаете только то, что нужно пользователю в данный момент.
Типовые сценарии динамического импорта:
- Большие разделы приложения, куда пользователь заходит редко.
- Тяжёлые библиотеки, которые нужны только в отдельных функциях, например редактор графики.
- Функции админ-панели, которые не нужны обычному пользователю.
Важно: динамический импорт — это асинхронная операция. Она возвращает промис, а значит влияет на event loop и порядок выполнения.
Стратегии производительности — preconnect, preload, prefetch и приоритеты
Производительность загрузки — это не только размер файлов, но и приоритеты сети. Браузер решает, что скачивать раньше, что позже, какие соединения держать, как кэшировать. Для управления есть несколько стратегий.
preconnect — заранее подготовить соединение
preconnect помогает, когда вы точно знаете, что будете обращаться к домену, например к CDN. Он позволяет заранее выполнить DNS, TCP и TLS этапы, чтобы затем запросы стартовали быстрее.
preload — заранее скачать критичный ресурс
preload используется для критичных файлов, без которых страница не может быстро стать полезной. Это помогает, когда вы знаете, что ресурс понадобится очень скоро, но браузер сам может начать его скачивать поздно.
prefetch — скачать «на будущее»
prefetch — низкоприоритетная подсказка: ресурс пригодится позже, когда пользователь перейдёт в другой раздел. Prefetch полезен для ускорения навигации, но при плохой сети может оказаться пустой тратой трафика.
Приоритеты и здравый смысл
Ключевой принцип: повышайте приоритет только тому, что реально влияет на ранний пользовательский опыт. Если вы загрузите 10 файлов как preload, вы создадите конкуренцию и не выиграете ничего. Производительность — это баланс, а не «всё сразу».
Ошибки загрузки и диагностика — сеть, кэш и CORS
Проблемы загрузки скриптов чаще всего связаны с сетью, кешем и политиками безопасности. Типовые сценарии:
- 404 или 500 — файл не найден или сервер упал.
- Неверный MIME type — сервер отдаёт скрипт как text/plain или HTML, браузер отказывается исполнять.
- Кеширование — старый файл в кеше и новый HTML, несовместимые версии.
- CORS — запросы на другой домен блокируются, особенно для модулей и импортов.
- Смешанный контент — попытка загрузить скрипт по http на https странице.
Диагностика в DevTools обычно начинается с вкладки Network: там видно статус, заголовки, размер, время, кэш. Затем стоит смотреть Console: ошибки CORS и MIME type обычно проявляются именно там. Для модулей важно проверять, откуда тянутся зависимости и доступны ли они по нужным правилам.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Модули и организация кода
Современная разработка на JavaScript почти всегда модульная. Модули помогают разделять ответственность, уменьшать связанность, переиспользовать код и управлять зависимостями. Модульность также важна для оптимизаций: tree shaking, code splitting и кэширование бандлов работают лучше, когда код организован как граф зависимостей.
ESM модули — export, import и статический анализ
ESM — это стандартная модульная система JavaScript. Её ключевое преимущество — статичность: импорты и экспорты анализируются заранее, ещё до выполнения кода. Это даёт инструменты сборки и браузеру возможность строить граф зависимостей и оптимизировать загрузку.
Что важно знать про ESM:
- export и import работают на уровне модуля, а не функции.
- Импорты обычно должны быть на верхнем уровне файла, чтобы граф был предсказуемым.
- Tree shaking лучше работает с ESM, потому что можно понять, что реально используется.
Для новичков полезная мысль: ESM — это не «другая версия JavaScript», это способ организовать код так, чтобы он был масштабируемым.
CommonJS — почему до сих пор встречается
CommonJS — историческая модульная система Node.js, где используются require и module.exports. Она долго была стандартом на сервере и до сих пор встречается:
- В старых проектах и библиотеках npm.
- В инфраструктурном коде, который запускался в средах без полноценного ESM.
- В некоторых инструментах, где важна совместимость и простота.
Главное отличие: CommonJS динамический. require выполняется в момент исполнения, а значит статический анализ сложнее, tree shaking хуже, и порядок загрузки зависит от времени выполнения.
Импорт JSON и ресурсов — реальные сценарии и ограничения окружений
На практике разработчики хотят импортировать не только JS, но и JSON, изображения, стили, конфиги. Здесь важно понимать: стандарт JavaScript не обязан поддерживать импорт любых ресурсов. Это зависит от окружения и инструмента сборки.
Типовые сценарии:
- Импорт JSON как конфигурации — удобно для локализации, настроек, данных.
- Импорт CSS и изображений — чаще всего через бандлер, который превращает импорт в ссылку или инлайн.
- Импорт текстовых файлов — через специфические плагины или загрузчики.
Ограничение: если вы пишете код, который должен работать без сборки прямо в браузере, вам придётся соблюдать правила браузерных модулей и доступных форматов. Поэтому в «чистом браузере» многие импорты ресурсов требуют дополнительной инфраструктуры.
Top level await — когда он полезен и какие у него риски
Top level await позволяет использовать await на верхнем уровне модуля, без обёртки в async функцию. Это удобно, когда модуль должен асинхронно подготовить что-то перед экспортом: например, загрузить конфигурацию или инициализировать соединение.
Но есть риски:
- Top level await может задерживать выполнение зависимых модулей, потому что они ждут завершения.
- Сложнее прогнозировать порядок и время инициализации приложения.
- Если в цепочке модулей много top level await, старт может стать медленнее.
Практическое правило: используйте top level await точечно, когда это реально упрощает архитектуру и не создаёт «бутылочное горлышко» на старте.
Циклические зависимости — как возникают и как их распознавать
Циклическая зависимость возникает, когда модуль A импортирует B, а B импортирует A напрямую или через цепочку. В графе зависимостей появляется цикл, и это может приводить к неожиданным undefined и частично инициализированным экспортам.
Как циклы появляются чаще всего:
- Когда два модуля начинают делить ответственность и взаимно тянут функции друг друга.
- Когда общий код не вынесен в отдельный модуль, и каждый пытается «подхватить» кусок из другого.
- Когда есть «бог-модуль», который импортируют все, и он импортирует половину проекта обратно.
Как распознавать:
- Ошибки вида cannot access before initialization и неожиданные undefined в экспортах.
- Предупреждения сборщика о circular dependencies.
- Странные баги старта приложения, которые исчезают при изменении порядка импортов.
Типовое решение — вынести общую часть в третий модуль и убрать взаимную зависимость.
Браузер против Node.js — ключевые отличия исполнения
JavaScript в браузере и JavaScript в Node.js — это один язык, но разные рантаймы. Если вы понимаете, что одинаково, а что разное, вы сможете писать переносимый код, быстрее находить баги и не попадаться на «почему это работает у меня, но не работает на сервере».
Что одинаково — язык, стандартные объекты и базовые концепции
Обычно одинаково:
- Синтаксис и семантика ECMAScript.
- Встроенные объекты языка — Array, Object, Map, Set, Promise, RegExp и другие.
- Базовые механики — прототипы, классы, замыкания, область видимости.
- Модель асинхронности — промисы и микрозадачи как концепция.
Это хорошая новость: вы можете учить ядро языка один раз и применять в разных средах.
Что разное — глобальные объекты, API и модель безопасности
Различия начинаются там, где язык взаимодействует с миром:
- В браузере глобальный объект исторически window, в Node.js — global, универсальный вариант — globalThis.
- В браузере есть DOM, история, события интерфейса, песочница и политики доступа.
- В Node.js есть fs, process, сеть, управление потоками и процессами.
- Модель безопасности в браузере строгая, в Node.js ответственность лежит на разработчике и окружении запуска.
Node.js event loop и I/O — почему сервер выдерживает тысячи соединений
Node.js построен вокруг событийной модели и неблокирующего I/O. Идея проста: вместо того чтобы выделять поток под каждый запрос, Node.js использует event loop и коллбэки или промисы для обработки завершений операций. Пока сеть или диск «заняты», JavaScript-поток свободен и может обслуживать другие задачи.
Почему это работает хорошо:
- I/O операции не блокируют JS-поток, они завершаются в фоне и возвращают результат через очередь событий.
- Один процесс может обслуживать много соединений, если обработчики не делают тяжёлой синхронной работы.
- Потоки данных и backpressure позволяют обрабатывать большие ответы без загрузки всей памяти.
Но есть важный предел: если вы делаете тяжёлые вычисления синхронно в обработчике запросов, вы блокируете event loop, и всё приложение становится медленным для всех клиентов.
Таймеры, микрозадачи и порядок — где встречаются отличия
Хотя концепция очередей одинаковая, детали порядка могут отличаться. В браузере цикл событий связан с рендерингом, а в Node.js — с I/O и внутренними фазами обработки. Это означает, что некоторые тонкие сценарии с таймерами и микрозадачами могут вести себя иначе.
Практическое правило: не строить архитектуру, которая зависит от тонкого порядка событий. Если логика корректна только при определённом микро-порядке, она хрупкая. Лучше делать явное управление: await, явные очереди, отмена, маркеры актуальности.
Потоки в Node.js — worker threads, child process и когда они нужны
Когда одного event loop мало, Node.js даёт способы настоящего параллелизма:
- worker threads — потоки внутри процесса, полезны для вычислений и тяжёлых задач.
- child process — отдельные процессы, полезны для изоляции, масштабирования и запуска внешних утилит.
Когда это нужно:
- CPU-bound задачи — шифрование, сжатие, обработка изображений, большие вычисления.
- Обработка больших массивов данных, где время выполнения измеряется сотнями миллисекунд и секундами.
- Сценарии, где важна изоляция ошибок и контроль ресурсов.
Серверные паттерны — streams, backpressure и обработка больших данных
На сервере часто нужно работать с большими данными: файлы, видео, архивы, логи. Если вы пытаетесь читать всё в память, вы быстро получите рост RAM и сбои. Потоки решают это: данные идут кусками, и вы можете управлять скоростью потребления.
Backpressure означает, что потребитель может сказать производителю «пожалуйста, притормози». Это защищает систему от переполнения памяти и очередей.
Практические примеры:
- Отдача файла пользователю без загрузки целиком в память.
- Парсинг больших CSV и JSON-логов кусками.
- Проксирование данных между сервисами с контролем потока.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠
Воркеры и фоновая работа — как разгрузить главный поток
В браузере главный поток ценен: он отвечает за реакцию интерфейса. Если вы делаете тяжёлую работу в нём, пользователь видит лаги. Воркеры и специализированные контексты позволяют вынести часть работы в фон и оставить главный поток для UI.
Web Workers — вычисления без блокировки UI
Web Worker — это отдельный поток исполнения JavaScript без доступа к DOM. Он подходит для вычислений и обработки данных. Главный поток общается с воркером через сообщения.
Когда воркер полезен:
- Парсинг и обработка больших данных, например 20 000–500 000 строк.
- Криптография, компрессия, вычислительные алгоритмы.
- Подготовка данных для графиков и визуализаций.
Ограничение: воркер не может напрямую менять DOM, потому что иначе возникли бы сложные проблемы синхронизации. Взаимодействие идёт через сообщения и передачу данных.
Service Workers — офлайн, кэширование и контроль запросов
Service Worker работает как прокси между страницей и сетью. Он может перехватывать запросы, отдавать ответы из кеша, обеспечивать офлайн-режим и фоновые синхронизации. Это основа PWA и стратегий надёжной доставки контента.
Что реально даёт Service Worker:
- Офлайн-страницы и кеширование ассетов.
- Быстрый старт за счёт отдачи критичных ресурсов из кеша.
- Контроль стратегии обновлений и версионирования кеша.
Важно: Service Worker требует дисциплины версий. Если вы неправильно обновляете кеш, пользователи могут видеть старый код, несовместимый с новым сервером.
Worklets — аудио и графика с низкими задержками
Worklets — специализированные контексты для задач, где критичны задержки. Например, AudioWorklet для обработки аудио в реальном времени. Они менее универсальны, чем Web Worker, но дают более подходящие гарантии для своих задач.
Передача данных — structured clone и transferable objects
Данные между потоками копируются не всегда буквально. Есть механизм structured clone, который умеет копировать многие структуры данных. Но копирование больших массивов может быть дорого.
Transferable objects позволяют передать владение буфером данных без копирования, что критично для производительности. Например, можно передать ArrayBuffer воркеру так, что в главном потоке он станет недоступен, а воркер получит его мгновенно.
Практический вывод: если вы гоняете между потоками десятки мегабайт данных, выбирайте transfer, иначе вы получите лишние копии и рост времени выполнения.
SharedArrayBuffer и Atomics — общий доступ и синхронизация
SharedArrayBuffer позволяет нескольким потокам видеть одну память. Это даёт максимальную производительность, но требует синхронизации через Atomics. Без Atomics вы получите гонки данных и неконсистентные состояния.
Когда это оправдано:
- Высокопроизводительные вычисления с частым обменом данными.
- Реализация очередей и буферов между потоками.
- Сценарии, где стоимость копирования слишком высока.
FAQ — самые частые вопросы о том, как работает JavaScript
Почему JavaScript называют однопоточным и что именно однопоточно
JavaScript называют однопоточным, потому что в рамках одного контекста исполнения движок выполняет инструкции последовательно в одном потоке. Однопоточно именно выполнение JavaScript-кода, то есть выполнение функций и выражений на call stack. При этом окружение вокруг может быть многопоточным: сеть, таймеры, декодирование, рендеринг, файловая система на сервере работают параллельно и возвращают результаты в event loop через очереди задач.
Чем движок отличается от рантайма
Движок — это компонент, который парсит код, строит AST, генерирует байткод или машинный код, выполняет инструкции и управляет памятью через сборщик мусора. Рантайм — это движок плюс окружение и API, которые связывают код с системой. В браузере рантайм даёт DOM, события и Web API, в Node.js — файловую систему, сеть, процессы и потоки данных.
Что такое ECMAScript и кто решает, каким будет JavaScript
ECMAScript — стандарт, который описывает язык JavaScript на уровне синтаксиса и семантики, включая типы, объекты, функции, промисы и правила выполнения. Решения по развитию стандарта принимает комитет TC39. Браузеры и серверные рантаймы реализуют этот стандарт и добавляют свои API, которые не входят в ECMAScript, например DOM или fs.
Как браузер понимает, когда выполнять скрипт
Браузер парсит HTML сверху вниз и реагирует на тег script. По умолчанию внешний script блокирует парсер: браузер останавливает разбор HTML, загружает скрипт и выполняет его, затем продолжает парсинг. Если указан defer, скрипт загружается параллельно и выполняется после построения DOM. Если указан async, скрипт выполняется сразу после загрузки, даже если HTML ещё парсится.
Почему без defer страница может зависать при загрузке
Без defer внешний script блокирует парсинг HTML и часто задерживает первую отрисовку и интерактивность. Если скрипт большой или выполняет тяжёлую инициализацию, главный поток занят, и пользователь видит «белый экран» или «не кликается». Особенно заметно на слабых устройствах, где одинаковый код может выполняться в 5–10 раз дольше, чем на мощном компьютере.
В чем разница между async и defer
defer сохраняет порядок скриптов и выполняет их после того, как HTML распарсен и DOM построен. async не гарантирует порядок и выполняет скрипт сразу после загрузки, что может произойти до завершения парсинга HTML. defer выбирают для логики, зависящей от DOM и порядка модулей. async используют для независимых скриптов, где порядок не важен, например для аналитики.
Почему type=module меняет порядок загрузки
Скрипт type=module загружается и выполняется по правилам модулей. Он имеет собственную область видимости, работает в строгом режиме и строит граф зависимостей через import. Модули по умолчанию ведут себя похоже на defer, но дополнительно инициируют загрузку импортируемых модулей и могут менять картину сетевых приоритетов и времени выполнения.
Что происходит при синтаксической ошибке в скрипте
Если в скрипте SyntaxError, код не сможет быть распарсен, и выполнение этого скрипта не начнётся. В блокирующем режиме это может остановить дальнейшее выполнение зависимых частей приложения. В модульной системе ошибка в одном модуле может сорвать загрузку графа импортов, что выглядит как «ничего не работает» без явных сообщений, если не смотреть в консоль.
Как JavaScript превращается в машинный код
Современные движки обычно проходят цепочку этапов: исходник парсится в AST, затем превращается в байткод, который интерпретируется. Когда движок видит, что участок кода стал горячим и выполняется часто, он может скомпилировать его в оптимизированный машинный код через JIT. При изменении предположений о типах или структуре данных движок может деоптимизировать код и вернуться к более безопасному пути выполнения.
Всегда ли JavaScript интерпретируемый язык
На практике JavaScript не только интерпретируемый и не только компилируемый. Он гибридный. Первый запуск часто идёт через интерпретацию байткода, а горячие участки могут компилироваться в машинный код. Поэтому фраза «JavaScript интерпретируемый язык» слишком упрощает картину и не объясняет современную производительность.
Что такое JIT компиляция простыми словами
JIT — это компиляция «по ходу работы». Движок сначала запускает код быстро и безопасно, собирая статистику о типах и ветках. Когда он видит, что функция вызывается много раз, он оптимизирует её под реальные данные и превращает в быстрый машинный код. Это ускоряет работу, но требует стабильности форм данных и типов.
Почему движок иногда деоптимизирует код
Оптимизация строится на предположениях: что типы стабильны, структура объектов не меняется, ветки кода предсказуемы. Если предположения ломаются, движок может деоптимизировать и вернуться к более универсальному пути. Это может дать эффект «код был быстрым, а потом стал медленным» без изменения исходника, если входные данные стали другими.
Что такое байткод и зачем он нужен
Байткод — промежуточное представление кода, более компактное и удобное для интерпретации, чем исходный текст. Он ускоряет старт и упрощает работу интерпретатора. Движок может интерпретировать байткод сразу, а затем оптимизировать горячие участки в машинный код.
Почему один и тот же код быстрее в одном браузере и медленнее в другом
Разные браузеры используют разные версии движков, разные стратегии JIT и сборки мусора, а также разные реализации Web API и рендеринга. Даже если стандарт языка одинаковый, детали оптимизаций, кеширования и планирования могут отличаться. Дополнительно влияет окружение: на одном устройстве CPU быстрее, на другом медленнее, и разница может быть кратной.
Что такое call stack и как он связан с ошибками
Call stack — стек вызовов, где движок хранит активные вызовы функций. Ошибки часто показывают стектрейс, то есть цепочку вызовов, которая привела к сбою. Понимание стека помогает найти первопричину: обычно нужно искать первую строку, относящуюся к вашему коду, и идти к месту, где сформировались неверные данные.
Почему возникает Maximum call stack size exceeded
Эта ошибка появляется, когда глубина рекурсии или цепочка вызовов слишком большая, и стек переполняется. Частая причина — рекурсивная функция без корректного условия остановки или обработка данных, где рекурсия уходит слишком глубоко. Решение — переписать на итерацию или использовать собственный стек в массиве.
Что такое execution context
Execution context — контекст выполнения, в котором движок хранит информацию о переменных, областях видимости, this и состоянии выполнения. Глобальный контекст создаётся при старте, контекст функции создаётся при вызове. Контекст связан с тем, как работает hoisting и почему переменные доступны определённым образом.
Что такое лексическое окружение и scope chain
Лексическое окружение хранит переменные и ссылки на внешние окружения. Scope chain — цепочка областей видимости, по которой движок ищет идентификаторы. Поиск идёт от текущего окружения наружу. Это объясняет замыкания и доступ внутренних функций к переменным внешних функций.
Что такое hoisting и почему var ведет себя странно
Hoisting — поднятие объявлений. Движок регистрирует объявления до выполнения строк кода. var поднимается как объявление и получает значение undefined до присваивания, поэтому переменная «существует», но ещё не инициализирована тем значением, которое вы ожидаете. let и const тоже поднимаются, но попадают в TDZ, поэтому доступ до инициализации запрещён.
Что такое temporal dead zone
Temporal Dead Zone — зона, где переменная let или const уже существует в области видимости, но к ней нельзя обращаться, пока не выполнится строка инициализации. Это предотвращает ошибки, когда переменная неожиданно равна undefined, как это бывает с var.
Как работает this и почему он теряется
this зависит от способа вызова функции. Если метод объекта вызвать как obj.method(), this будет obj. Если тот же метод передать как коллбэк без привязки, вызов произойдёт как обычная функция, и this может стать undefined. Это и называют «потерей this». Решение — bind, стрелочные функции-обёртки или иная архитектура вызова.
Почему стрелочные функции по-другому работают с this
Стрелочные функции не имеют собственного this. Они берут this из внешнего лексического окружения, в котором были созданы. Это удобно в коллбэках, потому что снижает риск потери контекста. Но стрелочные функции не подходят там, где нужен динамический this или где требуется использование new.
Чем отличаются function declaration и function expression
Function declaration поднимается полностью, поэтому функцию можно вызвать до места объявления. Function expression создаёт функцию как значение, обычно присваивая её переменной. Поведение с hoisting зависит от типа переменной: при var будет undefined до присваивания, при let и const будет TDZ до инициализации.
Что такое замыкание и почему оно удерживает память
Замыкание — это способность функции помнить лексическое окружение, в котором она была создана. Если внутренняя функция использует переменные внешней, эти переменные остаются достижимыми, даже когда внешняя функция завершилась. Это может удерживать память, если через замыкание сохраняется ссылка на большой объект или DOM-узел.
Что такое heap и чем он отличается от стека
Стек хранит контексты выполнения и ссылки, связанные с вызовами функций. Heap хранит объекты, массивы, функции и другие структуры, которые живут в динамической памяти. Когда вы присваиваете объект переменной, переменная обычно хранит ссылку на объект в heap.
Как работает сборка мусора
Сборщик мусора освобождает память объектов, которые стали недостижимыми. Основной принцип — достижимость от корней, таких как глобальные объекты и активные контексты выполнения. Многие движки используют поколенческую модель: молодые объекты собираются часто и быстро, старые — реже, но потенциально с большими паузами.
Почему память не освобождается сразу
Сборка мусора запускается периодически, а не после каждого удаления ссылки. Движок выбирает момент, чтобы балансировать между нагрузкой CPU и расходом памяти. Поэтому вы можете видеть, что память растёт, а потом падает скачком, когда проходит цикл сборки.
Что такое WeakMap и когда он нужен
WeakMap хранит ключи как слабые ссылки, поэтому наличие записи в WeakMap не удерживает объект в памяти. Это полезно для кешей и метаданных, которые привязаны к объектам, но не должны мешать сборке мусора. WeakMap часто используют, чтобы избежать утечек в сценариях, где обычный Map удерживал бы ключи навсегда.
Почему setInterval может накапливать задержку
setInterval не гарантирует строгий период. Если коллбэк выполняется дольше интервала или event loop перегружен, следующий запуск задерживается, и происходит дрейф. В итоге интервалы становятся нерегулярными. Для более точного расписания часто используют setTimeout с вычислением следующего времени запуска.
Почему setTimeout с нулем не выполняется мгновенно
setTimeout(0) ставит задачу в очередь macrotask. Она выполнится не раньше завершения текущей задачи и выполнения микрозадач. Если текущая задача тяжёлая или очередь занята, задержка может быть заметной. В фоне вкладки браузер может увеличивать минимальную задержку таймеров.
Что такое macrotask
Macrotask — задача в очереди задач, например обработчик события, setTimeout, setInterval или сообщение. Каждая macrotask выполняется целиком, и только после этого event loop переходит к следующей задаче.
Что такое microtask
Microtask — задача более высокого приоритета, которая выполняется после текущей macrotask и до перехода к следующей. Промисы планируют продолжение через микрозадачи, поэтому then и catch выполняются в microtask queue.
Почему Promise then выполняется раньше setTimeout
Потому что then попадает в microtask queue, а setTimeout — в macrotask queue. После завершения текущего синхронного выполнения event loop сначала опустошает очередь микрозадач, и только потом берёт следующую macrotask, где будет таймер.
Почему тяжелые вычисления роняют FPS
Если вычисления занимают больше бюджета кадра, браузер пропускает кадры. При 60 FPS на кадр приходится около 16,7 мс, и если JavaScript выполняется 40–100 мс подряд, анимации дергаются, прокрутка лагает, а клики обрабатываются с задержкой.
Как правильно анимировать в браузере
Для анимаций важно синхронизироваться с рендерингом, минимизировать работу в каждом кадре и избегать свойств, которые вызывают дорогой layout. Практический подход — обновлять состояние в requestAnimationFrame, а визуальные изменения делать так, чтобы браузеру было проще композить кадры.
Когда использовать requestAnimationFrame
requestAnimationFrame используют, когда обновление должно происходить перед отрисовкой кадра: анимации, плавные перемещения, синхронизация с прокруткой, визуальные индикаторы. Он помогает избежать лишних перерисовок и делает движение более стабильным.
Почему async await может замедлять если злоупотреблять
async await удобен, но каждый await разбивает выполнение на этапы и добавляет планирование через микрозадачи. Если вы ставите await внутри циклов на 5 000–50 000 итераций или делаете много мелких await подряд, вы получаете накладные расходы и последовательность вместо параллельного выполнения. Лучше группировать независимые операции и ждать их вместе.
Чем Promise.all отличается от Promise.allSettled
Promise.all завершается успешно только если все промисы успешны, иначе падает на первой ошибке. Promise.allSettled ждёт завершения всех промисов и возвращает результаты независимо от успеха или ошибки. all используют, когда без любой части результата нельзя продолжать. allSettled используют, когда важно собрать всё, даже если часть задач провалилась.
Как правильно обрабатывать ошибки в async await
Ошибки обрабатывают через try catch вокруг await. Внутри блока важно различать типы ошибок: отмена, таймаут, сетевой сбой, ошибки сервера и ошибки парсинга данных. Тогда вы можете показать пользователю корректное сообщение и выбрать правильную стратегию повтора или завершения операции.
Почему try catch не ловит ошибку в промисе без await
Потому что try catch ловит синхронные исключения в текущем стеке. Ошибка промиса возникает асинхронно, позже, когда промис завершится. Если вы не await-ите промис внутри try catch и не ставите catch на сам промис, ошибка уйдёт в необработанные отклонения.
Что такое DOM и почему операции с ним дорогие
DOM — объектная модель документа, дерево узлов страницы. Операции с DOM могут быть дорогими, потому что изменения могут влиять на layout и paint. Кроме того, чтение некоторых свойств может вынудить браузер синхронно пересчитать компоновку. Поэтому частые и несогласованные чтения и записи DOM вызывают лаги.
Что такое reflow и repaint
Reflow обычно означает перерасчёт layout, когда браузер заново вычисляет размеры и позиции элементов. Repaint — перерисовка пикселей, когда визуальный вид меняется, но геометрия может не меняться. Reflow обычно дороже, потому что может затронуть большое количество элементов.
Как уменьшить количество перерисовок
Нужно группировать изменения, избегать лишних чтений layout между записями, использовать батчинг обновлений, минимизировать количество операций DOM и по возможности переносить анимации на свойства, которые не требуют перерасчёта layout.
Что такое capturing
Capturing — фаза захвата, когда событие идёт от корня документа вниз к целевому элементу. Она используется реже, но полезна в сценариях, где нужно перехватить событие до того, как оно дойдёт до цели.
Что такое делегирование событий и зачем оно нужно
Делегирование — техника, когда вы ставите один обработчик на общий контейнер и определяете реальную цель по event.target. Это уменьшает количество слушателей, упрощает управление жизненным циклом, улучшает производительность и снижает риск утечек при динамическом создании элементов.
Почему addEventListener может приводить к утечкам
Если вы добавили обработчик и не сняли его, обработчик остаётся достижимым, а вместе с ним могут удерживаться связанные замыкания, объекты и DOM-узлы. В приложениях с динамическими компонентами это приводит к росту памяти и дублированию реакций на события.
Что такое пассивные обработчики событий
Пассивный обработчик сообщает браузеру, что вы не будете вызывать preventDefault. Это важно для прокрутки, потому что браузер может выполнять скролл без ожидания обработчика, снижая лаги. Пассивные слушатели особенно актуальны для touch и wheel событий.
Почему браузер ограничивает доступ к файлам и системе
Браузер защищает пользователя и изолирует сайты. Сайт не должен иметь возможность читать файлы, историю, настройки или данные других сайтов без разрешения. Поэтому доступ к файлам возможен только через явный выбор пользователем, а доступ к устройствам требует разрешений.
Что такое CORS и почему запросы блокируются
CORS — механизм, который позволяет серверу разрешить запросы с других источников. Если сервер не отправил нужные заголовки, браузер может заблокировать доступ к ответу для JavaScript. Запрос может быть отправлен и ответ может прийти, но доступ к данным будет закрыт, и вы увидите ошибку в консоли.
Что такое CSP и как он помогает от XSS
CSP — политика безопасности контента, которая задаёт, откуда разрешено загружать скрипты и можно ли выполнять inline код. CSP помогает от XSS, потому что даже если злоумышленник внедрит HTML с вредоносным скриптом, браузер может запретить его выполнение, если это нарушает политику.
Почему нельзя доверять данным из input
Данные из input — это пользовательский ввод, который может быть любым. Он может содержать неожиданные символы, скрипты, слишком длинные строки и попытки инъекций. Поэтому ввод нужно валидировать и экранировать, а критичные проверки делать на сервере.
Как устроены модули в JavaScript
Модули организуют код как набор файлов с export и import. В ESM импорты анализируются заранее, строится граф зависимостей, и рантайм загружает нужные модули. Модули имеют собственную область видимости, что снижает риск конфликтов глобальных переменных.
Чем ESM отличается от CommonJS
ESM статический: import и export анализируются заранее, что помогает оптимизациям и tree shaking. CommonJS динамический: require выполняется во время исполнения, что сложнее для статического анализа. CommonJS часто встречается в старых Node.js проектах и библиотеках.
Что такое tree shaking и почему он зависит от ESM
Tree shaking — удаление неиспользуемого кода на этапе сборки. Он работает лучше с ESM, потому что статические импорты позволяют понять, какие экспорты реально используются. В динамической системе сложнее гарантировать, что часть кода точно не понадобится.
Почему dynamic import помогает ускорить загрузку
Dynamic import позволяет загрузить модуль только тогда, когда он реально нужен. Это уменьшает стартовый объём JavaScript и время выполнения инициализации. Особенно полезно для редких сценариев и тяжёлых библиотек.
Что такое top level await и когда он уместен
Top level await позволяет использовать await на верхнем уровне модуля. Он уместен, когда модуль должен асинхронно подготовить данные перед использованием, например загрузить конфиг. Риск в том, что он задерживает выполнение зависимых модулей, поэтому его используют точечно.
Что такое циклические зависимости и как их избегать
Циклические зависимости возникают, когда модули импортируют друг друга напрямую или через цепочку. Это может приводить к частичной инициализации экспортов и неожиданным undefined. Избегают циклов, вынося общую логику в отдельный модуль и делая зависимости однонаправленными.
Чем JavaScript в Node.js отличается от JavaScript в браузере
Ядро языка одинаковое, но разные рантаймы. В браузере есть DOM, события интерфейса, песочница и политики безопасности. В Node.js есть файловая система, процессы, сеть и возможности работы с потоками и воркерами. Отличается и жизненный цикл: серверный процесс может жить неделями, а вкладка браузера зависит от пользователя.
Что такое libuv и зачем он Node.js
libuv — библиотека, которая обеспечивает неблокирующее I/O и event loop в Node.js на системном уровне. Она управляет очередями событий, таймерами и взаимодействием с операционной системой, позволяя JavaScript-потоку получать события о завершении операций ввода-вывода без блокировки.
Почему Node.js хорошо подходит для I O задач
Node.js эффективен для I/O, потому что не блокирует поток на ожидании сети или диска. Он обслуживает множество соединений через event loop и событийную модель. Если обработчики запросов быстрые и не делают тяжёлую синхронную работу, один процесс может выдерживать высокую конкурентность.
Когда Node.js не подходит и лучше выбрать другой подход
Node.js хуже подходит для тяжёлых CPU-bound задач, где нужно много вычислений в одном потоке. Если сервер должен постоянно считать сложную математику, обрабатывать изображения или выполнять длительные вычисления, лучше использовать worker threads, выносить вычисления в отдельные сервисы или выбирать платформы, где многопоточность встроена в модель по умолчанию.
Что такое worker threads в Node.js и зачем они нужны
Worker threads — потоки, которые позволяют выполнять вычисления параллельно основному event loop. Они нужны, чтобы не блокировать обработку запросов тяжёлыми расчётами. Типовые сценарии — шифрование, сжатие, обработка данных, генерация отчётов и любая работа, которая может занимать сотни миллисекунд и секунды.
Что такое Web Worker и когда его использовать
Web Worker — отдельный поток JavaScript в браузере без доступа к DOM. Его используют для вычислений и обработки данных, чтобы не блокировать UI. Это особенно полезно при работе с большими массивами, парсингом, аналитикой и подготовкой данных для визуализаций.
Чем Service Worker отличается от Web Worker
Service Worker работает как прокси между страницей и сетью и может перехватывать запросы, кешировать и обеспечивать офлайн-режим. Web Worker — вычислительный помощник для фоновой работы без UI. Service Worker живёт по своим правилам и связан с сетевой моделью и кешем, а Web Worker связан с вычислениями и сообщениями.
Как воркеры передают данные и почему копирование может быть дорогим
По умолчанию данные копируются через structured clone. Для больших объёмов это может быть дорого по времени и памяти, потому что создаются копии. Если вы передаёте десятки мегабайт, копирование может занять заметное время и увеличить потребление памяти.
Что такое transferable objects
Transferable objects позволяют передать владение объектом данных, например ArrayBuffer, в другой поток без копирования. Передача происходит быстро, но исходный объект становится недоступен в отправителе. Это полезно для производительности при больших данных и частом обмене.
Что такое SharedArrayBuffer и Atomics и когда они нужны
SharedArrayBuffer позволяет нескольким потокам иметь общий доступ к памяти. Atomics нужны для синхронизации, чтобы избежать гонок данных. Это используют в высокопроизводительных сценариях, где копирование слишком дорого и нужен быстрый обмен состоянием между потоками.
Что такое WebAssembly и зачем он рядом с JavaScript
WebAssembly — бинарный формат, который позволяет запускать низкоуровневый код в браузере и некоторых рантаймах рядом с JavaScript. Он полезен там, где нужны высокопроизводительные вычисления, например обработка видео, аудио, сложная математика и компиляция существующих библиотек. JavaScript при этом остаётся языком интеграции, управления UI и логики приложения.
Какие метрики важны для фронтенда — LCP, INP, CLS и роль JS
LCP связан со скоростью показа основного контента и может ухудшаться из-за блокирующих скриптов и тяжёлого CSS. INP отражает задержку взаимодействий и напрямую зависит от того, есть ли длинные задачи на главном потоке. CLS связан со сдвигами макета и часто зависит от того, как загружаются ресурсы и когда меняется DOM. JavaScript влияет на все три метрики через загрузку, выполнение, работу с DOM и управление событиями.
Какие инструменты помогают искать узкие места в коде
В браузере ключевые инструменты — DevTools: вкладки Network, Performance и Memory. На сервере — профилировщики CPU, инструменты мониторинга, метрики процессов и трассировка запросов. Дополнительно полезны source maps, логирование с уровнями и сбор ошибок в централизованной системе.
Как читать Performance профайл в DevTools
В профиле смотрят на длинные задачи, время в скриптах, события и этапы рендеринга. Полезно искать участки, где много времени уходит в конкретные функции, и проверять, не вызывают ли они лишний layout. Цель — уменьшить длительность задач, оптимизировать DOM и распределить работу так, чтобы UI оставался отзывчивым.
Почему console.log может влиять на производительность
console.log — не бесплатная операция. Логирование в цикле на 10 000–100 000 итераций может резко замедлить выполнение, потому что браузер или Node.js должны обработать вывод и сериализацию объектов. В DevTools некоторые объекты логируются с «живыми ссылками», что тоже может влиять на память. В продакшене логирование должно быть дозированным и управляемым по уровням.
Как подготовиться к собеседованию по внутренностям JavaScript
Лучше всего готовиться через практику: разбирать порядок выполнения, event loop, микрозадачи и таймеры, уметь объяснять this, замыкания, hoisting и TDZ. Полезно уметь читать стектрейсы и понимать, почему появляется переполнение стека. Отдельно ценится умение говорить о производительности: длинные задачи, утечки памяти, профилирование.
🟠🟠🟠 ВЫБЕРИТЕ ЛУЧШИЙ КУРС по JAVASCRIPT 🟠🟠🟠