Алгоритмы и задачи клиентской оптимизации

Оптимизация структуры веб-страниц

Показывать лекцию целиком

5.1. Динамические стили: быстро и просто

В книге "Разгони свой сайт" уже затрагивалась тема быстрого добавления стилевых правил в исходный документ динамически, не затрагивая при этом стадию предзагрузки (когда у нас еще белый экран в браузере). В ней однако не был рассмотрен следующий вопрос: какой метод использовать для добавления массива CSS-пра-вил в сам HTML.

Естественно, что таких вариантов существует несколько, и дальше они все будут рассмотрены с точки зрения производительности в клиентском браузере.

5.1.1. Тестовое окружение

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

5.1.2. XHR в <body>

Выполняется XMLHttpRequest к CSS-файлу, затем содержимое последнего вставляется через innerHTML в <body> документа. Случай был выбран просто как базовый, потому что большое число узлов в DOM-дере-ве делает такую операцию сразу менее эффективной, чем вставка в <head>. Да и стили внутри тела документа запрещены стандартами.

// чтобы не копировать всем известный код, запишем так:
var xhr = new XMLHttpRequest;
if (xhr) {
xhr.onreadystatechange = function() {
try {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
// вставим полученные данные прямо в <body>
document.body.innerHTML +=
'<style type="text/css">' + xhr.responseText +
'</style>';
}
}
} catch(e){}
};
xhr.open("GET", 'styles.css?'+Math.random(), true);
xhr.send(null);
}

Тут сразу возникает вопрос: как считать применение стилей? Ведь у браузера нет такого события. Немного подумаем и вспомним всем известный и раскритикованный подход с использованием offsetHeight. Он, конечно, будет не самым лучшим выбором, так как добавляет небольшую задержку (пропорциональную числу узлов), но это будет допустимо в рамках выбранного метода (число узлов постоянно для всех измерений).

Итак, для снятия времени применения CSS-правил использовалась следующая конструкция (которая запускалась сразу после обращения к внешнему файлу):

var start = new Date();
var _timer = setInterval(function(){
// находим известный элемент документа и проверяем,
// применились ли к нему стили
if (document.getElementById('neck').offsetHeight < 300) {
// сообщаем о применении
alert('CSS files loaded in '+(new Date() - start));
// убиваем таймер
clearInterval(_timer);
}
}, 10);

Конечно, на время загрузки влияют сетевые задержки. Для борьбы с ними (и не только) бралась серия из 15 замеров, и значения, превосходящие текущее среднее более чем в 2 раза, просто отбрасывались (для контроля 3-дельта-выбросов).

Все результаты приведены в конце раздела.

5.1.3. XHR в head

В этом случае код вставлялся уже в <head> и применялся ряд методов для разных браузеров (ибо не все хотели через innerHTML или innerText вставлять полученные данные).

var text = xhr.responseText;
var head = document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
// для IE
if (style.styleSheet) {
style.styleSheet.cssText = text;
} else {
// для Safari/Chrome
if (style.innerText == '') {
style.innerText = text;
// для остальных
else {
style.innerHTML = text;
}
}
head.appendChild(style);

5.1.4. Быстрый XHR в head

В следующем варианте проверялось прямое добавление стилевых правил к innerHTML в <head> (для тех браузеров, которые это поддерживают). Оказалось, что это вариант даже медленнее, чем предыдущий.

Если осуществлять это относительно обычного HTML-документа, то DOM-дерево изменяется быстрее (в IE6/7), поэтому на данный момент в общем случае практикуется именно такой подход.

var text = xhr.responseText;
var head = document.getElementsByTagName('head')[0];
if (/WebKit|MSIE/i.test(navigator.userAgent)) {
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = text;
} else {
style.innerText = text;
}
head.appendChild(style);
} else {
head.innerHTML += '';
}

5.1.5. DOM-метод

И, наконец, хит сезона. Добавляем новый файл стилей прямо в <head> при помощи DOM-методов.

var link = document.createElement('link');
document.getElementsByTagName('head')[0].appendChild(link);
link.setAttribute('type','text/css');
link.setAttribute('rel','stylesheet');
link.setAttribute('href','style.css');

5.1.6. Результаты

Ниже приведена таблица по исследованным браузерам для всех вариантов. В ней указано время в миллисекундах, прошедшее от начала вызова внешнего файла до окончания применения всех стилей.

Браузер XHR в <body> XHR в <head> Быстрый XHR в <head> DOM
IE 6 482 379 342 335
IE 7 532 364 391 353
IE 8b2 370 326 301 284
FX 3 420 294 300 282
Opera 9 892 894 1287 764
Safari 3 - 308 286 296
Chrome - 349 335 367

5.1.7. Выводы

Как хорошо видно из таблицы, наиболее быстрым способом для динамического добавления стилей в документ являются DOM-методы почти во всех случаях. Для Safari/Chrome вставка через XHR и специальные методы оказываются быстрее (но не намного). Отдельно хочется отметить довольно медленную работу Opera в таких задачах: по возможности стоит избегать динамических стилей для этого браузера.

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

5.2. Оптимизация CSS-структуры

Этот раздел написан после прочтения статей Steve Souders — don't use @import (http:/www.stevesouders.com/bLog/2009/04/09/dont-use-import/) и Simplifying CSS Selectors (http://www.stevesouders.com/blog/ 2009/06/18/simplifying-css-selectors/) — и обсуждения с ним производительности CSS-правил. Данный материал подготовлен с помощью Аба-новой Ольги (http://www.getincss.ru/).

5.2.1. <link> vs. ©import

Существует два способа загрузки файлов стилей: использовать тег <link>:

<link rel='stylesheet' href='a.css'/>
или импортировать файлы с помощью @import:
<style type='text/css'>
@import url('a.css');
</style>

Стоит использовать <link> для удобства, но вы должны помнить, что @import нужно размещать всегда в самом верху блока стилей, в противном случае они не импортируются.

5.2.2. ©import ©import

