Вы задумывались над тем, что если в конструкторе и методах использовать не 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-проектах, делитесь в комментариях!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (31)
LoadRunner
23.09.2016 17:40+1Я такое извращение в Кукараче только проделывал, чтобы сократить длину кода. Только преподавательница не могла прочитать мой код, так что пришлось делать его читаемым и повторяемым.
barkalov
23.09.2016 18:15+5Вы не пишете самое главное: какая разница получается после gzip.
xenohunter
23.09.2016 19:01Там и не используется gzip, финальный файл пакуется в zip-архив. Я написал в посте, что размер index.html был 31.9 Кб, а в виде архива занял 10.1 Кб.
Denai
23.09.2016 20:39Но без некоторых оптимизаций, приведённых в посте он был бы меньше в zip, или важен именно несжатый вариант?
xenohunter
24.09.2016 01:02Важен размер именно zip-файла.
Denai
24.09.2016 01:29+2this.this.this.this.
var s=this;s.s.s.s.
В таком случае первый вариант явно лучше, как и использование повторяющихся каких угодно огромных кусков кода. Баловство с вырезанием общей шапки картинки тоже пользы не должно было принести, кроме лишнего текста функции ничего оно не даст. Было бы здорово если бы вы сравнили текст без всей этой лишней ерунды и тот что получился. Сжатые версии естественноxenohunter
24.09.2016 01:34Ну, лишнего кода там — три символа: две кавычки и плюс. А вот ерунду, к сожалению, писал изначально, так что версии без неё нет. Но на следующий раз принял к сведению, создам две версии и буду их при сжатии сравнивать.
gearbox
23.09.2016 18:19+9размер zip-архива с этим файлом не должен превышать 13 килобайт;
Как ни смешно, но большая часть этих оптимизаций для сжатия оказалась излишней: даже с оригинальными именами глобальных переменных файл с игрой превратился в zip-архив размером 10.1 Кб (при размере index.html в 31.9 Кб).Такими оптимизациями влегкую можно подкузьмить архиватору и вызвать увеличение размера архива.
xenohunter
23.09.2016 19:32Да, возможно. Я об этом не подумал, так как интуитивно полагал, что архиватор просто создаёт словарь для сжатия, а уменьшение длины слов в этом словаре никак не повредит. В следующий раз попробую для эксперимента посжимать архиватором разные сборки: с подобными микрооптимизациями и без них.
RubaXa
23.09.2016 19:11-4А зачем вам вообще
this
? ПростоModuleName_setSomeProp(itemObj, '..value..')
и так далее, жаться будет на ура, читаемость отличная, даже работать будет быстрей.xenohunter
23.09.2016 19:32+4Если честно, вообще не понял, что за
ModuleName_setSomeProp(itemObj, '..value..')
.
leremin
23.09.2016 19:17+1Без архиватора было бы спортивнее: с этими сокращениями он, вероятнее, лучше справится. И еще: а если через обфускатор прогнать? Я ими не пользовался, но думаю, что с определенными настройками он код короче делает…
xenohunter
23.09.2016 19:37Да, я тоже думаю, что без архиватора, чистым текстом, было бы интереснее. Но — таковы правила, ничего не попишешь. Пробовал через packer, он сжимает до ~21 Кб, но с ошибками. Разбираться было некогда, да и есть опасения, что с такими наворотами может начать тормозить.
gearbox
23.09.2016 19:48Видимо организаторы исходят из того что практически все серверы и броузеры поддерживают дефлейт. А вот на мой взгляд спортивнее было бы если бы после загрузки и парсинга код отъедал не более 13 кбайт в оперативке /pokerface Дом и сам движок не считаем.
xenohunter
24.09.2016 01:05Такие условия слишком сложны для проверки. Нужно держать каждую игру запущенной (и играть в неё) довольно длительное время, чтобы удостовериться в объёмах используемой памяти. Что касается deflate, на сайте выкладывается разархивированная версия, так что в браузер попадает просто html-файл.
impwx
23.09.2016 21:13+2Самый первый кусок кода показывает, как нельзя делать минификатор. Замена подстроки
this
наs
сломала логику — вторая строка будет эквивалентнаthis.s.s.s
а неthis.this.this.this
.
Уверен, что автор это понимает и в продакшен-коде подобной ошибки бы не допустил, но все же пример стоит сделать более наглядным.xenohunter
24.09.2016 01:10Первый кусок кода — только для сравнения длин строк. Это вообще не рассматривается как рабочий код. Смысл в том, чтобы наглядно показать, сколько символов будут занимать четыре обращения к
this
и четыре обращения к переменной.
this.x=0;this.y=1;this.w=2;this.h=3; var s=this;s.x=0;s.y=1;s.w=2;s.h=3;
Вариант с переменной короче на один символ при четырёх обращениях к объекту. Каждое следующее обращение будет экономить ещё по три символа.
Shtucer
24.09.2016 00:56+2Например, в одном из конструкторов было 39 штук this. Заменив их на self, получилось сэкономить более 100 байт.
Но как?xenohunter
24.09.2016 01:11В комментарии выше есть пример;
self
при минификации кода становится односимвольной переменной.Shtucer
24.09.2016 01:19-1В том комментарии this напрямую превращается в s. А в статье превращением this в self экономятся килобайты.
xenohunter
24.09.2016 01:25В процессе минификации
self
превратится вs
. То есть, полная цепочка будет выглядеть так:
this.x = 0; this.y = 1; this.w = 2; this.h = 3; // код с this var self = this; self.x = 0; self.y = 1; self.w = 2; self.h = 3; // this заменили на self var s=this;s.x=0;s.y=1;s.w=2;s.h=3; // при минификации self стал одним символом
nanshakov
24.09.2016 11:21+1Честно говоря не знаю, какой алгоритм использует zip для сжатия. Но думаю можно подобрать имена так, что бы получался хороший вариант сжатия. Не понятно, почему не считать размер просто исходника.
xenohunter
24.09.2016 11:24Возможно, организаторы решили, что 13 Кб исходников — слишком мало для интересных игр. А было бы намного интереснее. Вон, люди пишут замечательные демки на JS в пределах 1 Кб.
wickedweasel
26.09.2016 04:03+2А вы не пробовали js хранить в виде png (пусть даже в инлайновом внутри html) и распаковывать его на лету?
https://habrahabr.ru/post/102153/xenohunter
26.09.2016 11:25+1Читал про это, но решил не использовать, так как потом всё равно в zip-архив нужно упаковать, а я сомневаюсь, что PNG бы сильно сжался. К тому же, только в этом году разрешили эту фичу использовать.
saksmt
27.09.2016 08:37+1*Почти offtop:* для любителей подобных соревнований есть ещё 1k Intro (http://archive.assembly.org/2016/1k-intro), но там несколько хардкорней — ассемблер, бутсектор, крутые демо.
Holix
Супер! Я для собственных модулей подобные оптимизации проделываю. Т.е. this помещаю в self, лишь для того, чтобы uglifyjs мог эту переменную потом утоптать. При таком подходе код не теряет читаемость, но хорошо жмется. Общие функции забрасываю в области замыкания для того-же.
xenohunter
Спасибо! Да, в минификации модулей есть и свои особенности. Здесь я, конечно, модули не использовал, чтобы не тратить место на require или что-то подобное: было ощущение, что каждый байт на счету.