Skip to content
Назад в блог

От JSMin до Terser: что минификация делает с JavaScript

6 мин. чтения
От JSMin до Terser: что минификация делает с JavaScript

Читаемый JavaScript и JavaScript, который выгодно отправлять пользователю, не обязаны выглядеть одинаково. В исходниках нужны осмысленные имена, комментарии и структура для отладки; браузеру достаточно программы с тем же наблюдаемым поведением. Минификация использует эту разницу для уменьшения файла, но современная production-сборка не ограничивается удалением пробелов. Она отдельно решает, какой код вообще попадёт в bundle, как сократить оставшиеся выражения и каким способом сжать полученные байты при передаче по сети.

JSMin удалял только то, что не требовалось интерпретатору

В 2001 году Дуглас Крокфорд выпустил JSMin, небольшой потоковый фильтр для JavaScript. Он заменял комментарии и лишние пробельные символы, но не строил синтаксическое дерево и не пытался переписать алгоритм. JSMin различал строковые и регулярные литералы, а переводы строк удалял осторожно, поскольку автоматическая расстановка точек с запятой делает некоторые переносы значимыми.

Такой подход хорошо соответствовал раннему вебу: один подготовленный скрипт можно было пропустить через фильтр непосредственно перед публикацией. По оценке Крокфорда, типичный файл становился примерно вдвое меньше, хотя конкретный результат зависел от количества комментариев и форматирования. Важно и другое его уточнение: JSMin затруднял чтение как побочный эффект, но не называл это обфускацией.

Уже тогда уменьшение исходного текста не заменяло транспортное сжатие. Крокфорд рекомендовал после JSMin применять gzip, потому что повторяющиеся имена и синтаксис хорошо сжимаются даже после удаления пробелов. Различие сохранилось до сих пор: минифицированный файл является готовой JavaScript-программой, а ответ с Content-Encoding: gzip или br браузер сначала распаковывает.

AST превратил удаление символов в преобразование программы

Потоковый фильтр видит соседние символы, но почти ничего не знает о связях между переменными, выражениями и областями видимости. В 2010 году Михаил Базон выпустил UglifyJS, написанный на JavaScript набор из parser, compressor и beautifier. Инструмент разбирал исходник в Abstract Syntax Tree, поэтому мог принимать решения на уровне языка: сокращать локальные имена, сворачивать константные выражения, объединять инструкции и отбрасывать недостижимые ветки.

Разделение на этапы объясняет современную терминологию. Парсер превращает текст в AST; compressor ищет более короткую эквивалентную форму; mangler переименовывает идентификаторы там, где это допустимо; printer снова создаёт JavaScript с минимальным количеством синтаксического шума. UglifyJS2 стал почти полной переработкой первой версии и закрепил эту архитектуру в сборочных инструментах Node.js.

Похожую градацию хорошо показывает Closure Compiler. Его простой режим убирает пробелы и сокращает локальные имена, а advanced-режим анализирует программу глобально, удаляет недостижимый код и выполняет inlining. Цена агрессивного анализа состоит в дополнительных предположениях: внешние имена, динамический eval() или смешение обработанного и необработанного кода могут сделать преобразование небезопасным. Минификатор поэтому не «понимает программу полностью», а доказывает конкретные преобразования внутри заданной модели.

Новый синтаксис потребовал нового сопровождаемого парсера

До ECMAScript 2015 большая часть production-кода после транспиляции укладывалась в синтаксис ES5. Затем в повседневный JavaScript вошли стрелочные функции, let и const, классы, деструктуризация, шаблонные строки, модули и новые конструкции следующих ежегодных выпусков. Минификатор не может безопасно переписать выражение, которое его парсер не способен разобрать, поэтому поддержка языка стала постоянной работой, а не однажды решённой задачей.

UglifyJS продолжил развивать ES5-ветку, а для нового синтаксиса существовал проект uglify-es. Когда его сопровождение прекратилось, в 2018 году появился Terser. Документация проекта прямо называет его форком uglify-es, сохранившим значительную совместимость с API и CLI UglifyJS 3, но рассчитанным на ES6 и более новые версии языка. Причиной форка была не одна конкретная ошибка и не абстрактная «драма ES6», а практическая необходимость поддерживать меняющуюся грамматику и оптимизации поверх неё.

Terser сохранил знакомые стадии compress, mangle и format. Он умеет учитывать модульный режим, сохранять имена функций и классов, выпускать source maps и ограничивать рискованные преобразования настройками. Его распространение связано не с тем, что минификация снова стала отдельной профессией, а с включением минификатора в bundler-пайплайн, где исходники уже прошли транспиляцию, разрешение модулей и другие этапы сборки.

