Журнал «Компьютерра» № 20 от 30 мая 2006 года - Компьютерра. Страница 30
$data =~ s! { ! {!g; # trim inside brackets
$data =~ s!; }!}!g; # trim inside brackets
print $data;
«Скормим» этому скрипту все имеющиеся у нас CSS по очереди:
perl compress.pl site.source.css > site.compress.css
С помощью такой несложной оптимизации мы можем уменьшить объем передаваемых данных на 50 процентов (во многом это зависит от вашего стиля кодирования – выигрыш может быть и гораздо меньше), а значит, увеличить скорость работы конечного пользователя. Но в идеале нам хотелось бы, чтобы пользователи вообще не запрашивали файлы до тех пор, пока это не станет совершенно необходимо. И для этого нам придется заняться HTTP-кэшированием.
Когда пользовательский агент запрашивает данные с сервера первый раз, он кэширует ответ, чтобы избегать повторных запросов в будущем. Как долго будет храниться этот кэш, зависит от двух факторов – настроек агента и соответствующих заголовков с сервера. Опции настройки агентов имеют незначительные различия, однако большинство из них сохраняет кэш по меньшей мере до окончания сессии, если им прямо не указано обратное.
Вы посылаете заголовки, запрещающие кэширование динамических страниц, чтобы не позволить браузеру кэшировать страницы, которые постоянно изменяются. В PHP это делается с помощью одной строчки:
header(«Cache-Control: private»);
Слишком просто, чтобы быть правдой? Ну, в общем-то, да – некоторые агенты порой игнорируют этот заголовок. Чтобы по-настоящему запретить браузеру кэшировать документ, следует быть немного более убедительным:
# ‘Expires’ in the past
header(«Expires: Mon, 26 Jul 1997 05:00:00 GMT»);
# Always modified
header(«Last-Modified: „.gmdate(„D, d M Y H:i:s“).“ GMT»);
# HTTP/1.1
header(«Cache-Control: no-store, no-cache, must-revalidate»);
header(«Cache-Control: post-check=0, pre-check=0», false);
# HTTP/1.0
header(«Pragma: no-cache»);
Это годится для контента, который мы не хотим кэшировать, но если контент не меняется при каждом запросе, нам нужно добиться от браузера обратного поведения. Для этого в заголовке запроса используется конструкция If-Modified-Since. Получив такой запрос, Apache (или любой другой веб-сервер) может выдать код 304 (Not Modified), тем самым сообщая браузеру, что у того в кэше уже находится актуальная версия документа. Благодаря этому механизму, нам не приходится пересылать файл заново, однако лишний запрос обрабатывать все же пришлось. Гм.
Использование entity tags похоже на работу с конструкцией if-modified-since. Apache на запрос к статическому ресурсу может отдавать заголовок Etag, содержащий контрольную сумму, сгенерированную из размера файла, времени последнего изменения и номера индексного дескриптора. Браузер может запросить заголовок файла, чтобы проверить e-tag документа перед загрузкой. Очевидно, что использование e-tag сопряжено с теми же накладными расходами, что и механизм if-modified-since, – клиент все еще вынужден делать лишний HTTP-запрос, чтобы определить валидность локальной копии.
Кроме того, нужно соблюдать осторожность с if-modified-since и e-tags, если выдача контента идет с нескольких серверов. В системе из двух хорошо сбалансированных серверов любой документ может быть запрошен одним и тем же агентом с любого из двух серверов – или с каждого (не одновременно). Это нормально. Для этого мы и выравнивали нагрузку. Однако если серверы генерируют разные e-tags или разные даты изменения документов, браузер не сможет нормально поддерживать актуальный кэш. По умолчанию e-tag генерируются с использованием индексных дескрипторов, которые на разных серверах разные. Это можно запретить с помощью следующей опции в настройках Apache:
FileETag MTime Size
Теперь Apache для генерации e-tag будет использовать только время последнего изменения и размер файла. Это, к сожалению, приводит нас к другой проблеме использования e-tag, которая тоже актуальна для if-modified-since (хоть и в меньшей степени). Поскольку e-tag зависит от времени последнего изменения, нам необходимо следить за синхронизацией. Если мы распределяем файлы по разным веб-серверам, всегда остается шанс, что на один из серверов файл попадет на секунду или две позже, чем на другой. В этом случае e-tag, сгенерированные серверами, будут отличаться. Мы можем изменить настройки так, чтобы генерировать e-tag только на основании размера файла, но это означает, что файл не обновится в браузере, если мы изменим его содержимое, а размер останется неизменным. Тоже не идеально.
Дело в том, что мы подходим к проблеме не с той стороны. Все возможные стратегии кэширования отталкиваются от того, что клиент спрашивает сервер, насколько актуальна копия, хранимая в кэше. Если бы сервер сам, без запроса, сообщал клиенту об изменениях файлов, то клиент в любой момент времени знал бы, что кэшированная копия валидна. Но веб устроен иначе – клиент запрашивает сервер, и никак иначе.
Или все же слегка иначе? Ведь перед отправкой любых JavaScript– или CSS-файлов клиент запрашивает страницу, которая на них ссылается с помощью тегов <script> и <link>. И мы можем использовать реакцию сервера для информирования клиентов о любых изменениях, произошедших с этими ресурсами. Звучит немного загадочно, поэтому скажу прямо: если мы будем изменять названия JavaScript– и CSS-файлов при каждом изменении их содержания, то сможем разрешить клиенту хранить их в кэше вечно (ведь содержимое файла по отдельно взятому адресу не меняется).
Если мы уверены в том, что конкретный ресурс никогда не изменится, то можем отправить несколько по-настоящему агрессивных заголовков. В PHP нам потребуется всего пара строк:
header(«Expires: „.gmdate(„D, d M Y H:i:s“, time()+315360000).“ GMT»);
header(«Cache-Control: max-age=315360000»);
Так мы говорим браузеру, что содержимое файла останется неизменным в течение десяти лет (т.е. плюс-минус 315 360 000 секунд), поэтому, единожды загрузив файл, браузер может следующие десять лет использовать локальную копию. Конечно, необязательно использовать для отправки JavaScript и CSS именно PHP. Мы перейдем к этому через несколько минут.
Ручное изменение названий файлов при изменении их содержимого – опасное занятие. Что произойдет, если вы переименуете файл, но забудете переименовать шаблоны, указывающие на него? Что произойдет, если вы поменяете не все шаблоны, а только часть? Что случится, если вы измените шаблоны, но сам файл не переименуете? И наиболее вероятный вариант: что произойдет, если вы измените содержимое, но забудете переименовать файл или изменить ссылки на него? В лучшем случае, пользователи будут работать на старой версии сайта. В худшем – сайт перестанет работать совсем. В общем, ручное изменение названий – дурацкая идея.
На наше счастье, компьютеры с такими задачами – тупым повторением одних и тех же операций при совпадении определенных условий – справляются отменно.
Чтобы сделать этот процесс максимально безболезненным, следует для начала уяснить, что нам вообще не нужно переименовывать файлы. Между реальным расположением файла на диске и URL, с которого он доставляется пользователю, нет ничего общего. Так что в случае Apache мы можем использовать mod_rewrite, создав простое правило для редиректа определенных URL к нужным файлам:
RewriteEngine on
RewriteRule ^/(.*.)v[0-9.]+.(css|js|gif|png|jpg)$ /$1$2 [L]
Это правило обрабатывает все URL с указанными расширениями, которые также содержат суффикс версии. В процессе обработки правило переписывает URL так, чтобы он указывал на путь к нужному файлу (исключая при этом суффикс). Например: