Статьи

Перевод: Николай Мациевский aka sunnybear
Опубликована: 14 января 2008

Практический CSS/JS: архивируем все!

Примечание: ниже частичный перевод статьи "Compress JavaScript and CSS without touching your application code", в которой описывается статичное сжатие CSS- и JS-файлов на сервере и корректная выдача их затем клиенту. Далее даны мои комментарии с более комплексным решением. Приношу извинения, если для кого-то тема будет слишком знакома или неинтересна: в Рунете нормальной статьи на данную конкретную тематику обнаружить не удалось.

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

  • Проверить, умеет ли клиент принимать файлы в формате gzip-encoded.
  • Обеспечить соответствующий вывод на стороне сервера через gzip-функции, либо уповать на то, что всем этим займется непосредственно Apache.
  • Поиграться с .htaccess, чтобы обеспечить корректный content type.

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

Включаем статическое сжатие

Однако, существует способ обойтись просто парой строчек в конфиге (.htaccess), если потратить пару минут и самому заархивировать все необходимые файлы. Предположим, что у нас есть JS-библиотека prototype.js на сервере. Заархивируйте ее через gzip (при помощи 7-zip или еще чего-нибудь, если вы работаете под Windows). В итоге, у вас должен появиться файл prototype.js.gz. Положите его в ту же директорию на сервере, что и исходный файл. Если вы работаете через командную строку в шеле, то достаточно выполнить:

gzip foo.js -c > foo.js.gz

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

AddEncoding gzip .gz
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{HTTP_USER_AGENT} !Safari
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)$ $1.gz [QSA,L]

В первой строке мы сообщаем серверу, что файлы с расширением .gz нужно отдавать с gzip encoding-type, чтобы браузер понял, что перед ним архив, а не текстовый файл. Второй строкой проверяется, принимает ли браузер архивированные файлы, при этом следующие строки просто не отработают, если этот тест провалится. Далее мы исключаем Safari, у которого проблемы с правильным восприятием архивированного содержимого. На четвертой строке мы проверяем, что архивный файл существует, и если это так, то добавляем .gz к существующему имени файла.

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

Меньше редиректов — легче серверу

Примечание: далее идут мои рассуждения на тему и собственное решение.

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

Во-вторых, мне не нравится, что при каждом запросе (ну или при 99%) к статичному ресурсу, Apache должен что-то искать и менять физический адрес, по которому этот файл нужно брать. Я против таких решений, поэтому я предложу другой путь: давайте будем переопределять физический адрес ресурса только для «старых» браузеров, а для всех остальных отдавать рядовой файл.

<IfModule mod_rewrite.c>
    RewriteEngine On
    AddEncoding gzip .gz
    RewriteCond %{HTTP:Accept-encoding} !gzip [OR]
    RewriteCond %{HTTP_USER_AGENT} Safari [OR]
    RewriteCond %{HTTP_USER_AGENT} Konqueror
    RewriteRule ^(.*)\.gz(\?.+)?$ $1 [QSA,L]
</IfModule>

<IfModule mod_headers.c>
    Header append Vary User-Agent
    <FilesMatch .*\.js.gz$>
	ForceType text/javascript
	Header set Content-Encoding: gzip
	Header set Cache-control: private
    </FilesMatch>
    <FilesMatch .*\.css.gz$>
	ForceType text/css
	Header set Content-Encoding: gzip
	Header set Cache-control: private
    </FilesMatch>
</IfModule>

В итоге, логика «переворачивается», и вместо набора «И»-условий мы получаем «ИЛИ»-условия. Первые пять строк уже достаточно подробно обсуждены вверху, я только остановлюсь на последней из них

RewriteRule ^(.*)\.gz(\?.+)?$ $1 [QSA,L]

В ней учитывается, что к статичным ресурсам можно добавлять уникальные GET-строки, чтобы перегружать кеш на клиенте (если ресурс поменялся, то стоит обновить его в кеше, иначе клиенты увидят старую версию). Например, foo.css.gz?v1234.