У production-пайплайна несколько разных способов уменьшить загрузку

Tree shaking работает раньше минификации и смотрит на граф модулей. Статические import и export позволяют сборщику увидеть, какие экспорты используются, но побочные эффекты усложняют решение: импорт CSS, регистрация обработчика или изменение глобального объекта нельзя удалять только потому, что результат импорта нигде не присвоен. Документация webpack поэтому разделяет анализ usedExports и декларации sideEffects, а затем передаёт оставшийся код минификатору.

После этого Terser может удалить локальный dead code, упростить выражения и сократить имена. Это уже не tree shaking всего проекта, потому что одиночный минификатор не разрешает зависимости и не знает полный граф приложения. На последнем сетевом этапе сервер применяет gzip или Brotli. Описание HTTP compression на MDN показывает, что браузер и сервер согласуют кодирование через Accept-Encoding и Content-Encoding; оно уменьшает число передаваемых байтов, но не меняет исходную программу в репозитории.

Эти оптимизации складываются, но не дублируют друг друга. Tree shaking исключает ненужные модули, минификация сокращает оставшийся JavaScript, а gzip или Brotli используют повторяемость полученного текста. Source map решает обратную эксплуатационную задачу, сопоставляя ошибку в компактном bundle с исходным файлом. Публиковать карту вместе с исходным содержимым или хранить её отдельно следует осознанно, но сама минификация не должна лишать команду возможности разбирать production-сбои.

Верхняя схема разделяет tree shaking, минификацию Terser и HTTP-сжатие, а нижняя показывает рост сложности и возможный рост размера после обфускации

Обфускация оптимизирует не размер, а стоимость анализа

Обфускатор меняет цель преобразования. Переименование идентификаторов, массив строк, разбиение литералов и control flow flattening должны затруднить восстановление замысла программы, даже если результат становится больше и медленнее. Dead code injection намеренно добавляет инструкции, которые никогда не нужны выполнению, поэтому противоречит задаче минимального размера.

Кодирование строк в Base64 или RC4 не превращает клиентский bundle в хранилище секретов. Чтобы программа смогла получить строку во время работы, вместе с ней приходится доставить декодер и необходимые данные. Аналитик может поставить breakpoint после декодирования, перехватить сетевой запрос или изучить значения в памяти. По той же причине проверку лицензии, постоянный приватный API-ключ или решение о доступе нельзя надёжно доверить только браузерному коду.

Обфускация всё же может быть полезна, когда цель сформулирована скромнее: убрать очевидные имена, затруднить массовое копирование небольшой проприетарной логики или увеличить время ручного анализа. Однако её нужно оценивать как компромисс. Документация JavaScript Obfuscator предупреждает, что control flow flattening замедляет выполнение и увеличивает файл, а dead code injection способен увеличить размер особенно сильно. Опции selfDefending и debugProtection также вмешиваются в форматирование и отладку, из-за чего усложняют работу не только исследователю, но и собственной команде.

Что именно делает инструмент

JS Minifier & Obfuscator загружает локальные browser-сборки Terser и JavaScript Obfuscator, после чего все преобразования выполняются в текущей вкладке без отправки исходника на сервер. Режим Minify разделяет compress и mangle, предлагает пресеты Safe, Default и Aggressive и позволяет отдельно управлять удалением недостижимого кода, console.*, debugger, сохранением имён функций и классов, top-level mangling и модульной семантикой.

Название Safe означает более осторожный набор параметров, а не доказательство безопасности результата. Aggressive может удалить console.* и переименовать верхнеуровневые символы, поэтому внешняя страница, тест или другой скрипт могут потерять ожидаемую ссылку. Инструмент не запускает полученный код и не строит source map; после копирования или скачивания результат нужно проверить в целевом окружении.

Режим Obfuscate предлагает четыре пресета и отдельные переключатели для массива строк, Base64 или RC4 encoding, разбиения строк, control flow flattening, dead code injection, числовых выражений, переименования глобальных идентификаторов, selfDefending и debugProtection. Интерфейс предупреждает, что High увеличивает размер и замедляет выполнение. Показанная статистика сравнивает сырые UTF-8 байты входа и выхода, а не размер после gzip или Brotli.

