Как выжать максимум из минификации кода
Вы задумывались над тем, что если в конструкторе и методах использовать не this
, а переменную, то после минификации экономия байтов начнётся уже с четвёртого this
?
// просто сравните длину строк
this.this.this.this.
var s=this;s.s.s.s.
Я использовал этот и некоторые другие упоротые способы для участия в конкурсе js13kGames, цель которого — написать игру, размер которой не превысит 13 килобайт.
Игра почти готова, осталось всего-то пару дней не спать…
Что за конкурс?
js13kGames, кажется, ещё не очень популярен в России, поэтому, кратко:
- он проводится каждый год с 13 августа до 13 сентября, начиная с 2012 года;
- весь код должен находиться в одном html-файле;
- размер zip-архива с этим файлом не должен превышать 13 килобайт;
- игра должна запускаться в последних стабильных версиях Chrome и Firefox;
- желательно, чтобы игра соотносилась с темой, которую озвучивают 13 августа.
В ущерб читаемости
Приведённый выше пример с this
не добавляет коду красоты, зато в конструкторах и методах, где this
используется интенсивно, такой подход экономит по 3 байта на каждом обращении, начиная с пятого. Например, в одном из конструкторов было 39 штук this
. Заменив их на self
, получилось сэкономить более 100 байт.
Думаю, во всём проекте только эти замены сохранили более килобайта.
Ещё один приём, подходящий, пожалуй, только для таких небольших спортивных проектов — это большое количество глобальных переменных и функций. Почти все инструменты общего назначения (random()
, getUniqueID()
и так далее), а также многие специфичные штуки (вроде функции, отключающей сглаживание при масштабировании в Canvas-контексте) лежат в глобальной области видимости. Здесь, конечно, стоит уделить особое внимание именам этих инструментов, чтобы код был как можно более самодокументированным.
t.r() // tools.random
r() // глобальная функция
При минификации все эти функции будут занимать один символ (вместо, например, трёх, если мы поместим их в объект), что даёт весьма впечатляющую экономию: одна только функция random()
встречается в коде 77 раз, и её «глобальность» спасает 150 байт.
Совсем специфичная ситуация: я решил хранить спрайты в gif’ках, закодированных в base64, и заметил, что все получившиеся строки начинаются на R0lGODlh
. Всего спрайтов получилось 14 (хотя, по изначальной задумке, должно было быть больше), и, вынеся этот начальный кусок строки в функцию, занимающуюся превращением строк в объекты Image
, я смог спасти ещё примерно 100 байт.
Последний нюанс, который, возможно, даже немного помогает с восприятием кода — это жёсткая необходимость следовать принципу DRY. Почему «возможно»? Потому что код становится иногда слишком фрагментированным. Практически каждые несколько строк кода, которые повторяются хотя бы дважды, становятся претендентами на выделение в функцию.
Откусив от геймплея
Некоторого количества кода удалось избежать, сымитировав поверхность, по которой бежит персонаж: на самом деле, это граница уровня, а текстура земли просто находится «за кадром». К сожалению, из-за этого решения платформер, по сути, почти перестал быть платформером, но на исправление такой фундаментальной оплошности времени уже не было.
В категорию «минус к играбельности» попадают и некоторые юниты, которые, хоть и заняли совсем мало места, получились слишком неадекватными.
Основной пример: камень, призванный разнообразить ландшафт, не смог стать поверхностью, на которую можно запрыгнуть. Вместо этого, он отталкивает игрока и наносит урон. Пришлось пойти на быстрый хак, чтобы выкрутиться: камень превратился в крысиное логово, и если начать по нему бить, то раз в пять ударов оттуда появляется крыса.
Анимация
Как я уже говорил, все спрайты хранятся в GIF-файлах, завёрнутых в base64. Размера они минимального, и при создании анимации увеличиваются в 16 раз (это размер внутриигрового «пикселя»). Объект с описанием спрайта также используются конструкторами юнитов для определения размеров; то есть, не анимация подгоняется под размер юнита, а наоборот.
Неиспользованное
В самом начале одной из идей было повсеместно заменить true
и false
на 1
и 0
, но, по ходу разработки, я совершенно забыл об этом, и вспомнил только в конце. К счастью, делать этого не пришлось: к моменту отправки работы на конкурс она проходила по размеру, и я даже не представляю, сколько ужаса пришлось бы пережить, прибегая к такому ненадёжному средству.
Для создания музыки я использовал нотацию, где каждые два символа представляют звук: строка — ноту, число — знаменатель её длительности (ноль вместо строки — пауза). Реальная длительность в милисекундах рассчитывается делением длительности целой ноты на знаменатель длительности ноты.
notes: [
'A4', 4, 0, 8, 'G4', 8,
'A4', 8, 'A4', 16, 'G4', 16, 'C5', 8, 'D5', 8,
0, 4, 'A4', 8, 'A4', 16, 0, 16,
'A4', 8, 0, 8, 'G4', 8, 0, 8
]
В планах было сократить объём записи саундтрека введением «сэмплов» — переиспользуемых музыкальных фраз, но до этого дело не дошло, поскольку сочинительство музыки пришлось на последний час перед нажатием на кнопку Submit, и ни о каком звуковом разнообразии речи идти уже не могло.
Заключение
Как ни смешно, но большая часть этих оптимизаций для сжатия оказалась излишней: даже с оригинальными именами глобальных переменных файл с игрой превратился в zip-архив размером 10.1 Кб (при размере index.html в 31.9 Кб). Чего не хватило по-настоящему — так это времени. Особенно его не хватило на level-дизайн, внятный саундтрек и хотя бы небольшое количество плейтестов.
Я уже участвовал в js13kGames в прошлом году, но в тот раз приступил к работе за два дня до окончания сроков. Поэтому, а также из-за меньшего опыта, первый блин вышел комом. В этом году мне даже есть чем гордиться, хотя, конечно, до настоящих высот геймдева ещё далеко.
Для энтузиастов минификации: код доступен на GitHub.
Интересно узнать и о ваших tinycode-проектах, делитесь в комментариях!
Комментарии (3)
23 сентября 2016 в 17:21
+1↑
↓
Супер! Я для собственных модулей подобные оптимизации проделываю. Т.е. this помещаю в self, лишь для того, чтобы uglifyjs мог эту переменную потом утоптать. При таком подходе код не теряет читаемость, но хорошо жмется. Общие функции забрасываю в области замыкания для того-же.
23 сентября 2016 в 17:31
0↑
↓
Спасибо! Да, в минификации модулей есть и свои особенности. Здесь я, конечно, модули не использовал, чтобы не тратить место на require или что-то подобное: было ощущение, что каждый байт на счету.
23 сентября 2016 в 17:40
0↑
↓
Я такое извращение в Кукараче только проделывал, чтобы сократить длину кода. Только преподавательница не могла прочитать мой код, так что пришлось делать его читаемым и повторяемым.