Руководство по минификации кода: CSS, JS и HTML
Минификация кода убирает из исходников CSS, JavaScript и HTML символы, которые не нужны машине, — пробелы, комментарии, переносы строк, — а многословные конструкции переписывает в более короткие эквиваленты. Поведение остаётся прежним; файл просто становится меньше и загружается быстрее.
Сразу о главном: минификация — это не сжатие. Minify работает с исходным кодом, срезая синтаксическую избыточность. Gzip и Brotli работают с байтами в пути по сети, кодируя повторяющиеся последовательности. Они выполняются на разных этапах, борются с разными видами избыточности и складываются друг с другом, поэтому минифицировать код стоит даже тогда, когда сервер уже раздаёт Brotli.
Нужно что-то сжать прямо сейчас? Переходите к минификатору CSS, минификатору JavaScript или минификатору HTML — каждый работает целиком в браузере. Но именно понимание механики позволяет решить, где сжимать и нужно ли вообще делать это вручную. Ниже разобрано, что на самом деле делает минификация, как минифицируются CSS, JS и HTML, как minify складывается с gzip и Brotli, когда сборщик уже всё делает за вас и как source map сохраняют отлаживаемость минифицированного кода.
Что такое минификация (и чем она не является)
Минификация делает две вещи. Она удаляет символы, не несущие смысла для парсера, и переписывает исходник в более короткую форму, которая означает ровно то же самое. Результат полностью эквивалентен для машины и почти нечитаем для человека. В том, как код выполняется, ничего не меняется — меняется только его поверхность.
Этот последний тезис — инвариант, который стоит удерживать до конца руководства: minify правит только поверхность исходника (пробелы, комментарии, имена идентификаторов, избыточный синтаксис), но никогда — поведение или вывод. Это зеркальное отражение форматирования. Форматирование добавляет пробелы, чтобы код читался; минификация срезает их, чтобы код стал маленьким. Оба лежат на одной оси «семантической эквивалентности», просто указывают в противоположные стороны.
Три похожих по звучанию операции постоянно путают. Эта таблица их разводит:
| Параметр | Format (beautify) | Minify | Compress (gzip/Brotli) |
|---|---|---|---|
| Что меняет | Добавляет пробелы, переносы строк, отступы | Убирает пробелы и комментарии, сокращает синтаксис | Побайтовое кодирование повторяющихся последовательностей |
| Какой слой | Исходный код | Исходный код | Передача / хранение |
| Это всё ещё исходник? | Да (читаемый) | Да (исполняемый, трудночитаемый) | Нет (бинарь, нужно декодировать) |
| Кто делает | Разработчик / редактор | Сборщик / минификатор | Сервер + браузер |
| Обратимо? | Семантически | Семантически (поведение не меняется) | Полностью (распаковка восстанавливает байты) |
Format и minify живут на одной оси — оси семантической эквивалентности. Сжатие лежит на совершенно другой. Отформатированный файл и минифицированный файл — оба валидный исходник; сжатый файл — это бинарный блоб, который надо декодировать, прежде чем хоть что-то сможет выполниться.
Вот тут и закрадывается дорогостоящее заблуждение: «мой сервер уже делает gzip, значит минификация бессмысленна». Это не так, и числа дальше в руководстве показывают почему. Минификация и сжатие убирают разную избыточность, поэтому одно не делает другое лишним. Держите это в голове, пока мы проходим по каждому языку.
Полезно подумать, почему байты, которые удаляет минификатор, вообще существуют. Пробелы, комментарии и описательные имена вы пишете для себя и коллег — они делают код ревьюабельным и сопровождаемым. Машина, которая парсит ваш CSS, выполняет JavaScript или строит DOM, игнорирует их все до единого. Минификация — это шаг, который выбрасывает материал, нужный только людям, когда люди с исходником уже закончили. Поэтому же минификация — это забота продакшена и никогда не разработки: читаемую версию вы храните в репозитории, а урезанную отправляете в браузеры. Читаемая копия — источник истины; минифицированная копия — артефакт сборки, который можно пересобрать в любой момент.
Как работает минификация CSS
CSS минифицировать мягче всего из трёх, потому что его грамматика почти не оставляет места для неоднозначности. Минификатор вырезает комментарии, схлопывает череду пробелов в ничто, убирает последнюю точку с запятой в каждом блоке и удаляет пробелы вокруг {, }, : и ;. Уже это очищает большую часть байтов.
CSS к тому же допускает набор эквивалентных переписываний, которого нет ни в одном другом языке. Хороший минификатор применяет их безопасно:
- Сокращение цветов.
#ffffffстановится#fff, а#ff0000схлопывается вred(или наоборот — что короче записать). - Удаление единиц у нуля.
0pxстановится0, аmargin: 0 0 0 0становитсяmargin: 0. - Срезание ведущих нулей.
0.5emстановится.5em. - Слияние сокращённых форм. Четыре отдельных объявления
margin-top,margin-right,margin-bottomиmargin-leftсворачиваются в одноmargin. - Объединение правил. Соседние правила с идентичными селекторами или объявлениями можно слить, а дублирующиеся объявления — отбросить.
Каждое из этих переписываний сохраняет результат отрисовки неизменным — это граница, которую корректный минификатор никогда не переступает. Но CSS чувствителен к порядку: более позднее правило перебивает более раннее через каскад. Поэтому безопасный минификатор не станет вслепую переставлять правила так, что это могло бы изменить, какое объявление побеждает. Сокращать байты можно; менять каскад — нельзя.
Это ограничение тоньше, чем кажется. Два объявления, которые выглядят сливаемыми, могут таковыми не быть, потому что что-то между ними ссылается на то же свойство с той же специфичностью. Рассмотрим:
.btn { color: #ff0000; }
.alert .btn { color: blue; }
.btn { color: #f00; }
Первое и третье правила имеют общий селектор и могли бы слиться — но только если это не переместит объявление за среднее правило так, что изменится победитель для элемента, подходящего под оба. Наивное слияние с перестановкой здесь способно сломать каскад. Это как раз тот граничный случай, который промышленный движок вроде CSSO умеет анализировать, и поэтому не стоит писать свой минификатор «удалить пробелы» на регулярках. Преобразования выглядят механическими, но анализ безопасности за ними — нет.
Наш минификатор CSS использует движок CSSO ровно для такой минификации без потерь, и работает он целиком в браузере, показывая, сколько байтов сэкономлено, чтобы вы видели влияние каждого прохода на размер. Тот же инструмент форматирует и в обратную сторону, так что минифицированную таблицу стилей, скопированную с живого сайта, можно развернуть обратно в читаемые правила с отступами. Берите его, когда скопировали фрагмент CSS и хотите проверить его сжатый размер, или когда отправляете статическую страницу без шага сборки, который сделал бы это за вас.
Как работает минификация JavaScript
Минификация JavaScript идёт куда дальше CSS, и именно здесь живут и выигрыш, и ловушки. Чтобы понять почему, посмотрите на небольшую функцию до и после Terser:
// до
function calculateTotal(items, taxRate) {
let runningTotal = 0;
for (const item of items) {
runningTotal += item.price * item.quantity;
}
return runningTotal * (1 + taxRate);
}
// после
function calculateTotal(t,a){let n=0;for(const o of t)n+=o.price*o.quantity;return n*(1+a)}
Имя функции calculateTotal выживает, потому что она экспортируется (или может вызываться извне); параметры и переменные цикла схлопываются до одиночных букв. Это суть, но минификатор JS делает несколько разных вещей:
- Манглинг идентификаторов. Локальные переменные и параметры переименовываются в одиночные буквы:
getUserPreferencesстановитсяa. Манглятся только локальные имена — глобальные и экспортируемые по умолчанию остаются нетронутыми, потому что их переименование сломало бы код, который ссылается на них снаружи. - Удаление мёртвого кода. Недостижимые ветки и неиспользуемые переменные вырезаются, работая рука об руку с tree-shaking на уровне сборщика.
- Свёртка констант и сжатие синтаксиса. Выражения укорачиваются:
trueстановится!0,falseстановится!1, аreturn undefined;становитсяreturn;.
Самое ценное, что нужно знать о минификации JS, — это ловушка автоматической вставки точек с запятой (ASI). JavaScript позволяет опускать точки с запятой, и парсер вставляет их за вас по определённым правилам. Когда минификатор удаляет переносы строк, на которые эти правила опираются, код может изменить смысл. Классический сбой — оператор, начинающийся с ( или [, который незаметно приклеивается к предыдущей строке:
const x = getValue()
[1, 2, 3].forEach(handle)
Без точек с запятой это парсится как getValue()[1, 2, 3] — выражение индексации, а не два оператора. После минификации в одну строку баг закрепляется намертво. Та же опасность возникает со строкой, начинающейся с (, где предыдущее выражение вызывается как функция. Современный Terser изящно справляется с большинством реальных случаев, потому что сначала разбирает код в абстрактное синтаксическое дерево и заново расставляет точки с запятой там, где они нужны, — он не занимается слепым удалением текста. Но плохой исходник плюс агрессивная минификация — настоящий источник продакшен-багов, и сбои противны именно тем, что проявляются только в минифицированной сборке, а не в разработке. Исправление на вашей стороне: пишите код с явными точками с запятой и однозначным синтаксисом, и минификатор останется безопасным. Правило линтера или автоформат, расставляющий точки с запятой на уровне исходника, убирает риск полностью.
Корректный минификатор сохраняет поведение — но только если на входе валидный стандартный JavaScript. Terser парсит ECMAScript; он не понимает TypeScript или JSX. Их сначала надо транспилировать в обычный JS, иначе минификация падает на этапе разбора. Если вы вставляете файл .ts в минификатор JS и получаете ошибку — вот почему.
Один терминологический вопрос всплывает часто: minify против uglify. По сути это одно и то же. «Uglify» идёт от UglifyJS, раннего популярного минификатора JS; Terser — его современный форк с поддержкой ES2015 и новее. Сегодня «minify» — общий термин для всех трёх языков, а «uglify» сохранился как старое, специфичное для JS название того же процесса.
Наш минификатор JavaScript запускает Terser в браузере — переименовывает локальные имена, выбрасывает мёртвый код и срезает комментарии — и сообщает, сколько байтов сэкономлено на каждом проходе.
Как работает минификация HTML
Минификация HTML начинается с основ: убрать комментарии (сохранив объявление <!DOCTYPE> и любые условные комментарии, на которые вы ещё опираетесь), схлопнуть пробелы между тегами и срезать лишние пробелы внутри списков атрибутов. Небольшой фрагмент показывает форму процесса:
<!-- nav -->
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
становится:
<ul><li><a href=/>Home</a><li><a href=/about>About</a></ul>
Комментарий исчез, отступы между тегами схлопнуты, необязательные закрывающие теги </li> отброшены, а значения атрибутов без особых символов потеряли кавычки. Дальше минификатор может применить ещё несколько приёмов, специфичных для HTML:
- Удаление необязательных закрывающих тегов. Спецификация HTML разрешает опускать
</li>,</p>,</td>и ряд других, поэтому минификатор может их отбросить. - Удаление кавычек у атрибутов. Когда в значении нет пробелов и особых символов,
class="x"становитсяclass=x. - Свёртка булевых атрибутов.
disabled="disabled"становится простоdisabled, аchecked="checked"становитсяchecked. - Минификация встроенных CSS и JS. Содержимое блоков
<style>и<script>тоже минифицируется, поэтому один проход ужимает весь документ.
Вот граница, которая важнее всего: в HTML пробелы иногда значимы. Внутри <pre> и <textarea> каждый пробел и перенос строки отрисовывается буквально. Элементы со white-space: pre ведут себя так же. А пробелы между строчными элементами влияют на раскладку — пробел между двумя тегами <a> показывается на странице как зазор. Агрессивная минификация, схлопывающая эти пробелы, способна изменить вид страницы. Практическое правило: после минификации проверьте отрисовку вокруг pre, textarea и границ строчных элементов, прежде чем выкатывать.
Наш минификатор HTML форматирует через js-beautify, а встроенные стили и скрипты минифицирует через CSSO и Terser — всё на стороне клиента. Особенно удобно для HTML-писем и разметки, выгруженной из CMS, где шага сборки, который сжал бы код за вас, обычно нет.
Minify против gzip против Brotli — как они складываются
Если сервер уже раздаёт gzip или Brotli, нужно ли всё ещё минифицировать? Да, и причина в том, что две техники убирают разную избыточность.
Минификация убирает синтаксическую избыточность на уровне исходника: пробелы, комментарии, длинные имена и многословные конструкции, существующие ради человеческой читаемости. Gzip и Brotli убирают статистическую избыточность на уровне байтов: строки и последовательности, повторяющиеся по файлу, заменяются более короткими кодами. Одно понимает синтаксис вашего кода; другое видит лишь поток байтов. Поскольку они нацелены на разное, их сложение работает — и работает хорошо.
Наглядный образ: gzip отлично замечает, что строка function встречается в бандле двести раз, и заменяет каждое вхождение короткой обратной ссылкой. Но он понятия не имеет, что getUserPreferences и getUserSettings — это имена переменных, которые можно сократить, или что целый блок if (false) { ... } никогда не выполнится. Минификация делает ровно это — структурные и семантические выигрыши, к которым побайтовый компрессор слеп. Запустите их вместе, и каждый вычистит то, чего не видит другой.
Вот арифметика — в порядке, в котором всё происходит на самом деле:
- Минификация сама по себе обычно ужимает CSS, JS и HTML на 20–30%, убирая пробелы и комментарии и сокращая синтаксис.
- Gzip поверх минифицированного вывода снимает ещё 60–80%, кодируя повторяющиеся последовательности, оставшиеся в тексте.
- Brotli вместо gzip даёт вывод ещё на 15–25% меньше благодаря большему встроенному словарю и лучшему алгоритму.
Вывод в одну строку: сначала minify, потом compress — совокупный результат нередко на 80–90% меньше исходника. Одно не исключает другое, и пропуск любого шага оставляет байты на столе.
Почему минификация всё ещё оправдывает себя поверх Brotli? Три причины:
- Меньший вход сжимается меньше. Минифицированный файл даёт компрессору меньше избыточного материала для разбора, а меньший и чище вход обычно даёт меньший выход.
- Minify делает то, чего не может сжатие. Удаление мёртвого кода и короткие имена переменных — это семантические удаления. Gzip не понимает ваш код — он видит лишь байты, — поэтому никогда не сможет удалить неиспользуемую функцию или переименовать переменную.
- Браузер парсит меньше байтов. После распаковки браузер получает минифицированный код. Меньше кода — быстрее разбор и выполнение, а не только меньше загрузка.
Порядок — это не выбор, он вытекает из того, где живёт каждый шаг. Минификация принадлежит этапу сборки (вы или сборщик делаете её один раз). Сжатие принадлежит этапу передачи (сервер делает его на каждый запрос, браузер распаковывает по прибытии). Поэтому конвейер естественно такой: minify → деплой → сервер сжимает. Запустить наоборот нельзя: «сжать, потом минифицировать» невозможно, потому что сжатый вывод — это уже не исходный код.
Есть небольшая, но важная оговорка к «сначала minify, потом compress»: если содержимое уже сжато, сжимать его снова бессмысленно или вредно. Уже бинарные, высокоэнтропийные ресурсы — JPEG, PNG, WebP, шрифты в WOFF2 — ничего не выигрывают от gzip или Brotli и вообще не должны попадать в правила текстового сжатия. Минификация — это исключительно текстовое преобразование, поэтому таких файлов она не касается; избирательным приходится быть именно в сжатии. Настройте сервер сжимать текстовые MIME-типы (HTML, CSS, JS, JSON, SVG) и оставьте уже сжатые бинарники в покое.
Настройка слоя передачи — включение Brotli, установка Content-Encoding — это вопрос эксплуатации, которым занимается ваш сервер или CDN. Это руководство остаётся на слое исходника, где происходит минификация. Если вы оптимизируете полезную нагрузку шире, то же мышление «экономь байты на слое кодирования» применимо и к изображениям; наше руководство по форматам изображений разбирает сторону WebP/AVIF/JPEG этой истории.
Когда минифицировать вручную не нужно
Маркетинг минификаторов об этом часто умалчивает: если у вас есть шаг сборки, ваш продакшен-вывод уже минифицирован. Современные сборочные конвейеры делают это автоматически.
Vite и esbuild минифицируют JavaScript и CSS из коробки. Rollup и webpack делают это через TerserPlugin и CssMinimizerPlugin. Lightning CSS обрабатывает CSS на нативной скорости. Next.js, Astro и подобные фреймворки минифицируют, выполняют tree-shaking и разбивают код на чанки в своих продакшен-сборках, не требуя от вас ни малейшего усилия. Команда обычно — не более чем vite build или npm run build: минификация входит в то, что значит «собрать для продакшена», а не отдельный шаг, который вы прикручиваете сбоку. Если это описывает ваш проект, прогон файла через отдельный минификатор после сборки в лучшем случае избыточен, а в худшем вреден — повторный манглинг уже минифицированного кода способен дать путаный вывод и не сэкономит сколько-нибудь заметных байтов.
Сборщики делают и то, чего не может отдельный минификатор: они минифицируют в контексте всего графа зависимостей. Tree-shaking, в частности, работает только тогда, когда сборщик видит каждый импорт и экспорт и может доказать, что конкретная функция нигде не используется. У однофайлового минификатора нет графа для анализа — он может выбросить мёртвый код внутри файла, который вы ему дали, но не способен понять, что целый импортированный модуль недостижим. Это ещё одна причина, по которой сборочный конвейер — правильный дом для продакшен-минификации.
Так когда же отдельный минификатор — правильный инструмент? В честном наборе случаев, где нет шага сборки, который сделал бы это за вас:
- Статические сайты и написанные вручную однофайловые страницы без сборщика в цепочке.
- HTML-шаблоны писем, где многие системы считают по байтам и сборочного конвейера нет вовсе.
- Сторонние фрагменты и код виджетов, которые вы встраиваете в чужую страницу.
- Быстрые проверки размера — вставьте блок, посмотрите, насколько он велик после минификации и сколько вы сэкономили. Для этого и нужен показ сэкономленных байтов.
- Чтение чужого минифицированного кода, когда вы запускаете форматировщик в обратную сторону, чтобы снова сделать код читаемым.
Решение простое. Есть сборка? Пусть минифицирует сборка. Нет сборки, разовая задача или просто проверка размера? Онлайн-инструмент — самый быстрый путь, и поскольку эти инструменты работают целиком в браузере, ваш код никогда не покидает устройство. Это важно для проприетарного или невыпущенного кода, который не стоит вставлять в серверный форматировщик, получающий копию всего. Это тот же аргумент о приватности, что проходит через наше руководство по стилю SQL — другой подробный разбор форматирования в этом кластере.
Source map — отладка минифицированного кода
Минифицированный код сам по себе — кошмар для отладки. Когда Terser переименовал каждую локальную переменную в a, b и c, продакшен-трейс стека, указывающий на bundle.min.js:1:48211, не говорит по сути ничего о том, что на самом деле сломалось.
Это решают source map. Source map — это файл .map, который записывает соответствие между каждой позицией в минифицированном выводе и соответствующей позицией в исходном коде. Когда DevTools браузера его загружает, он переводит минифицированные ошибки обратно в настоящие имена файлов, номера строк и имена переменных. Вы отлаживаете код, который написали, хотя браузер выполняет код, который произвела ваша сборка.
На практике сборщик генерирует source map рядом с минифицированным бандлом, а комментарий //# sourceMappingURL=bundle.min.js.map (или HTTP-заголовок) указывает браузеру на .map. Откройте DevTools, поймайте ошибку — и трейс стека покажет ваши настоящие имена файлов и номера строк вместо минифицированной каши. Карта загружается лениво, только когда DevTools открыты, поэтому вашим посетителям она ничего не стоит.
Есть и аспект приватности, о котором стоит знать. Публичная source map по сути отправляет ваш исходный код любому, кто откроет DevTools. Для открытого кода это нормально; для проприетарного — нет. Для этого и нужны скрытые source map — бандл не несёт комментария sourceMappingURL, так что публика карту никогда не видит, но вы всё равно загружаете её в сервис мониторинга ошибок вроде Sentry. Сервис деминифицирует продакшен-трейсы на своей стороне, давая вам читаемые ошибки, не раскрывая исходник миру.
Обратите внимание, как это подкрепляет более ранний тезис: source map — это возможность сборщика. Простой онлайн-минификатор обычно её не производит, потому что разовому сжатию она не нужна. Это ещё одна причина доверить продакшен-минификацию сборке — она даёт карту бесплатно. И помните, что source map никогда не меняет сам минифицированный бандл; это чисто отладочное подспорье, лежащее рядом. Не принимайте .map за продакшен-зависимость.
Часто задаваемые вопросы
Минификация — это то же самое, что сжатие?
Нет. Минификация переписывает ваш исходный код — срезая пробелы, комментарии и сокращая имена, — так что он остаётся валидным кодом, просто меньшего размера. Сжатие (gzip, Brotli) кодирует получившиеся байты для передачи, а браузер их декодирует. Они борются с разной избыточностью, работают на разных этапах и складываются: сначала minify, потом compress.
Нужно ли минифицировать, если я использую gzip или Brotli?
Да. Минификация всё ещё важна с gzip и Brotli. Минифицированный код даёт компрессору меньше избыточного входа, поэтому сжимается сильнее, а minify выполняет семантические удаления — мёртвый код, короткие имена переменных, — которые побайтовому сжатию недоступны. Браузер к тому же парсит меньше байтов. Используйте оба, именно в этом порядке.
Ломает ли минификация мой код?
Корректный минификатор сохраняет поведение — CSS отрисовывается идентично, а Terser держит JavaScript эквивалентным. Вывод работает так же, как исходник. Две оговорки: JavaScript, опирающийся на автоматическую вставку точек с запятой, требует валидного синтаксиса, а чувствительный к пробелам HTML вроде <pre> или <textarea> стоит проверить после минификации.
В чём разница между minify и uglify?
Для JavaScript это по сути одно и то же. «Uglify» идёт от UglifyJS, раннего популярного минификатора JS; Terser — его современный форк с поддержкой актуального синтаксиса. Сегодня «minify» говорят обобщённо про CSS, JS и HTML, а «uglify» — это старое, специфичное для JS название того же процесса.
Стоит ли минифицировать в разработке?
Нет. Минифицируйте продакшен-сборки, а не разработку. Минифицированный код нечитаем и труден для отладки, поэтому во время разработки нужен полный, отформатированный исходник. Ваш сборщик — Vite, esbuild, webpack — минифицирует автоматически при сборке для продакшена, нередко с source map, чтобы вы всё равно могли отлаживать выкаченный бандл.
Насколько минификация уменьшает размер файла?
Минификация сама по себе обычно ужимает CSS, JS и HTML примерно на 20–30%, в основном убирая пробелы и комментарии и сокращая имена. Со слоем gzip или Brotli поверх совокупный результат нередко на 80–90% меньше исходника. Точная цифра зависит от того, сколько пробелов и избыточности было в файле.