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

А вы когда-нибудь участвовали в js13kGames?

Проголосовало 294 человека. Воздержалось 76 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (31)


  1. Holix
    23.09.2016 17:21

    Супер! Я для собственных модулей подобные оптимизации проделываю. Т.е. this помещаю в self, лишь для того, чтобы uglifyjs мог эту переменную потом утоптать. При таком подходе код не теряет читаемость, но хорошо жмется. Общие функции забрасываю в области замыкания для того-же.


    1. xenohunter
      23.09.2016 17:31

      Спасибо! Да, в минификации модулей есть и свои особенности. Здесь я, конечно, модули не использовал, чтобы не тратить место на require или что-то подобное: было ощущение, что каждый байт на счету.


  1. LoadRunner
    23.09.2016 17:40
    +1

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


  1. barkalov
    23.09.2016 18:15
    +5

    Вы не пишете самое главное: какая разница получается после gzip.


    1. xenohunter
      23.09.2016 19:01

      Там и не используется gzip, финальный файл пакуется в zip-архив. Я написал в посте, что размер index.html был 31.9 Кб, а в виде архива занял 10.1 Кб.


      1. Denai
        23.09.2016 20:39

        Но без некоторых оптимизаций, приведённых в посте он был бы меньше в zip, или важен именно несжатый вариант?


        1. xenohunter
          24.09.2016 01:02

          Важен размер именно zip-файла.


          1. Denai
            24.09.2016 01:29
            +2

            this.this.this.this.
            var s=this;s.s.s.s.

            В таком случае первый вариант явно лучше, как и использование повторяющихся каких угодно огромных кусков кода. Баловство с вырезанием общей шапки картинки тоже пользы не должно было принести, кроме лишнего текста функции ничего оно не даст. Было бы здорово если бы вы сравнили текст без всей этой лишней ерунды и тот что получился. Сжатые версии естественно


            1. xenohunter
              24.09.2016 01:34

              Ну, лишнего кода там — три символа: две кавычки и плюс. А вот ерунду, к сожалению, писал изначально, так что версии без неё нет. Но на следующий раз принял к сведению, создам две версии и буду их при сжатии сравнивать.


  1. gearbox
    23.09.2016 18:19
    +9

    размер zip-архива с этим файлом не должен превышать 13 килобайт;
    Как ни смешно, но большая часть этих оптимизаций для сжатия оказалась излишней: даже с оригинальными именами глобальных переменных файл с игрой превратился в zip-архив размером 10.1 Кб (при размере index.html в 31.9 Кб).

    Такими оптимизациями влегкую можно подкузьмить архиватору и вызвать увеличение размера архива.


    1. xenohunter
      23.09.2016 19:32

      Да, возможно. Я об этом не подумал, так как интуитивно полагал, что архиватор просто создаёт словарь для сжатия, а уменьшение длины слов в этом словаре никак не повредит. В следующий раз попробую для эксперимента посжимать архиватором разные сборки: с подобными микрооптимизациями и без них.


  1. RubaXa
    23.09.2016 19:11
    -4

    А зачем вам вообще this? Просто ModuleName_setSomeProp(itemObj, '..value..') и так далее, жаться будет на ура, читаемость отличная, даже работать будет быстрей.


    1. xenohunter
      23.09.2016 19:32
      +4

      Если честно, вообще не понял, что за ModuleName_setSomeProp(itemObj, '..value..').


  1. leremin
    23.09.2016 19:17
    +1

    Без архиватора было бы спортивнее: с этими сокращениями он, вероятнее, лучше справится. И еще: а если через обфускатор прогнать? Я ими не пользовался, но думаю, что с определенными настройками он код короче делает…


    1. xenohunter
      23.09.2016 19:37

      Да, я тоже думаю, что без архиватора, чистым текстом, было бы интереснее. Но — таковы правила, ничего не попишешь. Пробовал через packer, он сжимает до ~21 Кб, но с ошибками. Разбираться было некогда, да и есть опасения, что с такими наворотами может начать тормозить.


    1. gearbox
      23.09.2016 19:48

      Видимо организаторы исходят из того что практически все серверы и броузеры поддерживают дефлейт. А вот на мой взгляд спортивнее было бы если бы после загрузки и парсинга код отъедал не более 13 кбайт в оперативке /pokerface Дом и сам движок не считаем.


      1. xenohunter
        24.09.2016 01:05

        Такие условия слишком сложны для проверки. Нужно держать каждую игру запущенной (и играть в неё) довольно длительное время, чтобы удостовериться в объёмах используемой памяти. Что касается deflate, на сайте выкладывается разархивированная версия, так что в браузер попадает просто html-файл.


  1. impwx
    23.09.2016 21:13
    +2

    Самый первый кусок кода показывает, как нельзя делать минификатор. Замена подстроки this на s сломала логику — вторая строка будет эквивалентна this.s.s.s а не this.this.this.this.

    Уверен, что автор это понимает и в продакшен-коде подобной ошибки бы не допустил, но все же пример стоит сделать более наглядным.


    1. 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;

      Вариант с переменной короче на один символ при четырёх обращениях к объекту. Каждое следующее обращение будет экономить ещё по три символа.


  1. xGromMx
    23.09.2016 21:30
    +2

    А мне вот это понравилось https://github.com/agar3s/devil-glitches


    1. xenohunter
      24.09.2016 01:13
      +1

      Да, очень красивая игра. Но мой фаворит — Super Chrono Portal Maker.


  1. Shtucer
    24.09.2016 00:56
    +2

    Например, в одном из конструкторов было 39 штук this. Заменив их на self, получилось сэкономить более 100 байт.

    Но как?


    1. xenohunter
      24.09.2016 01:11

      В комментарии выше есть пример; self при минификации кода становится односимвольной переменной.


      1. Shtucer
        24.09.2016 01:19
        -1

        В том комментарии this напрямую превращается в s. А в статье превращением this в self экономятся килобайты.


        1. 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 стал одним символом


  1. nanshakov
    24.09.2016 11:21
    +1

    Честно говоря не знаю, какой алгоритм использует zip для сжатия. Но думаю можно подобрать имена так, что бы получался хороший вариант сжатия. Не понятно, почему не считать размер просто исходника.


    1. xenohunter
      24.09.2016 11:24

      Возможно, организаторы решили, что 13 Кб исходников — слишком мало для интересных игр. А было бы намного интереснее. Вон, люди пишут замечательные демки на JS в пределах 1 Кб.


  1. wickedweasel
    26.09.2016 04:03
    +2

    А вы не пробовали js хранить в виде png (пусть даже в инлайновом внутри html) и распаковывать его на лету?
    https://habrahabr.ru/post/102153/


    1. gearbox
      26.09.2016 11:14

      Прикольная заморочка.


    1. xenohunter
      26.09.2016 11:25
      +1

      Читал про это, но решил не использовать, так как потом всё равно в zip-архив нужно упаковать, а я сомневаюсь, что PNG бы сильно сжался. К тому же, только в этом году разрешили эту фичу использовать.


  1. saksmt
    27.09.2016 08:37
    +1

    *Почти offtop:* для любителей подобных соревнований есть ещё 1k Intro (http://archive.assembly.org/2016/1k-intro), но там несколько хардкорней — ассемблер, бутсектор, крутые демо.