Следующие строки (ForceType / set Content-Encoding) форсируют для скриптов и файлов стилей соответствующие MIME-типы и Content-Encoding (ибо как удалось нарыть здесь, так браузеры воспринимают ответ лучше). По совету  Q_Zma для mod_rewrite и mod_headers добавил условие его наличия в Apache, если его нет, то браузеры, не поддерживающие gzip'а для CSS/JS-файлов (это, как было заявлено выше, и Safari), не отобразят страницу правильно. Поэтому стоит лишний раз задуматься, применять ли описанную методику в таких условиях.

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

Старая версия правил (без учета Konqueror'а) тестировалась здесь, полет нормальный.

UPD: по информации, подчерпнутой отсюда, добавлен заголовок Header append Vary User-Agent, ибо условие для выдачи gzip'ованного содержание принимается в том числе на основе UserAgent. Однако, после этой статьи добавлено Header set Cache-control: private для предотвращения проблем с IE.

Google Chrome и прочий зоопарк

С выходом браузера от Google, который прикидывается Safari, и появлением gzip у последнего ситуация немного изменилась. Поэтому предлагаю следующий набор правил в качестве наиболее актуального:

<IfModule mod_rewrite.c>
    RewriteEngine On
#перенаправляем Konqueror и «старые браузеры»
    RewriteCond %{HTTP:Accept-encoding} !gzip [OR]
    RewriteCond %{HTTP_USER_AGENT} Konqueror
    RewriteRule ^(.*)\.(css|js)$ $1.nogzip.$2 [QSA,L]
</IfModule>			    

<IfModule mod_headers.c>
    Header append Vary User-Agent
#выставляем для всех css/js файлов Content-Encoding
    <FilesMatch .*\.(js|css)$>
	Header set Content-Encoding: gzip
	Header set Cache-control: private
    </FilesMatch>
#сбрасываем Content-Encoding в том случае, если отдаем не архив
    <FilesMatch .*\.nogzip\.(js|css)$>
	Header unset Content-Encoding
    </FilesMatch>
</IfModule>

В Safari есть небольшой баг: этот браузер не воспринимает файлы стилей/скриптов с расширением .gz. Но это, естественно, обходится. Нам нужно на месте обычных файлов просто иметь архивы (чтобы лишний раз не перенаправлять браузеры), а на месте .nogzip-файлов (см. чуть выше по коду) должны находиться непожатые версии. Такие вот пироги.

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

  1. Добавляем описанные выше инструкции в конфигурационный файл Apache или .htaccess
  2. Пакуем файлы (с помощью 7-zip или gzip) и кладем на место обычных (расширение у файлов должно остаться прежним, .css или .js). Например, вы берете файл anyname.css, пакуете его 7-zip, у вас получается файл anyname.css.gz, переименовываем его обратно в anyname.css и заливаем на сервер. Для gzip все немного проще:
    gzip -c -9 -n anyname.css > anyname.css.gz
    mv anyname.css anyname.nogzip.css
    mv anyname.css.gz anyname.css
  3. Рядом с пожатыми файлами кладутся файлы с расширением nogzip.css или nogzip.js, которые содержат неархивированные копии. Например, после заливки сжатого файла anyname.css, вы создаете на сервере еще один файл anyname.nogzip.css, который является копией несжатого файла. Для gzip это копирование уже производится чуть выше второй строкой в листинге.

Циклические редиректы

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

RewriteCond %{REQUEST_FILENAME} !.nogzip
RewriteCond %{HTTP:Accept-encoding} !gzip
RewriteRule ^(.*)\.(css|js)?$ $1.nogzip.$2 [QSA,L]

RewriteCond %{REQUEST_FILENAME} !.nogzip
RewriteCond %{HTTP_USER_AGENT} Konqueror
RewriteRule ^(.*)\.(css|js)?$ $1.nogzip.$2 [QSA,L]

Таким образом мы запрещаем редиректы для файлов, имеющих .nogzip в своем имени, и предотвращаем образование циклов. Спасибо  anycolor за указание на такую проблему.

Читать дальше

Все комментарии (habrahabr.ru)