Pull to refresh

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

Reading time 7 min
Views 3.4K


Уже второй год я участвую в 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 байт и добавил один удаленный функционал.
Tags:
Hubs:
+56
Comments 48
Comments Comments 48

Articles