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

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

    Поделиться публикацией

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

      0

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

        0
        Спасибо! Да, в минификации модулей есть и свои особенности. Здесь я, конечно, модули не использовал, чтобы не тратить место на require или что-то подобное: было ощущение, что каждый байт на счету.
        +1
        Я такое извращение в Кукараче только проделывал, чтобы сократить длину кода. Только преподавательница не могла прочитать мой код, так что пришлось делать его читаемым и повторяемым.
          +5
          Вы не пишете самое главное: какая разница получается после gzip.
            0
            Там и не используется gzip, финальный файл пакуется в zip-архив. Я написал в посте, что размер index.html был 31.9 Кб, а в виде архива занял 10.1 Кб.
              0
              Но без некоторых оптимизаций, приведённых в посте он был бы меньше в zip, или важен именно несжатый вариант?
                0

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

                  +2
                  this.this.this.this.
                  var s=this;s.s.s.s.

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

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

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

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

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

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

                +4

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

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

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

                    0

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

                      0

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

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

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

                      Первый кусок кода — только для сравнения длин строк. Это вообще не рассматривается как рабочий код. Смысл в том, чтобы наглядно показать, сколько символов будут занимать четыре обращения к 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;

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

                      +2
                      А мне вот это понравилось https://github.com/agar3s/devil-glitches
                      +2
                      Например, в одном из конструкторов было 39 штук this. Заменив их на self, получилось сэкономить более 100 байт.

                      Но как?
                        0

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

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

                            В процессе минификации 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
                        Честно говоря не знаю, какой алгоритм использует zip для сжатия. Но думаю можно подобрать имена так, что бы получался хороший вариант сжатия. Не понятно, почему не считать размер просто исходника.
                          0

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

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

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

                              +1

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

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

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

                              Самое читаемое