Это инструмент для одного переданного фрагмента JavaScript. Флаг Module сообщает Terser, как интерпретировать вход, но не превращает страницу в bundler: она не разрешает импорты, не строит граф зависимостей и не выполняет tree shaking проекта. Для production-сборки такой эксперимент полезен при выборе параметров, однако окончательное преобразование должно оставаться воспроизводимой частью пайплайна вместе с тестами, source maps и HTTP compression.

Похожие инструменты

Частые вопросы

Кто создал JSMin и что он делал?

Дуглас Крокфорд выпустил JSMin в 2001 году. Этот потоковый фильтр удалял комментарии и ненужные пробелы, осторожно сохраняя переводы строк там, где JavaScript мог интерпретировать их как разделители инструкций. JSMin не строил AST, не переименовывал переменные и прямо не позиционировался как обфускатор.

Чем Terser отличается от UglifyJS?

UglifyJS Михаила Базона появился в 2010 году как написанный на JavaScript парсер, compressor и mangler. Когда синтаксис ECMAScript вышел далеко за пределы ES5, появилась ветка uglify-es, но её сопровождение прекратилось. Terser возник в 2018 году как совместимый форк uglify-es с поддержкой современного JavaScript и продолжил развивать его модель AST-преобразований.

Чем minify, compress и mangle отличаются друг от друга?

Minify является общей целью уменьшить текст программы. В терминологии Terser compress преобразует выражения и удаляет код, который можно доказуемо отбросить, mangle сокращает допустимые имена идентификаторов, а format печатает итоговый AST с минимальным синтаксическим шумом. Каждый этап должен сохранять наблюдаемое поведение в рамках своих предположений.

Tree shaking и минификация являются одним процессом?

Нет. Tree shaking анализирует граф ES-модулей и исключает неиспользуемые экспорты или целые модули с учётом побочных эффектов. Минификатор получает уже выбранный код и сокращает его, хотя затем может удалить дополнительные локальные выражения и недостижимые ветки. Эти этапы дополняют друг друга.

Заменяет ли минификация gzip или Brotli?

Нет. Минификация создаёт меньший JavaScript-файл, который браузер может выполнить напрямую. gzip и Brotli кодируют байты для передачи по HTTP, после чего браузер восстанавливает содержимое перед разбором. Обычно production-пайплайн сначала собирает и минифицирует код, а сервер затем передаёт результат с gzip или Brotli.

Может ли обфускация скрыть API-ключ или пароль?

Надёжно скрыть секрет в клиентском JavaScript нельзя. Браузеру нужны код, декодер и данные для выполнения программы, поэтому их может исследовать и пользователь. Обфускация способна замедлить анализ логики, но постоянные ключи, доверенные проверки и привилегированные операции должны оставаться на сервере.

Может ли минификация или обфускация сломать программу?

Да, если преобразование нарушает неявный контракт программы. Динамический eval, обращение к Function.name, внешние ссылки на глобальные имена, сериализация функций и код с важными побочными эффектами требуют осторожности. После преобразования нужны те же тесты, которые используются для обычной production-сборки.

Автор

Fullstack Engineer

В коммерческой разработке с 2011 года — больше 15 лет. Автор двух SaaS-продуктов: TeleGo.io и LingvoHabit.com. Статьи здесь выросли из реальной клиентской и продуктовой работы.

Подробнее об автореНаписать в Telegram

Похожие статьи

СЖАТИЕ ИЗОБРАЖЕНИЙ
СЖАТИЕ ИЗОБРАЖЕНИЙ · '26

Оптимизация SVG, экспорт кода и сжатие изображений

Оптимизируйте и трансформируйте SVG, экспортируйте React, React Native, Vue, PNG или Data URI и сжимайте растровые изображения локально в браузере.

#сжатие изображений #WebP формат #оптимизация SVG #PNG vs JPEG #AVIF #SVGO
4 мин. чтения
SYSTEMD
SYSTEMD · '26

systemd Builder: как Linux перешёл от init-скриптов к unit-файлам

Почему Linux-дистрибутивы перешли от SysV-скриптов к systemd: зависимости сервисов, активация по сокету, cgroups и спор о границах проекта.

#systemd #Lennart Poettering #SysV init #Linux systemd #unit файлы #systemd controversy
6 мин. чтения
COMMAND EXPLAINER
COMMAND EXPLAINER · '26

Command Explainer: как читать man-страницы и понимать Unix

Первая man-страница написана в 1971 году для UNIX. Документация остаётся главным источником знаний. Объясняем сложные команды простым языком — от tar до awk.

#Command Explainer #man pages #Unix philosophy #CLI документация #Linux команды #awk sed tar
3 мин. чтения