В приведенном ниже примере подключаются два файла стилей: a.css и b.css. Каждый файл по загрузке занимает ровно 2 секунды, чтобы было удобно отследить влияние на скорость загрузки в дальнейшем. В первом примере применяется @import для загрузки обоих файлов стилей. Здесь HTML-документ содержит следующий блок стилей:

<style type='text/css'> @import url('a.css'); @import url('b.css'); </style>

Если вы всегда будете использовать только @import для загрузки стилей, то проблем с производительностью не будет, хотя, как мы увидим ниже, это может привести к ошибке с JavaScript. Оба файла загружаются параллельно (рис. 5.1) Но проблемы начинают появляться, если применять @import внутри файла стилей либо вместе с <link>.

(рис 5.1) Параллельная загрузка стилей. Источник: getincss.ru

5.2.3. <link> ©import

В следующем примере используется тег <link> для загрузки a.css и @import для b.css:

<link rel='stylesheet' type='text/css' href='a.css'/> <style type='text/css'> @import url('b.css'); </style>

В IE (тестировалось в 6, 7 и 8) это привело к тому, что файлы загружаются последовательно друг за другом, как показано на рис. 5.2. Соответственно, время загрузки страницы в IE увеличится.

(рис 5.2) @import блокирует <link> в IE. Источник: getincss.ru

5.2.4. <link> с ©import

Тут файл a.css загружается через <link> и содержит внутри правило @import для b.css: В документе:

<link rel='stylesheet' type='text/css' href='a.css'/>

в a.css:

@import url('b.css');

Этот способ также приводит к тому, что файлы загружаются последовательно (рис. 5.3), а не параллельно, и теперь это происходит не только в IE, но и в остальных браузерах. Если подумать — все логично: браузер загружает a.css и начинает анализировать его. Как только внутри обнаружено правило @import, начинается загрузка файла b.css.

(рис 5.3) @import блокирует <link> не только в IE. Источник: getincss.ru

5.2.5. Блоки <link> с ©import

Незначительное отличие от предыдущего примера привело к удивительным результатам: в IE. <link> используется для вызова a.css и для нового файла proxy.css, который содержит только @import для b.css.

В HTML-коде:

<link rel='stylesheet' type='text/css' href='a.css'> <link rel='stylesheet' type='text/css' href='proxy.css'>

В proxy.css:

@import url('b.css');

Результаты эксперимента в IE показаны на рис. 5.4. Первый запрос — HTML-документ. Второй запрос — a.css (2 секунды). Третий — proxy.css. Четвертый — b.css (2 секунды). И вот что удивительно, IE не хочет начинать загрузку b.css, пока файл a.css не будет загружен полностью. Во всех других браузерах такого сюрприза не происходит, что приводит к более быстрой загрузке страницы (см. рис. 5.5).

(рис 5.4) Результаты в IE. Источник: getincss.ru(рис 5.5) Результаты в других браузерах. Источник: getincss.ru

5.2.6. Много ©import

Применение сразу нескольких правил @import в IE приводит к тому, что файлы загружаются не в том порядке, в котором они указаны в коде. В этом примере используется 6 файлов стилей (каждый из которых загружается по 2 секунды), за которыми следует JS-скрипт (4 секунды для загрузки).

<style type='text/css'>
@import url('a.css'); 
@import url('b.css'); 
@import url('c.css'); 
@import url('d.css'); 
@import url('e.css'); 
@import url('f.css'); 
</style>
(рис 5.6) Много @import. Источник: getincss.ru

На рис. 5.6 мы увидим, что самый долгий по загрузке — это скрипт. Несмотря на то, что он указан после стилей, в IE он загружается первым. Если в скрипте содержится код, который зависит от применяемых стилей ( getElementsByClassName, и т. п.), это может привести к ошибкам работы скрипта, так как он загружается раньше, чем стили.

5.2.7. <link> <link>

Проще и безопасней задействовать <link> для загрузки стилей:

<link rel='stylesheet' type='text/css' href='a.css'>
<link rel='stylesheet' type='text/css' href='b.css'>

Использование <link> обеспечивает параллельную загрузку файлов во всех браузерах (см. рис. 5.7). Применение <link> также гарантирует, что файлы будут загружены именно в том порядке, который указан в коде документа.

(рис 5.7) Использование <link>. Источник: getincss.ru

Для нас особенно плохо то, что ресурсы могут быть загружены в другом порядке, нежели указано в документе. Все браузеры должны заглядывать вперед перед загрузкой стилей для извлечения правил @import и начинать их загрузку немедленно. До тех пор, пока браузеры не реализуют это, стоит избегать использования @import и загружать стили только с помощью <link>.

5.2.8. Упрощаем CSS-селекторы

Для большинства сайтов возможный выигрыш в производительности после оптимизации CSS-селекторов будет крайне незначительным и не будет стоить потраченного времени. Есть несколько типов CSS-правил (например, expression для IE) и взаимодействий с JavaScript, которые могут существенно замедлить страницу. Именно на них и нужно концен-тировать усилия.

