Ниже перевод статьи "Optimizing Page Load Time", в которой автор математически рассчитывает оптимальный размер файлов для эффективной передачи при веб-запросах, рассматривает некоторые прикладные вопросы оптимизации загрузки страницы с учетом особенностей браузеров, а также дает несколько развернутых и ценных советов. Мои комментарии далее курсивом.
Существует распространенное мнение, что быстро загружающая страница положительно влияет на впечатление пользователя (improve the user experience). В последние годы многие сайты начали использовать для этой цели технологию AJAX, чтобы уменьшить время ожидания (при загрузке данных). Вместо того, что запрашивать с сервера новую страницу полностью при каждом клике, браузер часто можно либо поменять вид самой страницы (отобразив или скрыв какие-либо блоки), либо подгрузить небольшую порцию HTML-, XML- или JavaScript-кода и внести изменения на существующую страницу. В любом случае, это значительно уменьшает время, проходящее между кликом пользователя и окончанием визуализации браузером нового содержания.
Однако, для большинства сайтов, загрузка страницы затрагивает десятки внешних объектов, основное время загрузки тратится на различные HTTP-запросы картинок, JavaScript-файлов и файлов стилей. AJAX, возможно, поможет в данной ситуации, но ускорение или удаление этих HTTP-запросов может принести гораздо больше пользы, хотя на данный момент нет единого мнения (a common body of knowledge), как именно это следует делать.
При работе над оптимизацией времени загрузки страницы в заметном AJAX-приложении у меня был шанс исследовать, насколько можно уменьшить задержку за счет внешних объектов. Особенно меня интересовало, как именно конкретная реализация HTTP-клиента в известных браузерах и параметры распространенных интернет-соединений влияют на загрузку страницы, содержащих большое количество маленьких объектов.
Я бы отметил несколько интересных фактов:
Connection: keep-alive
, когда браузер может использовать одно соединение с сервером, чтобы загружать через него достаточно большое количество ресурсов. В случае конвейера браузер может послать несколько GET-запросов в одном соединении, не дожидаясь ответа от сервера. Сервер в таком случае должен ответить на все запросы последовательно.) Это влечет дополнительные задержки на прохождение запроса туда-обратно, что, в общем случае, примерно равно времени ping'а (отнесенном к разрешенному числу одновременных соединений). Если же на сервере нет элементов поддержки активных HTTP-соединений (HTTP keep-alives), то это повлечет еще одно трехступенчатое TCP «рукопожатие», которое, в лучшем случае, удваивает задержку.На основе заявленных предпосылок я примерно смоделировал эффективную ширину канала для пользователей, учитывая некоторые сетевые особенности при загрузке объектов различных размеров. Предполагалось, что каждый HTTP-запрос занимает 500 байтов и что HTTP-ответ содержит дополнительно к размеру запрошенного объекта еще 500 байтов заголовков. Это наиболее простая модель, которая рассматривает только ограничения на канал и его асимметрию, но не учитывает задержки на открытие TCP-соединения при первом запросе для активного (keep-alive) соединения, которые, однако, сходят на нет при большом количестве объектов, передаваемых за один раз. Следует также отметить, то рассматривается наилучший случай, который не включает другие ограничения, например, «медленный старт» (slow-start) TCP, потерю пакетов и т.д. Полученные результаты достаточно интересны, чтобы предложить несколько путей для дальнейшего исследования, однако, не могут никоим образом рассматриваться как замена экспериментов, проведенных с помощью реальных браузеров.
Чтобы выявить эффект от активных соединений и введения нескольких хостов, я смоделировал пользователя с интернет-соединением с 1,5Мб входящим / 384Кб исходящим каналом, находящегося на расстоянии 100мс без потери пакетов. Это очень грубо приближает среднее ADSL-соединение на другой стороне США от ваших серверов. Ниже показана эффективная пропускная способность канала при загрузке страницы с множеством объектов определенного размера. Эффективная пропускная способность определялась как отношение общего числа полученных байтов ко времени их получения:
Следует отметить следующее:
network.http.pipelining
в about:config
), число используемых хостов перестанет играть значительную роль, и он будет использовать свой канал еще более эффективно, однако, мы не сможем это контролировать на стороне сервера.Возможно, более прозрачным будет следующий график, на котором изображено несколько различных интернет-соединений и выведено относительное ускорение для запроса страницы с множеством мелких объектов для случая использования 4 хостов и включение активное соединения на сервере. Ускорение измеряется относительно случая 1 хоста с выключенным keep-alive (0%).
Что тут интересного:
Еще хотелось бы проверить влияние размеров заголовков на эффективную пропускную способность канала. Вышележащий график предполагает, что размер заголовков составляет 500 байтов дополнительно к размеру объекта как для запроса, так и для ответа. Как же изменение этого параметра отразится на производительности нашего 1,5Мб/384Кб канала и расстояния до пользователя в 100мс? Предполагается, что пользователь уже изначально использует 4 хоста и активное соединение.
По графику хорошо видно, что при небольших размерах файлов основные задержки приходятся на исходящий канал. Браузер, отправляющий «тяжелые» запросы на сервер (например, с большим количеством cookie), по-видимому, замедляет скорость передачи данных более чем на 40% для этого пользователя. Естественно, размер cookie можно регулировать на сервере. Отсюда простой вывод, что cookie нужно, по возможности, делать минимальными или направлять ресурсные запросы на сервера, которые не выставляют cookie.
Как я уже говорил выше, все полученные графики являются результатом моделирования и не учитывают некоторое количество реальных особенностей окружающего мира. Но я взял на себя смелость проверить полученные результаты в реальных браузерах в «боевых» условиях и убедился в их состоятельности. Я бы хотел в будущем уделить больше времени тестированию именно в реальных браузерах, включаю широкий диапазон размеров объектов, скоростей доступа и различных задержек.
Можно легко измерить реальную пропускную способность канала для пользователей вашего сайта, и если пользователи загружают вашу страницу существенно медленнее, чем могли бы (учитывая физические ограничения на их канал), возможно, стоит применить меры к исправлению этой ситуации.
Прежде, чем давать браузер любые ссылки на внешние объекты (<img src="...">
, <link rel="stylesheet" href="...">
, <script src="...">
и т.д.), мы можем записать текущее время. После загрузки всей страницы можно будет его вычесть и получить, таким образом, полное время загрузки страницы (за исключением HTML-файла и задержек, связанных с первым соединением с сервером). Полученное время можно затем добавить к вызову любого URL (например, картинки), расположенной на вашем сервере.
JavaScript-код для этого будет выглядеть примерно следующим образом:
<html> <head> <title>...</title> <script type="text/javascript"> <!-- var began_loading = (new Date()).getTime(); function done_loading() { (new Image()).src = '/timer.gif?u=' + self.location + '&t=' + (((new Date()).getTime() - began_loading) / 1000); } // --> </script> <!-- Здесь будут размещаться ссылки на любые внешние JS- или CSS-файлы, главное, чтобы они шли ниже верхнего блока // --> </head> <body> <!-- Здесь идет обычное содержание страницы // --> </body> </html>
Эта конструкция произведет примерно следующую запись в лог-файл:
10.1.2.3 - - [28/Oct/2006:13:47:45 -0700] "GET /timer.gif?u=http://example.com/page.html&t=0.971 HTTP/1.1" 200 49 ...
В этом случае, как можно понять из записи, загрузка оставшейся части страницы http://example.com/page.html
заняла у пользователя 0,971 секунды. Если предположить, что всего на странице было загружено файлов общего размера в 57842 байтов, 57842 байтов * 8 битов в байте / 0,971 секунды = 476556 битов в секунду (4б5Кбит). Такова эффективная пропускная способность канала при загрузке этой страницы. Если у пользователя физический входящий канал 1,5Мб, значит, есть большой простор для увеличения скорости загрузки.
После того, как вы соберете некоторую статистику по времени загрузки страницы и эффективной ширине канала для реальных пользователей, вы можете поэкспериментировать с изменениями, которые могут улучшить эти показатели. В случае значительных достижений в улучшении этого показателя стоит закрепить внесенные изменения.
Нижеприведенные советы частично уже фигурировали в других статьях: советы от Yahoo, объединение CSS-файлов, оптимизация времени загрузки страницы и многих других. Однако, повторение — учения, к тому же, в следующих советах есть несколько свежих моментов.
Можно попробовать следующие вещи:
Включите активное HTTP-соединение для внешних объектов. В противном случае вам придется добавить задержки, связанные с трехступенчатой передачей пакетов при установлении нового HTTP-соединения. Однако если вы озабочены доступностью сервера при достижении глобального максимума активных соединений, то можно выставить небольшой тайм-аут, например, 5–10 секунд. Также стоит выдавать статичные ресурсы с другого сервера, чем динамические. Поддержка тысяч соединений на загрузку статичных файлов со специализированных серверов может выразиться всего в 10Мб оперативной памяти, тогда как основной сервер может затрачивать 10Мб на каждое соединение.
Загружайте меньше небольших объектов. В связи с накладными издержками на передачу каждого объекта, один большой файл загрузится быстрее, чем два более мелких, каждый в два раза меньше первого. Стоит потратить время на то, чтобы привести все вызываемые JavaScript-файлы к одному или двум (или даже, используя технику «ненавязчивый» JavaScript, вообще обойтись без вызовов внешних файлов), равно как и CSS-файлы. Если на вашем сайте используется больше, попробуйте сделать специальные скрипты для публикации файлов на «боевом» сервере или уменьшите их количество. Если пользовательский интерфейс повсеместно использует десятки небольших GIF-файлов, стоит рассмотреть их преобразование в более простой CSS-дизайн (который не потребует такого большого числа картинок) и(ли) объединение в несколько больших ресурсных файлов, используя распространенную технику CSS sprites.
Если пользователи регулярно загружают десяток или больше некеширующихся или некешируемых объектов, стоит распределить их загрузку по 4 хостам. В этом случае обычно пользователь сможет установить в 4 раза больше соединений. Без HTTP-конвейера это выльется в уменьшение потерь на пересылку запроса примерно в 4 раза.
При генерации страницы перед вами встанет задача распределения картинок по 4 разным хостам. Это легче всего сделать с помощью любой хеш-функции, например, MD5. Вместо того чтобы загружать все <img>
с одного http://static.example.com/
, создайте 4 хоста (например, static0.example.com
, static1.example.com
, static2.example.com
, static3.example.com
) и используйте 2 бита из MD5-суммы для каждой картинки, чтобы выбрать, на какой именно хост ставить ссылку на ее загрузку. Убедитесь, что все страницы используют один и тот же алгоритм соответствия (указывают на один и тот же хост для каждой картинки), иначе вы будете безрезультатно бороться против кеширования.
Стоит, однако, заметить, что добавление еще одного хоста увеличивает расходы на дополнительные DNS-поиск и установку HTTP-соединения. Если у пользователей включена конвейерная обработка запросов или страница подгружает менее десяти объектов (лично я бы рекомендовал ориентироваться на 5–6 на хост, т.е. при 10 объектах можно вводить второй хост, при 16 — третий, а при 25 – четвертый), то пользователи не ощутят выигрыша от увеличения числа параллельных запросов и вместо ускорения загрузки сайта заметят ее замедление. Преимущества данного подхода появятся только для страниц с большим числом внешних объектов. Стоит каким-либо образом измерить разницу во времени загрузки для ваших пользователей прежде, чем полностью внедрять данную методику.
Возможно, лучшим выходом для ускорения загрузки ваших страниц для вернувшихся посетителей будет безусловное кеширование браузером статических картинок, файлов стилей и скриптов. Это никак не поможет при загрузке страницы для нового посетителя, но существенно уменьшит время загрузки страницы при повторных посещениях.
Выставляйте HTTP-заголовок Expires
везде, где только возможно, на несколько дней или даже месяцев в будущем (на самом деле, это зависит от аудитории вашего сайта — как часто она меняется — стоит ориентироваться на время, за которое возвращается до 80% от всех посетителей). Такой заголовок будет сообщать браузеру, что ресурс не нужно проверять при каждой загрузке страницы, что поможет уменьшить задержку или время на установление соединения для объектов, которые и так можно не загружать.
Вместо того чтобы полагаться на логику кеширования на стороне браузера, можно изменить объект, просто изменив его URL. Одним из наиболее простых путей в данном случае будет создание новой директории с номером версии, которая будет содержать обновленный статичный файл, организация подключения таких файлов в HTML-коде и их распознавание на сервере, как ссылок на действительные файлы (через mod rewrite
, скорее всего, но я против таких решений: гораздо надежнее добавлять номер версии в GET-строке вызова ресурса). Вместо <img src="http://example.com/logo.gif">
можно использовать <img src="http://example.com/build/1234/logo.gif">
. Если вы захотите выпустить новую версию на следующей неделе, то все ссылки на файл нужно будет заменить на <img src="http://example.com/build/1235/logo.gif">
. Это решает также ту часть проблем, когда браузер кеширует какие-либо файлы дольше, чем предполагалось — так как URL объекта изменился, то это уже совсем другой объект.
Если вы решить архивировать (gzip) HTML, JavaScript или CSS, возможно, стоит добавить «Cache-Control:
private
» в заголовок Expires
. Это предотвратит кеширование этих объектов на прокси-серверах, которые не распознают, что ваши сжатые файлы нельзя доставлять всем пользователям (а только тем, которые отправили на сервер соответствующие заголовки). Заголовок Vary
призван решить эту проблему более элегантно, но на него нельзя полностью полагаться из-за ошибок в IE.
Во всех остальных случаях, когда для одинаковых URL'ов предполагается отдавать одинаковое содержание с сервера (к примеру, статичные картинки), в заголовки стоит добавить Cache-Control: public
, сообщая прокси, что они вправе закешировать результат и отдавать его разным пользователям. Если в локальной кеширующей прокси уже есть ваш файл, то задержка на его доставку до пользователя будет еще меньше; так почему бы не разрешить прокси отдавать ваши статичные объекты, если они в ней имеются?
Избегайте использование GET-параметров в URL'ах картинок, ресурсных файлов и т.д. По крайней мере, Squid отказывается кешировать, по умолчанию, любой URL, содержащий знак вопроса (в таком случае, мы получим кеширование на клиенте, некоторую дополнительную задержку при загрузке файлов в первый раз и относительно быструю разработку, когда новые версии можно будет помечать через GET-параметр файла, на мой взгляд, если мы не рассматриваем максимально оптимизированную под кеширование систему, то такой подход вполне допустим). До меня доходили слухи, что некоторые другие приложения вообще не кешируют такие URL'ы, но точной информации у меня нет.
На тех страницах, где пользователи часто видят одно и то же содержание снова и снова, как, например, на главной странице или в RSS-потоке, внедрение условных GET-запросов может сказаться положительно на уменьшении времени ответа и экономии серверного времени и объема передаваемых данных в тех случаях, когда страница не изменилась.
При выдаче статичных файлов (включая HTML) с диска большинство веб-серверов автоматически генерируют заголовок Last-Modified
и(ли) ETag
в ответе с вашей стороны и обслуживают соответствующие If-Modified-Since
и(ли) If-None-Match
заголовки в клиентских запросах. Но если вы используете включения на стороне сервера, динамические шаблоны или генерируете ваши страницы «на лету», вам придется позаботиться об таких метках в заголовках самостоятельно.
Общая идея предельно проста: при генерации страницы вы обеспечиваете браузер некоторым количеством дополнительной информации о том, что же это конкретно за страница, которую вы ему отправляете. Когда браузер запрашивает эту же страницу снова, он отправляет полученную информацию обратно на сервер. Если она совпадает с тем, что вы собираетесь отправлять, то вы точно знаете, что на клиенте уже есть эта информация, и передаете только 304-ответ (Not Modified
), который гораздо меньше 200, вместо того, чтобы выдавать содержание страницы заново. И если вы достаточно сообразительны относительно информации, которую вы передаете в ETag, то можно избежать запросов (которые могут быть достаточно интенсивными) к базе данных, необходимых для генерации страницы.
Уменьшайте размер HTTP-запросов. Зачастую cookie выставляются на весь домен или даже на все поддомены, что означает их отправку браузером даже при запросе каждой картинки с вашего домена. Это может вылиться в то, что 400-байтный ответ с картинкой превратится в 1000 байтов или даже больше, в зависимости от добавленных заголовков cookie. Если на странице у вас много некешируемых объектов и большие cookie на домен, рассмотрите возможность вынесения статичных ресурсов на другой домен (кстати, так поступил Яндекс, вынеся статику на Yandex.net) и убедитесь, что cookie там никогда не появятся.
Уменьшайте размер HTTP-ответов, включив gzip-сжатие для HTML и XML для браузеров, которые это поддерживают. Например, загрузка документа размером в 17Кб, которым является данная статья, занимает 90мс для пользователя с DSL-каналом в 1,5Мб. Он же может занимать 37мс, если документ сжать до 6,8Кб. Целых 53мс выигрыша во времени загрузки страницы из-за простого изменения. Если размер ваших HTML-страниц значительно больше, то ускорение будет еще более значительным.
Если вам не чуждо новаторство и вы обладаете достаточной смелостью, можно попробовать отдавать браузерам сжатый gzip'ом JavaScript и посмотреть, какие браузеры с этим справятся. (Подсказка: от IE4 до IE6 запрашивают сжатый JavaScript, а затем жестоко обламываются, получая его именно сжатым. По моим собственным данным, не все так плохо, где-то 1/1000 пользователей IE обладает описанной проблемой, подробнее о заголовках для сжатия JS и CSS можно прочитать здесь). Также можно взглянуть в сторону обфускаторов JavaScript-кода, которые вырезают лишние пробелы, комментарии и т. д. Обычно, таким образом можно уменьшить файл от 1/3 до 1/2 от его изначального размера.
Рассмотрите возможность расположить небольшие объекты (или зеркало, или их кеш) максимально близко от пользователей в терминах сетевых задержек. Для больших сайтов с международной аудиторией можно использовать как платные сети доставки содержания (Content Delivery Network), так и отдельные виртуальные машины в пределах 50мс для 80% ваших пользователей вместе с множеством доступных методов для распределения запросов пользователей на ближайшую к ним виртуальную машину (к слову сказать, как раз так работают сайты многих международных компаний, в том числе, и Acronis, распределяющие пользователей по локальным версиям в зависимости от географического признака).
Регулярно проверяйте ваш сайт, заходя с помощью «рядовых» соединений. В моем случае использование «медленного прокси-сервера», который эмулировал плохое DSL-соединение из Новой Зеландии (768Кбит входящий, 128Кбит исходящий, 250мс задержка, 1% потери пакетов) вместо гигабайтного канала с несколькими миллисекундами от серверов в штатах, оказалось весьма полезным. Мы очень оперативно обнаружили и устранили ряд функциональных ошибок и проблем удобства использования.
Для моделирования такого медленного соединения, я использовал модули ядра Linux netem и HTB, которые доступны с версии 2.6. Оба этих модуля устанавливаются с командой tc. Это позволяет добиться наиболее точной эмуляции, которую мне удалось найти, но я бы не назвал ее идеальной. Лично я не пользовался, но, по общему мнению, Tamper Data для Firefox, Fiddler для Windows и Charles для OSX могут выставить ограничения на скорость загрузки и, возможно, легко настраиваются, но, скорее всего, они не умеют аккуратно настраивать сетевые задержки.
Для Firefox можно использовать Firebug, который позволяет построить реальный график загрузки сайта во времени и отобразить, что грузится в браузере в данный момент. Это расширение также позволяет увидеть, как Firefox ждет окончания одного HTTP-запроса, чтобы начать следующий, и как время загрузки возрастает с каждым подключаемым объектом. Расширение YSlow к Firebug также предлагает ряд советов, как улучшить производительность вашего сайта.
Команда разработчиков Safari предлагает пару советов по скрытой возможности в их браузере, которая позволяет получить также некоторую дополнительную информацию по поводу загрузки.
Если же вы хорошо знакомы с HTTP-протоколом и TCP/IP на пакетном уровне, то можно попробовать посмотреть, что происходит, используя tcpdump, ngrep или ethereal. Эти инструменты являются просто обязательными для сетевых отладок любого рода.
Попробуйте протестировать часто загружаемые страницы вашего сайта на производительность из локальной сети, используя ab, который поставляется вместе с веб-сервером Apache. Если сервер отвечает дольше, чем 5–10 миллисекунд при генерации страницы, значит, стоит хорошо разобраться, на что же уходит серверное время.
Если в результате таких тестов задержки оказались весьма высокими, и процесс веб-сервера (или CGI, если вы используете его) «отъедал» слишком много CPU, то причиной этого зачастую может оказаться необходимость в компиляции скриптов в процессе выполнения при каждом запросе. Такое программное обеспечение, как eAccelerator для PHP, mod_perl для perl, mod_python для python и т. д. могут кешировать серверные скрипты в скомпилированном состоянии, существенно ускоряя загрузку вашего сайта. Кроме этого, стоит найти профилирующий инструмент для вашего языка программирования, чтобы установить, на что же тратятся ресурсы CPU. Если вам удастся устранить причину больших нагрузок на процессор, то страницы будут отдаваться быстрее, и вы сможете выдавать больше трафика при меньшем числе машин.
Если на сайте при создании страницы выполняется много запросов к базе данных или какие-либо другие тяжелые вычисления, стоит рассмотреть добавление кеширования на стороне сервера для медленных операций. Большинство людей начинают с записи кеша в локальную память или локальный диск, однако, эта логика перестает работать, если ваша система расширяется до кластера веб-серверов (каждый со своим локальным диском и локальной памятью). Стоит взглянуть в сторону использования memcached, который создает очень быстрый общий кеш, который объединяет свободную оперативную память всех имеющихся машин. Клиенты к нему портированы на большинство распространенных языков.
(Опционально) Подать петиция производителям браузеров с целью включить конвейерную обработку HTTP-запросов по умолчанию в новых браузерах. Если это будет сделано, то нам не придется исполнять эти «танцы с бубнами» (these tricks), и большая часть веба будет загружаться быстрее для среднего пользователя. (В Firefox'е это отключено, предположительно, из-за некоторых прокси, некоторых балансировщиков нагрузок и некоторых версий IIS (привет, Microsoft!), которые впадают в шок при конвейерных запросах. Но в Opera, по-видимому, провели существенную работу для того, чтобы включить эту возможность по умолчанию. Почему так не могут поступить все остальные браузеры?)
Указанный список содержит мои мысли по увеличению скорости связи между браузером и сервером и может быть применен, в общем случае, ко многим сайтам не зависимо от того, как используется веб-сервер или язык разработки для написания сайта. Однако, к сожалению, все эти вещи делают довольно редко.
Хотя все советы направлены на уменьшения времени загрузки страницы, положительными побочными эффектами могут стать уменьшения трафика от сайта и уменьшения нагрузки на серверный процессор на просмотр одной страницы. Уменьшения затрат на ваш вебсайт при улучшении пользовательского восприятия ресурса должно быть веской причиной, чтобы потратить некоторое время на оптимизацию такого рода.