Заметки за июль 2012 года

Профилирование CSS и неиспользуемые селекторы

Я уже писал, что самыми эффективными селекторами с точки зрения анализа и обработки их браузером будут простые селекторы. Когда браузер встречает каскад, то ему требуется пройти от текущего элемента до корня документа в поисках всех элементов, удовлетворяющих условию селектора.

А что произойдет, если браузер никогда не встретит элемент с нужными характеристиками во время такого обхода? Чтобы ответить на этот вопрос я решил сделать несколько замеров скорости отрисовки достаточно сложной страницы (около 2500 элементов и глубиной 20 уровней). Страница содержала реальные и вполне типичные для проекта данные — таблица на сотню строк, свёрстанная блочными элементами.

Для создания нагрузки я применил модифицированный скрипт, который постоянно прокручивает окно, заставляя браузер перерисовывать всё содержимое окна. А замеры делал с помощью профайлера Opera Dragonfly.

Итак, на моей тестовой странице была подключена библиотека виджетов jQuery UI. Но так как фактически никаких виджетов там не было, то CSS-правила, относящиеся к jQuery UI, никогда не применялись. Они-то и создавали эту паразитную нагрузку.

Сначала я сделал замер с подключенными стилями темы.

Эксперимент с подключенными стилями темы jQuery UI

А затем повторил эксперимент без подключения этих стилей.

Эксперимент без стилей jQuery UI

По рейтингу затраченного времени на пересчёт стилей видно, что самыми «тяжёлыми» оказались каскады, для которых никогда не находятся подходящие элементы. На странице множество элементов span (1123, если быть точнее), но, ни один не имеет предка с классом ui-datepicker-next или ui-datepicker-prev.

Из этого конкретного эксперимента можно сделать вывод, что чем больше будет загружено CSS-правил, тем медленнее будет работать движок браузера на перерисовке страницы, даже если эти правила никогда не будет применены на странице.

Отсюда напрашивается ещё один вывод: объединение разношерстных стилей в один файл с большой вероятностью ухудшит производительность стадий reflow и repaint. Деградация будет не так заметна, если в стилях гарантировано не применяются каскады с селекторами по тегу.

Комментарии к заметке: 6

Подсветка области клика на iOS

Когда пользователь прикасается к ссылке или элементу, для которого указан обработчик клика, то Safari на iOS подсвечивает этот элемент.

Цвет этой подсветки задается с помощью CSS свойства -webkit-tap-highlight-color.

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

Предположим, что нас интересует только клик по элементу с классом item__add.

$(".list").on("click", ".item__add", function () { … });

Но подсветка будет активироваться у элемента с классом list, так как именно на него повешен обработчик события. Исправим это CSS свойством в селекторе .list.

.list {
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
Комментарии к заметке: 3

Конкурирующие асинхронные запросы

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

var concurrent = (function () {
    var concurrentRequests = {};
    return function (opts, cache) {
        // вместо jqXHR функция будет возвращать promise-объект
        var deferred = $.Deferred(), promise = deferred.promise();

        // сохраняем колбеки, если они есть
        promise.done(opts.success || $.noop);
        promise.fail(opts.error || $.noop);
        promise.always(opts.complete || $.noop);

        // удаляем колбеки из параметров,
        // так как cacheable не обрабатывает их
        delete opts.success;
        delete opts.error;
        delete opts.complete;

        // таймстемп запроса.
        // ответы с таймстемпом меньше текущего игнорируются
        var requestId = concurrentRequests[opts.url] = $.now();

        // кеширующий или обычный запрос
        (cache ? cacheable : $.ajax)(opts)
            .done(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.resolveWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            })
            .fail(function () {
                if (concurrentRequests[opts.url] <= requestId) {
                    deferred.rejectWith(promise,
                        Array.prototype.slice.apply(arguments));
                }
            });

        return promise;
    }
}());

В паре с функций cacheable получилась достаточно универсальная замена традиционной функции $.ajax.

$(".list").each(function () {
    var list = $(this);
    list.on("click", ".list__page", function () {
        concurent({
            url: "/list/",
            data: {page: $.tim($(this).text())},
            dataType: "html"
        }, true).done(function (markup) {
            list.html(markup);
        });
    });
});
Оставте свой комментарий

Уборка мусора в Git

Удалить все неиспользуемые объекты можно командой

git gc

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

Удалить все ветки, которых нет во внешнем репозитории можно командой

git remote prune origin

Если даже на какой-либо объект нет явной ссылки, то на протяжении 30 дней на все объекты сохраняется ссылка в reflog. По этому когда производится уборка мусора все коммиты за последний месяц всё равно остаются в репозитории.

Чтобы избавиться от таких недоступных комитов, выполним последовательность команд

git reflog expire --expire=1.minute refs/heads/master
git fsck –unreachable
git gc
Комментарии к заметке: 1

Нелинейная шкала слайдера

Когда слайдере нужно использовать для ввода несоизмеримых по величине данных (например, от 1 тысячи до 10 миллионов рублей), то разумнее использовать какую-то нелинейную шкалу. В частности, для ввода сумм подходящей шкалой будет логарифмическая (по основанию 10).

Для сравнения приведу два слайдера с линейной и нелинейной шкалой.

Нелинейная система координат

Чтобы обычный слайдер превратить такой нелинейный нужно определить две функции, которые будут переводить числа из одной системы отсчета в другую.

function fromSlider(value) {
    return Math.round(Math.pow(10, value));
}

function toSlider(value) {
    return Math.log(value) / Math.log(10);
}

Теперь там где нужно установить значение в слайдер используем toSlider, а там где получаем значение из слайдера — fromSlider.

$(".logarithmic__slider").slider({
    min: toSlider(1000),
    max: toSlider(10000000),
    step: 0.01,
    value: toSlider(initialValue),
    slide: function (e, ui) {
        $(".logarithmic__value").html(fromSlider(ui.value));
    }
});

Округление и форматирование

Чтобы данные выглядели презентабельно их хорошо бы ещё округлить и отформатировать.

function round(n, threshold) {
    var digits, out;

    n = n.toFixed(0);
    digits = n.length;
    out = n.substr(0, Math.min(digits, threshold));

    if (digits > threshold) {
        out = out + "000000000000".substr(0, digits - threshold);
    }

    return parseInt(out);
}

Функция оставляет только указанное количество значимых разрядов в числе. Остальные будут обнулены.

function splitGroups(n, delim) {
    var digits;

    delim = delim || '<span class="tsp"> </span>';

    n = n.toFixed(0);
    digits = n.length > 3 ? n.length % 3 : 0;

    return (digits ? n.substr(0, digits) + delim : '') +
        n.substr(digits).replace(/(\d{3})(?=\d)/g, "$1" + delim);
}

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

.tsp {
    font-size: 50%;
    line-height: 1;
}

Или можно указать требуемый разделитель вторым параметром функции.

Комментарии к заметке: 1