Большая часть информации о быстродействии CSS-селекторов может быть получена из статьи David Hyatt Пишем эффективный CSS для интерфейсов в MoziLLa (https://deveLoper.moziLLa.org/en/Writing Efficient CSS). Стоит привести оттуда одну цитату:

"Система стилей находит элемент, соответствующий CSS-правилу, начиная с правого конца селектора и двигаясь налево. Пока мы еще находим элементы, соответствующие селектору, мы продолжаем двигаться. Остановка возможна только в случае нахождения элемента, соответствующего всему CSS-селектору, или невозможности такой элемент найти для какой-то части селектора".

Не очень понятно, как устроен движок CSS-селекторов в других браузерах, например, в IE (а если принять во внимание тесты CSS-производи-тельности из книги "Разгони свой сайт", то возникает предположение, что в Opera движок разбора CSS-селекторов работает как раз слева направо). Может быть, он использует комбинированный подход: как справа налево, так и слева направо. Переключение может происходить по какому-то признаку (например, по наличию #id).

Однако по заявлению Виталия Харисова, руководителя группы HTML-верстки в Яндексе, тесты показывают (http://clubs.ya.ru/yacf/ replies.xml?item no=338ncrnd=3604), что все браузеры применяют селекторы одинаково — справа налево.

Благодаря вышесказанному мы можем теперь сфокусировать оптимизационные усилия на тех CSS-селекторах, правая часть которых будет соответствовать большому числу элементов на странице. Давайте рассмотрим для примера DIV DIV DIV P A.class0007 {}. У этого селектора присутствует 5 уровней вложенности потомков, для которых необходимо найти соответствие в DOM-дереве. Это выглядит очень ресурсоемко, однако при взгляде на самую правую часть этого селектора, A.class0007, мы прекрасно понимаем, что ей соответствует только 1 элемент на странице, поэтому браузеру очень легко установить точное соответствие.

Ключевым моментом в оптимизации CSS-селекторов является самая правая часть, часто также называемая "ключевым селектором". Вот пример гораздо более ресурсоемкого селектора: A.class0007 * {}. Хотя он может и выглядеть просто, но для браузера очень тяжело его вычислить. Поскольку браузер будет двигаться справа налево, он начнет со всех элементов, которые подходят под ключевой селектор (*). Это означает, что браузер попытается проверить все элементы на странице и установить, нужно ли к ним применить этот стиль. На следующей диаграмме показана разница во времени между тестами для универсального селектора и прошлыми тестами для наследственных селекторов.

(рис 5.8) Разница во времени загрузки (в мс) для универсального селектора. Источник: stevesouders.com

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

A.class0007 DIV {} #id0007 > A {} .class0007 [href] {} DIV:first-child {}

Не все CSS-селекторы существенно влияют на производительность, даже те, которые могут выглядеть сложно. При оптимизации стоит фокусироваться на устранении CSS-правил с ключевым селектором, соответствующим большому числу элементов. Это становится особенно важно для Веб2.0-приложений с большим числом узлов DOM-дерева, огромным количеством CSS-правил и отрисовок страниц.

Некоторой проблемой является то, что почти все известные JavaScript-библиотеки разбирают селекторы слева направо. Таким образом, мы должны писать два вида селекторов: оптимизированных под браузеры и оптимизированных под JavaScript-библитеки. На данный момент только немногие поддерживают нотацию справа налево, например, CSS1-ветка YASS (http://yass.webo.in/).

5.3. Пишем эффективный CSS

В книге "Разгони свой сайт" тема производительности CSS-селек-торов уже поднималась. Несмотря на то, что выводы были подкреплены значительным объемом исследований, основной вопрос — как же должна выглядеть эффективная таблица стилей, которая обеспечивает наискорейшее отображение документа на экране, — так и остался без ответа.

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

Также известно, что селекторы обладают различной сложностью (например, селектор . class1 .class2, очевидно, должен отрабатывать медленнее, чем просто . class2).

5.3.1. Модель

Для уточнения существенных факторов и относительного ранжирования известных правил по написанию эффективного CSS-кода была взята за основу следующая формула:

Время отрисовки = Размер DOM-дерева * Число CSS-селекторов * Сложность стилевых правил * 
*Время отрисовки одного правила + Время создания документа

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

5.3.2. Уточнения по ходу

Также практически сразу становится ясно, что в формуле фигурирует не размер всего DOM-дерева, а число элементов, на которые влияет данный селектор (это, в частности, объясняет, почему универсальный селектор (*) такой ресурсоемкий). Для уточнения этого момента была проведена серия тестов с одинаковым DOM-деревом и различными CSS-правилами (одно применялось ко всему дереву, а другое — только к десятой его части).

Еще не стоит забывать о наличии у браузеров собственной таблицы стилей, которая применяется к каждой странице, выводящейся на экран. Размер этой таблицы можно выяснить достаточно просто: нужно всего лишь открыть две страницы с разным (и достаточно большим) числом CSS-правил, но одинаковым DOM-деревом и проверить, насколько замедлилась загрузка. Зная отношение размера двух таблиц стилей, можно вычислить неизвестный размер таблицы стилей самого браузера (для основных браузеров он составил в районе 30-50 правил, для IE — порядка 200).

И еще один момент, который всплыл по ходу расследования: имеет значение размер полного DOM-дерева, не только число тегов, но и число текстовых узлов, хотя это никак и не влияет на основные выводы.

5.3.3. Результаты

В ходе проведения различных тестов модель удалось уточнить и показать, что время создания документа зависит от числа узлов в дереве (что вполне очевидно). Это можно проверить двумя наборами тестов: на увеличении числа CSS-селекторов без увеличения DOM-дерева и на увеличении DOM-дерева без увеличения числа CSS-селекторов. При этом хорошо видно, что время создания документа не является постоянным и несколько увеличивается при увеличении размера документа.

Также было проверено, насколько составные селекторы отрабатывают медленнее своих элементарных собратьев (имеется в виду разница между . classl, . classl .class2 и . classl .class2 . class3). Разница была зафиксирована, но оказалась несущественной (каждое звено прибавляет примерно 10-20% к общей сложности селектора).

Итак, после всех уточнений, формула приобрела следующий вид:

T = (сумма(DOM1 * K) + DOM2 * In) * t + DOM2 * L

Здесь:

  • T — время отображения документа на экране;
  • DOM1 — число элементов, на которое может повлиять данное CSS-правило (разбор CSS-правил в браузерах идет справа налево);
  • DOM2 — размер всего DOM-дерева;
  • K — сложность каждого отдельного CSS-правила в таблице стилей, от 1 до 1,5;
  • In — число встроенных CSS-правил в браузере, порядка 40-200;
  • t — характерное время обработки одного правила для одного узла дерева, находится в районе 0,0001...0,0005 мс;
  • L — характерные издержки на создание одного элемента DOM-дерева, находятся в районе 0,0005...0,005 мс.
  • Данная модель позволила аппроксимировать время отображения страницы с точностью 10% (в редких случаях 20%, — видимо, есть еще много неучтенных факторов, например, особенности выделения памяти различными браузерами). Тестирование проводилось на документах от 5000 DOM-узлов и от 0 CSS-правил.

    5.3.4. Выводы

    Анализ предложенной и проверенной модели позволяет сделать огромное количество весьма интересных выводов. Давайте остановимся на некоторых из них.

  • Размер DOM-дерева играет основную роль. Просто наиглавнейшую. Поэтому совет на все времена: уменьшайте DOM всеми возможными способами. Уменьшение его (как хорошо видно из итоговой формулы) на 20% приведет к пропорциональному ускорению отображения страницы.
  • Стоит также учесть, что в формуле фигурирует не только общий размер дерева, но и число элементов, которые обрабатываются при применении CSS-селектора. Именно по этой причине неэффективно использовать универсальный (*) селектор и теги: они охватывают существенное количество элементов.
  • В качестве альтернативы применения общих тегов и универсальных селекторов можно назвать два выхода.
  • Использовать уникальные теги для уникальных элементов на странице (например, для скругленных уголков использовать редкие теги — ins, del, q, u, b, i).
  • Использовать уникальные классы для каждого набора стилевых правил.
  • Если первый подход может быть применим для небольших сайтов (например, для уменьшения размера HTML-кода), то в случае средних и крупных проектов однозначно стоит применять второй подход (его, кстати, вовсю рекомендует и Виталий Харисов в своем своде правил для эффективного CSS и фреймворке Monkey Joe, http://clubs.ya.ru/yacf/).
  • Использование сложных правил (с несколькими звеньями селекторов) может быть оправдано (это не влечет значительных издержек), однако если применять везде уникальные классы, то наследование обычно пропадает само собой.
  • В качестве глобального сброса стилевых правил ("ластик") можно рекомендовать сбрасывать правила только у тех элементов, которые отображаются. Например, если на странице 90% DOM-дерева — это div, для которых не нужны никакие правила по умолчанию, то переход от глобального "ластика" к локальному или вообще его устранение за счет индивидуальных правил способно несколько увеличить производительность).
  • Оптимизировать число CSS-правил стоит, если их больше 100-200 (ибо в противном случае правила самого браузера будут перекрывать все ваши усилия по увеличению эффективности).
  • Также стоит отметить, что по результатам тестирования Виталия Харисова (http://cLubs.ya.ru/yacf/repLies.xmL7item no=338) неиспользуемые CSS-правила добавляют некоторую задержку в отображении страницы (до 10% от времени отрисовки), поэтому их тоже стоит избегать.

    Для средней HTML-страницы время ее отображения (размер DOM-дерева — 1000 элементов, CSS-правил — порядка 500, каждое из них в среднем применяется к 40% элементов) составит порядка 100 мс. Простой оптимизацией можно уменьшить этот показатель вдвое (например, сузив область воздействия самих селекторов, если DOM-дерево уменьшить не получается).

    5.4. Стыкуем асинхронные скрипты

    Этот раздел написан под впечатлением от статьи Steve Souders (автора знаменитых советов Yahoo! касательно клиентской производительности) "CoupLing Async Scripts" (http://www.stevesouders.com/bLog/ 2008/12/27/coupLing-async-scripts/). Steve проанализировал поведение JavaScript-файлов при загрузке и предложил несколько путей для обхода их "блокирующих" свойств.

    Если скрипты загружаются в обычном порядке ( <script src="..."> ), то они блокируют загрузку всех остальных компонентов страницы (в последних версиях Firefox и в Safari это не так, но речь идет в основном про 70% пользователей с IE) и блокируют отрисовку всей той части страницы, которая расположена ниже вызова скриптов по HTML-коду. Асинхронная загрузка скриптов (например, при помощи динамического создания объектов после срабатывания комбинированного события window.onload) предотвращает такое поведение браузера, что ускоряет загрузку страницы.

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

    Существует несколько стандартных путей для стыковки асинхронно загружаемых скриптов с другим JavaScript-кодом.

  • window onload. Выполнение внутреннего JavaScript-кода может быть привязано к событию window onload. Это очень просто в использовании, но часть скриптов может быть выполнена раньше.
  • onreadystatechange у скрипта. Внутренний код может быть привязан к событию onreadystatechange и(или) onload (необходимо будет использовать оба варианта, чтобы покрыть все популярные браузеры). В этом случае кода будет больше, он будет более сложным, но будет гарантия, что он исполнится сразу после загрузки соответствующих внешних файлов.
  • Встроенные вызовы. Внешние скрипты могут быть модифицированы таким образом, чтобы включать в самом конце вызов небольшого участка кода, который вызовет соответствующую функцию из внутреннего кода. Это все замечательно, если внешние и внутренние скрипты разрабатываются одной и той же командой. Но в случае использования сторонних разработок это не обеспечит всей необходимой гибкости для связки внешних скриптов с внутренним кодом.
  • В этом разделе параллельно освещаются два вопроса: как асинхронные скрипты ускоряют загрузку страницы и как можно состыковать асинхронные скрипты и внутренние, используя модифицированный вариант загрузчика от Джона Ресига (автора jQuery) — шаблон двойного тега <script>.

    5.4.1. Обычные вызовы скриптов

    Если добавить скрипт на страницу обычным способом (через <script src="..."> ), диаграмма загрузки будет примерно следующей.

    Хотя скрипт и функционирует, но это не сделает нас намного счастливее, ибо загрузка страницы замедлится. На рис. 5.9 хорошо видно, как скрипт (по имени sorttable-async.js ) блокирует все остальные HTTP-запросы на странице (в частности, arrow-right-20x9.gif ), что замедляет загрузку страницы. Все диаграммы загрузки сняты при помощи Firebug 1.3 beta. В этой версии Firebug красной линией отображается событие onload (а синяя линия соответствует событию domcontentloaded ). Для версии с обычным вызовом скрипта событие onload срабатывает на 487-й миллисекунде.

    (рис 5.9) Диаграмма загрузки скриптов в обычном случае.Источник: stevesouders.com

    5.4.2. Асинхронная загрузка скриптов

    Скрипт sorttable-async.js в данном случае не является необходимым для первоначального отображения страницы. Такая ситуация (внешние скрипты, которые не используются для первоначального отображения страницы) является кандидатом номер 1 для внедрения асинхронной загрузки. Вариант с асинхронной загрузкой скриптов подключает этот скрипт, используя DOM-методы для создания нового тега <script>:

    var script = document.createElement("script"); script.src = "sorttable-async.js";
    script.text = "sorttable.init()"; // это объясняется чуть ниже document.getElementsByTagName("head")[0].appendChild(script);

    Диаграмма HTTP-загрузки для асинхронной загрузки скриптов изображена на рис. 5.10. Стоит обратить внимание, как асинхронный подход предотвращает блокирующее поведение: sorttable-async.js и arrow-right-20x9.gif загружаются параллельно. Это снижает общее время загрузки дл 429 мс.

    (рис 5.10) Диаграмма загрузки скриптов в асинхронном случае, источник:stevesouders.com

    5.4.3. Шаблон двойного скрипта от Джона Ресига

    Асинхронная загрузка скриптов позволяет ускорить загрузку страницы, но в этом случае остается, что улучшить. По умолчанию скрипт вызывает "сам себя" при помощи прикрепления sorttable.init() к обработчику события onload для этого скрипта. Некоторое улучшение производительности (и уменьшение кода) может быть достигнуто, если вызвать sorttable.init() внутри тега <script>, чтобы вызвать его сразу же после загрузки внешнего скрипта (подключенного через src).

    Выше описано три способа по стыковке внутреннего кода с асинхронной загрузкой внешних скриптов: window onload, onreadystatechange у скрипта и встроенный в скрипт обработчик. Вместо всего этого можно использовать технику от Джона Ресига — шаблон двойного тега <script>. John описывает, как связать внутренние скрипты с загрузкой внешнего файла, следующим образом:

    <script src="jquery.js" type="text/javascript">
    jQuery("p").addClass("pretty"); </script>

    В этом случае код внутри <script> срабатывает только после того, как загрузка и инициализация внешнего скрипта завершилась. У этого подхода по стыковке скриптов есть несколько очевидных преимуществ:

  • проще: один тег <script> вместо двух;
  • прозрачнее: связь внутреннего и внешнего кодов более очевидна;
  • безопаснее: если внешний скрипт не загрузится, внутренний код не будет выполнен, что предотвратит появление ошибок, связанных с неопределенными переменными.
  • Это замечательный шаблон для асинхронной загрузки внешних скриптов. Однако чтобы его использовать, нам придется внести изменения как во внутренний код, так и во внешний файл. Для внутреннего кода придется добавить уже упомянутую выше третью строку, которая выставляет свойство script.text. Для завершения процесса стыковки нужно добавить в конец sorttable-async.js:

    var scripts = document.getElementsByTagName("script"); var cntr = scripts.length; while ( cntr ) {
    var curScript = scripts[cntr-1];
    if ( -1 != curScript.src.indexOf("sorttable-async.js") ) { eval( curScript.innerHTML ); break;
    }
    cntr—;
    }

    Этот код проходится по всем скриптам на странице, находит необходимый блок, который должен загрузить сам себя (в этом случае это скрипт с src, содержащим sorttable-async.js ). Затем он выполняет код, который добавлен к скрипту (в этом случае —sorttable.init()), и таким образом вызывает сам себя. (Небольшое замечание: хотя призагрузке скрипта текст в нем был добавлен при помощи свойства text, обращение к нему происходит при помощи свойства innerHTML. Это необходимо для обеспечения кроссбраузерности.) При помощи такой оптимизации мы можем загрузить внешний файл скрипта, не блокируя загрузку других ресурсов, и максимально быстро выполнить привязанный к данному скрипту внутренний код.

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

    var _on_ready_execution = setInterval(function() { if (typeof urchinTracker === 'function') { urchinTracker();
    clearInterval(_on_ready_execution);
    }
    }, 10);

    Этот подход был уже описан в книге "Разгони свой сайт" (http://speedupyourwebsite.ru/books/speed-up-your-website/), однако он предполагает дополнительную нагрузку на процессор для постоянной проверки готовности искомого скрипта и не срабатывает в случае недоступности внешнего файла: проверка продолжает выполняться.

    Однако в случае проверки по интервалу нам совсем не нужно модифицировать внешний файл, в случае же двойного использования тега <script> это просто необходимо. Проверку по интервалу можно улучшить, если по истечении некоторого времени (5—10 секунд, например) перезапускать загрузку внешнего файла (меняя исходный тег <script> при помощи уникального GET-параметра), а после нескольких неудачных перезапусков вообще прекращать загрузку (возможно, с каким-то сообщением об ошибке).

    5.4.4. "Ленивая" загрузка

    Общее время загрузки может быть уменьшено еще больше, если использовать "ленивую загрузку" скрипта (загружать его динамически как часть обработчика события onload ). Мы можем просто оборачивать заявленный выше код в обработчик события onload:

    window.onload = function() {
    var script = document.createElement("script"); script.src = "sorttable-async.js"; script.text = "sorttable.init()";
    document.getElementsByTagName("head")[0].appendChild(script);
    }

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

    (рис 5.11) Диаграмма загрузки в случае "ленивой" загрузки скриптов, источник stevesouders.com

    5.4.5. Заключение

    Асинхронная и "ленивая" загрузка скриптов уменьшает общее время загрузки страницы тем, что предотвращает обычное блокирующее поведение скриптов (буквально "клин клином вышибает"). В качестве демонстрации этого можно привести различные варианты добавления скрипта на тестовую страницу:

  • обычная загрузка скриптов — 487 мс;
  • асинхронная загрузка — 429 мс;
  • "ленивая" загрузка 320 мс.
  • Выше показано время, после которого срабатывает событие onload. Для других веб-приложений применение асинхронной загрузки для улучше ния производительности может привести к гораздо более впечатляющим результатам и быть намного более приоритетным. В нашем случае асинхронная загрузка скриптов немного лучше (~400 мс против 417 мс). В обоих случаях нам нужно каким-то образом связывать внутренние скрипты с внешними.

    5.5. Стыкуем компоненты в JavaScript

    После вышеописанного материала можно задуматься и о модульной загрузке какого-либо сложного JavaScript-приложения. Предложенный подход в таком случае будет довольно громоздким: нам нужно будет в конец каждого модуля вставлять загрузчик следующих модулей. А если нам на разных страницах требуются различные наборы модулей и разная логика их загрузки? Тупик?

    Ан нет. Не зря упоминается в самом начале прошлого раздела о событии onload / onreadystatechange для скриптов. Используя их, мы можем однозначно привязать некоторый код к окончанию загрузки конкретного модуля. Дело за малым: нам нужно определить этот самый код каким-либо образом.

    5.5.1. Решение первое: дерево загрузки

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

    var modules = [
    [0,  'iteml', function(){
    alert('item1 is loaded'); }],
    [1,  'item2', function(){
    alert('item2 is loaded'); }],
    [1,  'item3', function(){ alert('item3 is loaded');
    ];

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

    Давайте рассмотрим, каким образом можно использовать данную структуру.

    /* перебор и загрузка модулей */ function load_by_parent (i) { i = i || 0;
    var len = modules.length, module;
    /* перебираем дерево модулей */ while (len—) {
    module = modules[len]; /* и загружаем требуемые элементы */ if (!module[0]) { loader(len);
    }
    }
    }
    /* объявляем функцию-загрузчик */ function loader (i) {
    var module = modules[i]; /* создаем новый элемент script */
    var script = document.createElement('script'); script.type = 'text/javascript'; /* задаем имя файла */
    script.src = module[1] + '.js'; /* задаем текст внутри тега для запуска по загрузке */
    script.text = module[2]; /* запоминаем текущий индекс модуля */
    script.title = i + 1; /* выставляем обработчик загрузки для IE *
    / script.onreadystatechange = function() { if (this.readyState === 'loaded') 
    { /* перебираем модули и ищем те, которые нужно загрузить */ load_by_parent(this.title);
    }
    };
    /* выставляем обработчик загрузки для остальных */
    script.onload = function (e) { /* исполняем текст внутри тега (нужно только для Opera) 
    */ if (/opera/i.test(navigator.userAgent)) { eval(e.target.innerHTML);
    }
    /* перебираем модули и ищем те, которые нужно загрузить */ load_by_parent(this.title);
    };
    /* прикрепляем тег к документу */
    document.getElementsByTagName('head')[0].appendChild(script);
    }
    /* загружаем корневые элементы */ load_by_parent();

    Мы можем вынести загрузку корневых элементов в событие загрузки страницы, а сами функции — в какую-либо библиотеку, либо объявить прямо на странице. Задавая на каждой странице свое дерево, мы получаем полную гибкость в асинхронной загрузке любого количества JavaScript-модулей. Стоит отметить, что зависимости в таком случае разрешаются "от корня — к вершинам": мы сами должны знать, какие базовые компоненты загрузить сначала, а потом загрузить более продвинутые.

    5.5.2. Решение второе: загрузка через DOM-дерево

    Кроме того, подобной проблемой уже занимался Андрей Сумин и даже предложил свое решение в виде библиотеки JSX (http://jsx.ru/), которая позволяет назначать список зависимостей через DOM-дерево. Для этого у элементов, которые требуют загрузки каких-либо модулей для взаимодействия с пользователем, назначается класс с префиксом jsx-compo-nent, а далее идет уже список компонентов. Сама библиотека обходит DOM-дерево, находит все модули, которые нужно загрузить, и последовательно их загружает. Просто замечательно.

    Но что, если нам требуется поменять обработчик по загрузке этого модуля? Как его задавать? Сама JSX использует атрибуты искомых узлов DOM-дерева сугубо для определения параметров этих модулей. Это достаточно удобно: ведь таким образом можно назначить и инициализатор модуля.

    Также библиотека позволяет отслеживать повторную загрузку модулей, осуществлять догрузку модулей в случае плохого соединения и даже объединять разные модули в один исходный файл через систему алиасов. Таким образом, проблема асинхронной загрузки произвольного дерева модулей оказывается решенной. В случае JSX задача разрешается в обратном порядке: мы указываем основной файл (вершину дерева зависимостей), а он уже загружает все необходимые ему модули либо проверяет, что модули загружены. Это все?

    5.5.3. Решение третье: JSX+7YASS

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

    Для примера можно рассмотреть следующий участок HTML-кода:

    <div id="item1" class="yass-module-utils-base-dom">
    <span id="item2" class="yass-module-dom"
    title="_('#item2')[0].innerHTML = 'component is loading...';"> </span> </div>

    Давайте разберемся, какую логику загрузки он обеспечивает.

  • YASS при инициализации обходит DOM-дерево документа и выбирает все узлы с классом yass-module-*.
  • После этого формируется два потока загрузки модулей: для dom-base-utils и для dom.Причем в последнем случае загрузки фактически не будет: загрузчик дождется, пока состояние компонента dom будет выставлено в loaded, и только потом запустит (черeз eval ) код, записанный в title этого элемента (в данном случае это span ).
  • Первый поток загрузки асинхронно вызовет три файла с сервера: yass.dom.js, yass.base.js и yass.utils.js. По загрузке всех этих модулей (ибо они вызваны в цепочке зависимостей, и в данном случае dom зависит от base, который зависит от utils ) будут вызваны соответствующие инициализационные функции (если они определены). Таким образом, возможны два типа обработчиков: непосредственно по загрузке компонента (будет вызвано для всех компонентов в цепочке) и после загрузки всей заданной цепочки компонентов (в нашем случае это dom-base-utils ).
  • Если мы хотим каким-то образом расширить нашу цепочку, то можем в конце каждого из указанных файлов прописать загрузку какой-либо другой цепочки (например, base-callbacks ), которая "заморозит" загрузку модуля base до получения callbacks. Сделать это можно (имея в виду, что расширяем зависимости модуля base) следующим образом:
    _.load('callbacks-base')
    ;
  • Предыдущий шаг может быть также выполнен при помощи самого DOM-дерева: нам нужно будет прописать для произвольного элемента класс yass-module-callbacks-base. Это добавит в дерево зависимостей искомую цепочку.
  • Для большей ясности описанное выше конечное дерево загружаемых модулей можно представить так:

    dom-> base -> utils -> callbacks

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

    Естественно, весь указанный функционал уже добавлен в последнюю версию YASS (http://yass.webo.in/).

    5.6. Что такое CDN и с чем его едят

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

    CDN (англ. Content Delivery Network) — распределенная сеть хранения данных. Предназначена как для обеспечения отказоустойчивости, так и для максимально быстрого времени ответа при запросе файлов. Информация данного раздела подготовлена при помощи специалистов первой CDN в России — NGENIX.

    После общения с множеством специалистов возникла следующая проблема: все знают, что CDN — это очень хорошо, это правильно, это стоит использовать. Данный раздел пытается ответить на этот вопрос и объяснить, в каких случаях CDN может решить возникающие проблемы и ряд технологических и бизнес-задач.

    (рис 5.12) Схема CDN, источник: ngenix.net

    5.6.1. Доступность контента

    Нередко бывает, что каждый час простоя сайта интернет-магазина измеряется в весьма значительных суммах. Если отказ сервера, на котором расположен сайт, произошел ночью (по времени технической поддержки), а магазин должен быть доступен 24 часа в сутки (например, в связи с широким географическим покрытием пользователей), то это может вылиться в значительные финансовые потери.

    Распределенная сеть серверов позволяет легко решить эту проблему: если отказывает один из имеющихся серверов, то его место тут же занимает самый близкий из сетевых соседей. Таким образом, конечный пользователь не замечает каких-либо перебоев в работе сайта.

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

    5.6.2. Высокая скорость загрузки

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

    В качестве примера стоит привести следующие цифры: время ответа обычного сервера, расположенного на VPS (даже не на shared-хостинге), составляет 50-200 мс (в зависимости от различных условий, в том числе от загруженности канала). Для CDN это число очень редко превышает 10 мс.

    5.6.3. Снижение нагрузки на сервер — источник информации

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

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

    5.6.4. Размещение "тяжелого" контента

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

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

    5.6.5. Отказоустойчивость и безопасность

    Сеть CDN размещается на мощнейших технологических площадках и насчитывает сотни и тысячи серверов, что увеличивает стоимость эффективной DDoS-атаки на нее до фантастических величин. Если ваш бизнес не приносит миллионы долларов в день, то вы можете спокойно разместить свой сайт в такой сети и будете защищены от нападок недобросовестных конкурентов.

    5.6.6. Масштабируемость и эластичность по нагрузке

    Сеть распределенных серверов обладает еще одним преимуществом: она позволяет легко наращивать мощности, от обслуживания 1000 человек день до нескольких миллионов и более. Таким образом, резкое увеличение нагрузки (выход нового продукта, статья в известном издании, промо-кампания) никак не скажется на доступности вашего сайта, его содержимое будет отдаваться по-прежнему быстро и без каких-либо перебоев.

    5.6.7. CDN в России

    Компания NGENIX первой в России начала предоставлять услуги распределенной доставки и дистрибуции цифрового контента с применением технологии CDN на базе собственной мультисервисной сети доставки контента NGENIX CDN с точками присутствия в крупнейших телекоммуникационных центрах страны и ближнего зарубежья.

    Сегодня решения и сервисы NGENIX помогают медиа-компаниям и контент-провайдерам распространять в Интернете тяжелый мультимедийный контент на высокой скорости с широким охватом российской Интернет-аудитории. Сеть NGENIX CDN является универсальной платформой для доставки статического и динамического контентов, ускорения загрузки Интернет-сайтов, повышения производительности веб-приложений, вещания видео, цифровой дистрибуции программного обеспечения, музыки, игр и т. п.

    Уникальная распределенная инфраструктура NGENIX CDN позволяет решать широкий круг сетевых и бизнес-задач — от базового ускорения загрузки сайта до реализации сложных инженерно-технических решений, учитывающих IT-архитектуру, бизнес-модель, размер и профиль целевой Интернет-аудитории заказчика.

    Практическое использование CDN на примере Google Apps

    Итак, вы готовы использовать Google для хостинга своих файлов? Тогда вперед!

    5.7.1. Все по порядку

  • Для загрузки файлов на CDN нам будет нужен Python. Ну, просто потому что на нем работает сам Google Apps. Для корректной работы рекомендуют версию 2.5 (на 3.0 Google SDK может не запуститься, на 2.5 работает исправно). Загружаем Python отсюда: http://www.python.org/download/, устанавливаем, запоминаем директорию установки (она нам пригодится в дальнейшем).
  • Загружаем последнюю версию Google Apps SDK (http://code.google.com/appengine/downloads.html). Устанавливаем ее (так как мы выполнили уже п. 1 и поставили Python, то проблем у нас не возникнет). Если в ходе установки выбираем нестандартную директорию, то опять-таки запоминаем к ней путь.
  • Регистрируемся на appengine.google.com (для этого понадобится аккаунт Google). Если в Google Apps Engine аккаунт уже был, то пропускаем этот пункт.
  • После регистрации заходим и создаем свое приложение. Нужно выбрать уникальный URL (поддомен appspot.com) и название: Дополнительно нужно будет подтвердить аккаунт через SMS, но ведь мы собираемся там просто CDN развернуть, а не спамить, правда?(рис 5.13) Загружаем Google App Engine SDK(рис 5.14) Создание нового приложения в Google App Engine
  • Теперь (или параллельно ожиданию подтверждения от GoogLe) готовим рабочую директорию с файлами у себя на машине (ведь мы для этого устанавливали сначала Pyt hon, а потом SDK). Называем ее произвольным образом, в корне создаем файл app.yaml, в который записываем:
    application: ваш_идентификатор_приложения 
    version: 1 
    runtimee: 
    python api_version: 1
    
    handlers:
    -	url: /favicon.ico static_files: favicon.ico upload: favicon.ico
    -	url: /.* script: cacheheaders.py
    В случае нашего примера идентификатор был просто webo, он соответствует адресу webo.appspot.com, а version соответствует версии приложения. Очень удобно отлаживать новую версию, в то время как более старая замечательно работает. Переключение между версиями происходит из панели управления GoogLe Apps (http://appengine.googLe.com/ depLoyment).
  • Сюда же, в директорию, закидываем файл favicon.ic o от рабочего сайта и создаем еще один файл, cacheheaders.py: (сразу стоит отметить, что в качестве отбивки во всех Python-скриптах используется не табуляция, а двойной пробел):
    import wsgiref.handlers from google.appengine.ext import webapp
    
    class MainPage(webapp.RequestHandler):
    
    def output_file(self, path, lastmod): import datetime try:
    self.response.headers['Last-Modified'] = lastmod.strftime("%a, %d %b %Y %H:%M:%S GMT") 
      expires=lastmod+datetime.timedelta(days=365)
    self.response.headers['Expires'] = expires.strftime("%
    a, %d %b %Y %H:%M:%S GMT")
    self.response.headers['Cache-Control'] = 'public,
    max-age=31536000'
    fh = open(path, 'r')
    self.response.out.write(fh.read())
    fh.close
    return
    except IOError:
    self.error(404)
    return
    def get(self, dir, file, extension):
    if (dir != 'i' and extension != 'jpg' and extension !=
    'png' and extension != 'gif'):
    self.error(404)
    return
    if extension == "jpg":
    self.response.headers['Content-Type'] =
    "image/jpeg"
    elif extension == "gif":
    self.response.headers['Content-Type'] =
    "image/gif"
    elif extension == "png":
    self.response.headers['Content-Type'] =
    "image/png"
    try:
    import os
    import datetime
    path = dir+"/"+file+"."+extension
    info = os.stat(path)
    lastmod = datetime.datetime.fromtimestamp(info[8])
    if self.request.headers.has_key('If-Modified-Since'):
    dt = self.request.headers.get('If-Modified-
    Since').split(';')[0]
    modsince = datetime.datetime.strptime(dt,
    "%a, %d %b %Y %H:%M:%S %Z")
    if modsince >= lastmod:
    # Файл более старый, чем закэшированная копия
    (или такой же)
    self.error(304)
    return
    else:
    # Файл более новый
    self.output_file(path, lastmod)
    else:
    self.output_file(path, lastmod)
    except:
    self.error(404)
    return
    def main():
    application =
    webapp.WSGIApplication([(r'/(.*)/([^.]*).(.*)',
    MainPage)], debug=False)
    wsgiref.handlers.CGIHandler().run(application)
    if __name__ == "__main__":
    main()
    По поводу этого файла — небольшое лирическое отступление. Как выяснилось в ходе исследования, Google Apps по умолчанию не поддерживает Last-Modified / ETag (только Expires, который настраивается простой строкой в app.yaml — defaultexpiration: "365d" ). Чтобы обеспечить поддержку этого необходимого для CDN функционала (для 304-ответов), мы и заводим обработчик cacheheaders.py.

    Конечно, можно обойтись простым кэшированием, но мы же хотим максимально правильный CDN? Сам файл cacheheaders.py просто проверяет, что запрос пришел к папке i для нашего приложения и расширение у файла . png, . gif или .jpg, после этого он отдает либо сам файл, либо 304-ответ (сравнивая заголовок браузера If-Modified-Since с меткой времени файла).

  • Теперь настроим скрипт для загрузки файлов из нашей директории на Google. Для этого нужно завести в нашей папочке (или еще где-нибудь, это уже не важно) файл upload.bat. (если вы собираетесь загружать файлы из-под другой операционной системы, нежели Windows, то логику файла придется переписать на соответствующем скриптовом языке). В файле записываем:
    "путь_к_установленному_Python_из_пункта_1" 
    "C:\ProgramFiles\Google\google_appengine\appcfg.py" update 
    "путь_к_рабочей_папочке_с_файлами"

    Если в пункте 2 вы выбрали нестандартную директорию для Google App Engine SDK, то ее придется подставить вместо C:\Program Files\Google\google_appengine.

  • Создаем папку i в рабочей директории, в которую можно загрузить все файлы, которые предполагается отдавать с CDN. В имени файла должна отсутствовать точка (иначе cacheheaders.py будет некорректно обрабатывать расширение для файла — и его придется подправить).
  • Запускаем наш upload.bat, вводим логин/пароль от Google App Engine (только в первый раз) и радуемся процессу загрузки файлов на CDN.
  • И вот сейчас уже любой файл по адресу вашидентифика-тор.appspot.com/i/ будет отдаваться через сеть серверов Google по всему миру (например, http://webo.appspot.com/i/b.png ). Радуемся!
  • 5.7.2. Подводим итоги

    Если ваш проект не создает большой статической нагрузки (оценочно не более 250—500 Кб/с), то вы с легкостью можете воспользоваться серверами Google для выдачи своих файлов.

    Отмеченные минусы:

  • по умолчанию доступно только большое время кэша, настройка Last-Modified требует дополнительной логики и нагрузки на процессор (может стать критичной при большом количестве мелких файлов);
  • Google CDN не позволяет изменять заголовок Content-Encoding. При настройке архивирования придется положиться на логику серверов Google;
  • процесс обновления сайта может стать достаточно трудоемким, если его не автоматизировать (но автоматизируется он довольно просто). Также в бесплатной версии присутствует ограничение на число ежедневных обновлений файловой системы.
  • Во всем остальном — это идеальный выбор. Например, webo.in уже использует эту CDN для выдачи всех фоновых изображений (они обслуживаются с адреса webo.appspot.com/i/).

    Также стоит отметить, что существует возможность полностью прикрепить домен к Google App Engine и применять для обслуживания его содержания какое-либо приложение App Engine. Это позволит (в случае полностью статического сайта) загружать его максимально быстро, совершенно бесплатно используя мощности Google (в разумных пределах для среднего сайта это порядка 20 тысяч посетителей в день).

    Вернуться к учебному плану