«Ускоряем» открытие тяжелого сайта

На днях потребовалось ускорить открытие сайта. Проблема заключалась в том, что одни только JS-файлы, даже собранные в один и сжатые обфускацией, весили более 500kB, а ведь еще есть css тоже довольно крупный.
В связи с этим, пользователям, у которых файлы закешированы (например новый пользователь или после билда сменилась версия), с медленным интернетом приходилось ждать довольно долго, смотря на белый экран.

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

Метод работает на IE9 (с нюансами естесственно), IE10 и выше — идеально, а так же на всех основных браузерах Chrome, Firefox, Opera, Safari.

Стили

.progress-bar {
    margin: 200px auto;
    top : -4px;
    display: none;
    width : 400px;
    border: 1px solid gray;
    -webkit-border-radius: 8px;
    -moz-border-radius: 8px;
    border-radius: 8px;
}

.progress-bar div {
    height : 16px;
    width  : 0;
    background: lightgray;
    -webkit-border-radius: 8px;
    -moz-border-radius: 8px;
    border-radius: 8px;
}

.progress-bar-text {
    text-align: center;
    margin-top: 200px;
}

 

Простейшая верстка:

<body>
    <div class="progress-bar">
        <div id="progressBar"></div>
    </div>

    <div id="loadingPanel" class="progress-bar-text">
        Подождите... Идет загрузка...
    </div>
</body>

 

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

Список того, что нужно загрузить будем хранить в переменной fileList.

var progressWidth  = 0,
    fileList = [
        {type : 'style',  url : '/css/bundle.css'},
        {type : 'script', url : '/js/bundle.js'}
    ];

 

Функция, запускающая загрузку файлов, по завершении вызовет callback переданный в нее.

function loadFiles(callback) {
    if (loaded) {
        // Дабы не загружать дважды
        window.console && console.error('Already loaded.');
        return;
    }

    loaded = true;

    var len = fileList.length,
        count = len;

    var _afterAll = function() {
        if (--count == 0) {
            if (typeof callback == 'function') {
                setTimeout(callback, 10);
            }
        }
    };

    for(var i = 0; i < len; i++) {
        loadFile(fileList[i], len, _afterAll);
    }
};

 

В цикле мы запускаем функцию отвечающую за получение файла.
Эта функция сама будет обновлять наш progressBar.
Но как получать прогресс загрузки? Оказывается XMLHttpRequest уже давно имеет событие onprogress для скачивания файла (подробнее можно почитать здесь).
Но к сожалению IE9 не умеет вызывать onprogress, и по этому мы проверяем содержит ли new window.XMLHttpRequest() в себе событие onprogress. Если нет отображаем заглушку, с уведомлением, ждите, мы грузимся.
По завершении загрузки, в зависимости от типа файла мы либо подключаем стиль (браузер в этот раз файл берет из кеша, для этого-то мы его и качали), либо евалим если это JavaScript.

function loadFile(file, len, callback) {
    var progressBar  = $('#progressBar'),
        loadingPanel = $('#loadingPanel');

    file.url += '?' + REVISION; // тут мы добавляем номер ревизии, чтобы при каждом билде файл перезакачивался заново

    $.ajax({
        xhr: function () {
            var xhr = new window.XMLHttpRequest();

            if ('onprogress' in xhr) {
                progressBar.parent().show();
                loadingPanel.hide();

                var last = 0, max = 100 / len;
                xhr.addEventListener("progress", function (evt) {
                    if (evt.lengthComputable) {
                        var percentComplete = Math.min((evt.loaded / evt.total) * max, max);

                        if (last < percentComplete) {
                            progressWidth += percentComplete - last;
                            last = percentComplete;
                            progressBar.width(progressWidth + '%');
                        }
                    }
                }, false);
            }

            return xhr;
        },

        type: 'GET',
        url: file.url,
        success: function (data) {
            if (file.type == 'style') {
                $('head').append('<link rel="stylesheet" type="text/css" href="' + file.url + '" media="screen" />');
            } else if (file.type == 'script') {
                evalJS(data);
            }

            callback();
        },
        cache : true
    });
}

function evalJS(text) {
    try {
        if (window.execScript) {
            window.execScript(text);
        } else {
            eval.call(window, text);
        }
    } catch (error) {
        window.console && console.error(error);
    }
}

 

В jQuery, в метод ajax я передал свойство cache: true, для того, чтобы браузер не качал файлы если они уже есть в кеше, иначе, поумолчанию, jQuery будет качать .js файлы все время, не зависимо есть ли они в кеше или нет.

В общем-то все просто, но как оказалось, в Opera тоже не все гладко (по крайней мере в 12 версии) evt.loaded может в разы превышать evt.total.
Других подводных камней я не обнаружил.

Demo
Исходники примера