Всё только о JavaScript

/ Статьи / Отложенные вычисления

Очень быстрый setTimeout

Какой минимальный таймаут можно указать функции setTimeout? Указать можно одну миллисекунду, но вот реально минимальная задержка в разных браузерах будет разная, но больше миллисекунды, иногда значительно. Создадим небольшой тест.

var i = 0, d = new Date(), N = 1000;
(function() {
    if (i < N) {
        i++;
        setTimeout(arguments.callee, 1);
    } else {
        alert((new Date() - d) / N);
    }
})();

Этот код делает N последовательных (не рекурсивных) вызовов функции через setTimeout и выводит реальную среднюю задержку в миллисекундах. В идеале она должна быть равна 1, на практике всё гораздо печальней: в Firefox и Safari минимальная задержка в среднем равна 11 мс, в Opera — 2 мс, Chrome — 4 мс, IE — 15 мс.

Тем не менее есть возможность написать setTimeout с "нулевой" задержкой. Во всех современных браузерах, кроме IE младше 8-й версии, у объекта window есть метод postMessage, который отправляет окну/фрейму текстовое сообщение. Задержка между отправкой сообщения и его получением минимальная, чем и можно воспользоваться.

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

Для остальных браузеров код будет такой.

var setZeroTimeout = (function() {
    var fn, ctx;

    window.addEventListener('message', function() {
        if (fn) {
            fn.call(ctx);
        }
    }, false);

    return function(_fn, _ctx) {
        fn = _fn;
        ctx = _ctx;
        window.postMessage('', '*');
    };
})();

Если вы на своём сайте используете postMessage для других целей, можно создать скрытый iframe и посылать сообщения в него.

Перепишем тест под использование setZeroTimeout.

var i = 0, d = new Date(), N = 1000;
(function() {
    if (i < N) {
        i++;
        setZeroTimeout(arguments.callee);
    } else {
        alert((new Date() - d) / N);
    }
})();

Результаты будут примерно следующие: в Safari 2 мс, в Opera 0.04 мс (т.е. доли миллисекунды). В остальных браузерах результаты разнятся в зависимости от N: чем больше N, тем меньше среднее время. В Firefox — от 10 до 0.07 мс, в Chrome — от 1 до 0.02 мс.

Как видите, результат налицо — реализованный setZeroTimeout гораздо быстрее нативного setTimeout.