JS1k — пишем отличное веб приложение в 1024 байт



    Уже второй год я участвую в JS1k, в прошлом году был пробный скрипт, сейчас я решил подойти основательно. Приложения я уже написал и отправил. В статье я хочу поделиться своим опытом: как стоит писать приложение для JS1k, чем сжимать, как сократить код в 4 раза и вообще как впихнуть что-то интересное в 1 Кб.

    Тема текущего JS1k "Oregon Trail" — классическая игра для Apple II (что это можно нагуглить). Поэтому рекомендуется написать что-то в этом духе, но это не обязательно.

    Начало


    Вам необходимо придумать небольшое приложение или демку, которое по вашему мнению может влезть в 1кб (продумать детали и управление). Если вы сомневаетесь в своих прикидках, то можно посмотреть, что впихнули в прошлом году: Legend Of The Bouncing Beholder, Tiny chess. Прочитайте правила и используйте html шаблон — тогда ваше приложение будет 100% работать в демо среде.
    Начните писать скрипт, без каких-либо оптимизаций, но следите за его размером. Если он стал больше 4-5Кб — вам стоит придумать другую тему или в будущем придется попотеть (у меня было 4393 байт).

    Первичная упаковка кода


    Ваш код должен быть в замыкании, иначе ничего не получится. Из всех существующих упаковщиков лучший — UglifyJS на все остальные можно не смотреть. Если вам лень ставить UglifyJS, то используйте веб-интерфейс.
    Упакуйте ваш код, если он получился в пределах 1024 байт — отлично — можно дальше не читать, а сразу отправить приложение. То, что ваш код сразу влез в 1024 байт говорит о том, что ваше приложение не достаточно интересное или не детализированное. Если ваш ужатый код стал в пределах 2Кб — все нормально не стоит делать таких глаз «о_О», уверяю вас его можно сократить ещё в 2 раза (у меня было около 1600 байт).

    Ручное сжатие или сжимаем несжимаемое


    Эта часть об ручном сжатии тех моментов, которые не умеет UglifyJS. Сейчас важен каждый байт!
    Каждый раз когда вы что-то исправили упаковывайте код UglifyJS и смотрите размер. Проверяйте, чтобы все работало.

    0. Не стоит делать работу за минификатор — срочно дайте понятные названия всем свойствам/функциям! Пишите читаемый код, минификатор удалит лишние скобки и точки с запятой за вас.
    1. Если вы точили под IE — удалите все хаки — он не участвует (ch9+, o11+, fx3.6+, sa5+).
    2. Удалите все хаки (дописанные функции bind, forEach), да прямо сейчас.
    3. Вам надо избавиться от зарезервированных слов — удаляйте лишние var, typeof. Сократите число функций до минимума (объедините если возможно) пример1 пример2.
    4. Весь HTML, созданный через DOM, перепишите на ручное создание(строки).
    5. Найдите часто употребляемые имена методов свойств (2 и более раза) и строковые и числовые константы — составьте словарь и замените точечный вызов на скобочный.
    // Было:
    var canvas, div1, div2;
    canvas.moveTo(150, 150);
    canvas.moveTo(153, 151);
    canvas.moveTo(11, 151);
    canvas.moveTo(153, 120);
    canvas.moveTo(153, 1);
    div1 = '<div style="width:150px;height:200px;color:red"></div>';
    div2 = '<div style="width:150px;height:200px;color:blue"></div>';
    
    // Стало:
    var canvas, div1, div2,
          __moveTo__ = 'moveTo';
          __width_height__ = 'width:150px;height:200px;';
    canvas[__moveTo__](150, 150);
    canvas[__moveTo__](153, 151);
    canvas[__moveTo__](11, 151);
    canvas[__moveTo__](153, 120);
    canvas[__moveTo__](153, 1);
    div1 = '<div style="' + __width_height__ + 'color:red"></div>';
    div2 = '<div style="' + __width_height__ + 'color:blue"></div>';
    

    Формула показывающая на сколько сократится код
    Для методов: (N * (L+1)) - (N * (2 + V) + V + 4 + L)
    L — длина метода без точки
    N — число вызовов
    V — длина переменной после уменьшения минификатором (обычно 1)
    В нашем случае мы получим 9 байт

    Для строк (в худшем случае): N * L - (N * (4 + V) + V + 4 + L)
    L — длина строки
    N — число замен
    V — длина переменной после уменьшения минификатором (обычно 1)
    В нашем случае мы получим 13 байт

    6. Вынесите часто используемые глобалы в переменную var __document__ = document;
    7. Посмотрите нужен ли вам with
    8. Используйте только глобальные переменные вашего замыкания
    9. Замените циклы for на while habrahabr.ru/blogs/javascript/115369/#comment_3737137, избавьтесь от оптимизаций счетчиков c = smth.length
    // Было:
    for (i = 0;i<smth.length; i++)do(i);
    
    // Стало:
    i=smth.length;while(i--)do(i);
    

    Ещё минус 6*N байт
    10. Если вы создаете функции через Function Expression var blabla= function (){}; переделайте их в Function Defination function blabla(){} Ещё минус пара байт.
    11. Вместо getElementById, getElementsByTagName используйте querySelector, querySelectorAll
    12. Вместо Math.round(num) используйте ~~num
    13. Удалите двойные и одинарные кавычки из атрибутов вашего HTML кода <div id="aaa"> - <div a=aaa>
    14. Замените строки в id элементов на числа <div id=aaa> - <div a=1>
    15. Сократите цвета до 3-4 символов. Не стоит использовать точные цвета их можно всегда заменить идентичными для восприятия. '#ff0000' -> 'red', '#fedc52' -> '#fd5'
    16. js1k предоставляет нам 3 переменные for free, используйте их и внесите их в своё замыкание в порядке a, b, c
    var b = document.body;
    var c = document.getElementsByTagName('canvas')[0];
    var a = c.getContext('2d');
    (function (ctx, __document__body__, canvas) {
    // Ваш код
    }(a,b,c))
    
    Порядок a,b,c важен потому, что минифиикатор переделает ваш код в такой вид:
    (function(a,b,c){
    // Ваш код
    }(a,b,c))
    
    И вам не нужно будет парься об сопоставлении имен внутренних переменных с внешними и можно будет убрать глобальное замыкание. Минус целых 26 байт!
    17. Используйте короткое сравнение e.keyCode^27||e.preventDefault() вместо e.keyCode==27&&e.preventDefault()
    18. Если ваш код все ещё больше 1024 — начинайте удалять не важные блоки (детали), если это возможно.
    19. Если у вас не демка и не игра, то вам нужно намекнуть пользователю как ей пользоваться не читая описание.
    20. Если это возможно, используйте html атрибуты для бинда событий, или присваивайте события через точку, используйте более короткие название событий onkeyup вместо onkeypress (в моём коде в 1м месте атрибут использовать невозможно)

    Советы из комментов

    1. Используйте, оно буквенные теги a,b,i,p,q,s,u (но не создавайте свои — Опера не умеет вызывать события в атрибутах в кастомных тегах)
    2. Попробуйте другие упаковщики, возможно Углифай вам не подходит compressorrater.thruhere.net

    Пример, того, что может у вас получиться после ручной оптимизации


    В этом году я решил создать sticky notes приложение с сохранением данных в localStorage. Вот такой код получился у меня после оптимизации:
    // Full version of Notes ll be on my web site soon.
    // Creating closure to compile with UglifyJS
    // (!) After compile remove global closure manually
    (function (ctx, __document__body__, canvas) {
        // Dictionary for some frequently used method names and strings
        var __fillStyle__ = 'fillStyle',
            __dblclick__ = 'dblclick',
            __setAttribute__ = 'setAttribute',
            __background_and_left__ = 'position:absolute;left:',
            __fillRect__ = 'fillRect',
            __addColorStop__ = 'addColorStop',
            __innerHTML__ = 'innerHTML',
            __textarea__ = 'textarea',
            __ffe__ = '#ffe',
        
        // Other shorthands
            __document__ = document,
            __localStorage__ = localStorage,
            i,j,c,imageData,
            width = 200,
            height = 250,
            paperGradient = ctx.createLinearGradient(width, height/2, width, 0);
    
        // Making fixed canvas size
        canvas[__setAttribute__]('width', 202);
        canvas[__setAttribute__]('height', 275);
    
        // Creates note at e.clientX e.clientY
        function createPaper(e) {
            // Opera haven't onbeforeuload event
            // I must save document.body content on each keyup and each keyCode=27 keydown
            // Sorry... :3
            // (!) replace b and save to actual after min
            __document__body__[__innerHTML__] += '<a onkeydown=event.keyCode^27||b.removeChild(this),w() style=' + __background_and_left__ + e.clientX + 'px;top:' + e.clientY + 'px;><i style=' + __background_and_left__ + '30px;top:10px;color:#a53;font-size:9px>'+Date()+'</i><img src=' + imageData + '><' + __textarea__ + ' onkeyup=this.'+__innerHTML__+'=this.value,w() style=' + __background_and_left__ + '28px;top:33px;background:transparent;width:170px;height:200px;border:0;line-height:20px;overflow:hidden>Esc</' + __textarea__ + '></a>';
            save();
        }
        
        // Saves document.body to localStorage
        function save() {
            __localStorage__[__dblclick__] = __document__body__[__innerHTML__];
        }
    
        // Making sexy paper
        // Gradient
        paperGradient[__addColorStop__](0, '#ff9');
        paperGradient[__addColorStop__](1, __ffe__);
    
        ctx[__fillStyle__] = paperGradient;
        ctx.strokeStyle = '#aa7';
    
        // Draw paper body
        ctx.strokeRect(1,1, width,height);
        ctx[__fillRect__](1,1, width,height);
    
        // Creating paper texture
        i = width;
        ctx[__fillStyle__] = __ffe__;
        while(--i) {
            while(--j) {
                Math.random()>.7&&ctx[__fillRect__](i,j,1,1);
            }
            j = height;
        }
    
        // 2 Vertical red lines
        ctx[__fillStyle__] = '#a51';
        ctx[__fillRect__](20, 1, 1, height);
        ctx[__fillRect__](22, 1, 1, height);
    
        // Some horizontal gray lines
        ctx[__fillStyle__] = '#aaa';
        for (i = 50; i < height; i += 20) ctx[__fillRect__](0, i, width, 1);
    
        // Grabbing image source
        imageData = canvas.toDataURL();
    
        // Print "Hello message" or load localStorage content
        __document__body__[__innerHTML__]=__localStorage__[__dblclick__] || __dblclick__+', uses ' + __localStorage__;
    
        // Some action events
        __document__.addEventListener(__dblclick__, createPaper, 0);
    
    
    }(a,b,c)) // Vars must be in a, b, c order
    

    Этот код сжимается Углифаером до 1048 байт, потом удаляется глобальное замыкание и он сокращается ещё до 1022 байт. При оптимизации мне пришлось сократить некоторые детали.

    Ссылки на мою демку


    Оригинал — то, что было в начале (двойной клик создает заметку, Esc по заметке — удаляет, данные красиво лежат в localStorage) JS — 4393 байт: azproduction.ru/labs/1kjs-ios-javascript-notes/index.original.html
    Демка на JS1k (управление аналогично оригиналу) JS — 1022 байт: js.gd/1hn (старая версия)

    Если сравнить эти 2 приложения, то фактически они ничем не отличаются. Из второго убран скотч и нет числа создания.

    То, что не вошло в советы, но пригодится в будущем


    -1. Используя bind можно значительно сократить длину вызова метода. FF 3.6 не умеет bind поэтому такой способ нам пока не подходит.
    // Без изменений
    c.lineTo(150,150);c.lineTo(150,200);c.lineTo(150,250);c.lineTo(250,350);c.lineTo(450,350);
    // Оптимизация со словарем
    var a='lineTo';c[a](150,150);c[a](150,200);c[a](150,250);c[a](250,350);c[a](450,350);
    // Оптимизация с контекстом
    var a=z.lineTo.bind(z);a(150,150);a(150,200);a(150,250);a(250,350);a(450,350);
    

    Если вы захотели участвовать, то не стоит откладывать все на последний момент — придумывайте демки, уменьшайте код. До дедлайна JS1k ещё очень далеко — 24 апреля 2011. Удачи!

    Критика, пожелания, советы по уменьшению кода приветствуются!

    UPD Добавил советы читателей и пару своих. Почитав комменты, посмотрев другие скрипты, уменьшил размер скрипта с 1022 до 975 байт и добавил один удаленный функционал.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 48

    • UFO just landed and posted this here
        0
        А почему делать можно только через замыкание?
          +3
          Иначе минификатор будет думать, что все ваши переменные глобальные и не будет их оптимизировать — не будет сокращать имя переменной.
            +1
            Странно, что опции для минификатора нету, типа «воспринимать как локальный контекст» =(
          +1
          Приемы ручного сжатия напомнили, как мы участвовали в Time Limit Exceeded — индийский контест на решение задач на C так, чтобы программа получалась минимальной длины. Славно мы тогда поизвратились, я о C столько нового узнала :-)
            +3
            индийский контест

            чтобы программа получалась минимальной длины

            Налицо парадокс :)
              0
              Они стараются измениться ;)
                +2
                Ннэ, так учатся писать нечитабельный код. Проведут соревнование, а потом разбирают решения-победители и берут на вооружение самые чудовищные приемы :-)
            0
            Вообще можно ещё код сжать в png. Загрузчик кода из png, разумеется, тоже нужен будет, но размер таким образом можно уменьшить, например, и со 123 кб до 30 кб (как показывает практика). :)
              0
              загрузчик под килобайт вроде весит
                0
                Я загрузчик ужимал до минимума: bolknote.ru/files/tank-1k-game/
                  +1
                  1 кб? С загрузчиком? Браво! :)
                    +1
                    Спасибо :)
                    0
                    Здорово — распаковщик в 109 байт + 90 байт накладных расходов. Правда этот подход в таком виде в каком есть не годится для js1k (запрещены внешние ресурсы). Если создавать картинку с image:data, то получим +30-40% к весу картинки. И не понятно будет ли оправдано это.

                    Вобщем прикинул: у вас картинка весит 753 байта, а кода в ней на 1059 (хотя я думал там будет 1059*4 байт ибо RGBA, почему вы брали каждый 4-й элемент, храните ещё что-то в RGB?). С data:image мы получим около 1054 байт + 100 байт распаковщик (можно оптимизировать если точить под js1k). Если занимать все пространство RGBA, то минимизированный скрипт можно ужать ещё в 5-6 раз!

                    Имея картинку в 650 байт (910 image:data) в неё можно засунуть около 3640 байт ужатого скрипта. Плюс 100 байт оптимизированный под js1k распаковщик. В сумме получим 1010 байт чистого кода, который теоретически должен развернуться до 3640 байт.

                    Поправьте если что не так.
                      +1
                      Есть вариант без внешних ресурсов :) bolknote.ru/files/tank-1k-game-2/ (правда, я знаю как его ещё уменьшить).
                      Вобщем прикинул: у вас картинка весит 753 байта, а кода в ней на 1059 (хотя я думал там будет 1059*4 байт ибо RGBA, почему вы брали каждый 4-й элемент, храните ещё что-то в RGB?)
                      Я вообще хотел класть во все четыре компоненты, но сжимается лучше всего выбранный вариант.

                    +1
                    Да, верно. Тут это и правда неуместно будет, т.к. цифры другие. Здесь это было реализовано и в рамках 10 кб это будет уместно. Не подумал.
                      0
                      10кб тоже интересный формат, но мало кому доступен — слишком много времени уходит на написание и оптимизацию.
                  +2
                  Напомнило (если кто-то ещё не читал): www.wasm.ru/article.php?article=onebyte
                    0
                    Восхитительно! Благодарю за наводку.
                    +2
                    var a='lineTo';c[a](150,150);c[a](150,200);c[a](150,250);c[a](250,350);c[a](450,350);
                    c[a='lineTo'](150,150);c[a](150,200);c[a](150,250);c[a](250,350);c[a](450,350);

                    Вообще как-то негусто приёмов описано.
                      +1
                      Все в ваших руках.
                        0
                        c[a='lineTo'](150,150);c[a](150,200);c[a](150,250);c[a](250,350);c[a](450,350);

                        l=c.lineTo;l(150,150);l(150,200);l(150,250);l(250,350);l(450,350);
                          0
                          Серёг, или я торможу или lineTo получит в this window.
                            0
                            не, это я торможу :) Забыл про контекст
                            0
                            var a=z.lineTo.bind(z);a(150,150); ;)
                              +1
                              23+11*N vs 15+14*N
                              С семи вызовов это становится выгодней, а если выкинуть «var » в начале, то — с пяти.
                                +1
                                а если заменить .bind на [__bind__]? ;)
                                  0
                                  если бинд 1н, то не получим профита ;)
                                    0
                                    Ну да, все зависит от приложения)
                          0
                          попробуйте для сжатия compressorrater.thruhere.net/ для меня там лучшим оказался Packer (Version 3.1) + допиливаю руками названия функций которые он не умеет сжимать. в моем случае Packer оказался на 10% лучше UglifyJS. Пишу тетрис. Пытаюсь затолкать 2048 байт. осталось еще 100 байт срезать, уж не знаю где еще :)
                          0
                          или я чегото не понял, или обясните мне: почему не использовали gcc? он очень качественно все делает и Вам бы не пришлось почти ничего оптимизировать руками. Почему о нем нету ни слова?
                            –2
                            GCC умеет работать с JavaScript?
                              0
                              ага, code.google.com/closure/
                              Куча опций оптимизаций, сам сжимаю только им — по анализам, вроде самый оптимальный
                                0
                                GCC Advanced=on сжал мой скрипт до 1100 байт, UglifyJS — 1048, Packer 3.1 — 1049. Притом Packer и GCC коверкают порядок переменных (d,g,f вместо a,b,c) и от обрамляющего замыкания невозможно сразу избавиться. Если вы в GCC пользуетесь особыми настройками, то посвятите.
                                  0
                                  хм… странно, я пробовал пакером и gcc свои скрипты — gcc выиграл во много раз. конкретно по цифрам могу написать вечером

                                  для gcc advanced нужно приводить код в спрециальный вид, собственно как и для uglyjs только свои правила.
                                  0
                                  Однако… Это не тот gcc, о котором все подумали :)
                                    0
                                    ну конечно не тот. я думал раз тема про JS все поймут о чем я ;)
                              0
                              Вроде бы не нашел у вас совет такого рода:
                              Если часто используется какой либо метод, например Math, можно сократить код так:
                              var m = Math;
                              И вызывать, например, так: m.abs();

                              Возможно я туплю и glifyJS это все уже сам делает.
                                0
                                на сколько я понимаю любой пакер делаем именно так, вычленяет одинаковые строки и заносит их в глобальные переменные до которых потом доступается.
                                +3
                                9. Замените циклы for на while, избавьтесь от оптимизаций счетчиков c = smth.length
                                Потом замените while на for и получите ещё N байт ;)
                                i=smth.length;while(i--)do(i);
                                for(i=smth.length;i--;)do(i);
                                


                                13. Удалите двойные и одинарные кавычки из атрибутов вашего HTML кода <div id=«aaa»> — <div a=aaa>
                                14. Замените строки в id элементов на числа <div id=aaa> — <div a=1>

                                Почему бы не создавать собственные элементы? Например, <r>. Вроде бы и html короче и путь в querySelectorAll. Конечно, с канвасом так не выйдет.
                                  0
                                  Дельно, правда опера не умеет вызывать события в атрибутах (onclick) у своих элементов <r>
                                  <z onclick=alert(1)>qqq</z>
                                    0
                                    Действительно. Прям как у ie. Чтож, есть элементы a,b,i,p,q,s,u + значительное количество двубуквенных.
                                  0
                                  Кстати, иногда имеет смысл сделать подобную оптимизацию:
                                  __fillStyle__ = 'fillStyle',
                                  __fillRect__  = 'fillRect',
                                  // =>
                                  __fill__ = 'fill',
                                  __fillStyle__ = __fill__ + 'Style',
                                  __fillRect__  = __fill__ + 'Rect'
                                  


                                  Особенно в паре с другими оптимизациями:
                                  __fillStyle__ = 'fillStyle',
                                  __fillRect__  = 'fillRect',
                                  // =>
                                  __fill__   = 'fill',
                                  __stroke__ = 'stroke',
                                  __Style__  = 'Style',
                                  __Rect__   = 'Rect',
                                  __fillStyle__   = __fill__   + __Style__,
                                  __fillRect__    = __fill__   + __Rect__,
                                  __strokeStyle__ = __stroke__ + __Style__,
                                  __strokeRect__  = __stroke__ + __Rect__,
                                  


                                  И, особо, при использовании длинных повторяющихся названий:
                                  __getElement__             =  'getElement',
                                  __getElementById__         = __getElement__ + 'ById',
                                  __getElementsByTagName__   = __getElement__ + 'sByTagName',
                                  __getElementsByClassName__ = __getElement__ + 'sByClassName',
                                  
                                    0
                                    В 1кб это не выгодно, пытался так сделать, сужу по своему опыту
                                      0
                                      ctx.f = ctx.fillRect;
                                      ctx.__defineSetter__(«s», function (arg) { this.fillStyle = arg })

                                      ctx.s = 'green';
                                      ctx.f(5, 5, 25, 25);
                                      0
                                      Ещё читал вариант, если необходимо использоваться сообственные функции, то лучше создать одну функцию, а внутри сделать ветвление зависимо от первого параметра.
                                      function foo () {
                                        // foo code
                                      }
                                      function bar () {
                                        // bar code
                                      }
                                      function qux () {
                                        // qux code
                                      }
                                      
                                      // =>
                                      
                                      function g (fn) {
                                        if (fn == 'foo') {
                                          // foo code
                                        }
                                        if (fn == 'bar') {
                                          // bar code
                                        }
                                        if (fn == 'qux') {
                                          // qux code
                                        }
                                      }
                                      

                                        +2
                                        В таком виде не очень подходит для событий — основное место скоплений функций, но можно отфильтровать по event.type.
                                        function g(e,t) {
                                          t = e.type;
                                          t=='click'&&code;
                                          t=='keyup'&&code;
                                          t=='keydown'&&code;
                                        }

                                      Only users with full accounts can post comments. Log in, please.