Как выжать максимум из минификации кода

Вы задумывались над тем, что если в конструкторе и методах использовать не 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

    Я такое извращение в Кукараче только проделывал, чтобы сократить длину кода. Только преподавательница не могла прочитать мой код, так что пришлось делать его читаемым и повторяемым.

© Habrahabr.ru