Есть куча советов как убыстрить отдачу сайта – это и статика через nginx
и кластеризация и куча еще всяческих хитрых технологий. Однако во всех
книжках, советующих как можно повысить загрузку сайтов можно найти две
постоянно повторяющиеся темы – «склеивание CSS/JS» и «включение сжатия».
Склейка
Все
просто – если например у Вас на страничке 3 CSS файла и 5 JS, браузеру
при загрузке придется создавать 8 соединений и выкачивать по ним данные,
а как известно, лучше несколько больших файлов чем множество мелких.
Связано это с тем, что на каждую установку соединения браузер тратит
время и зачастую немаленькое – до 40% времени загрузки.
Стандартные
методы написать некий командный файл, который пробегался бы по нужным
файлам и склеивал их в один мне не нравились в принципе, ибо делать
ручками вещи, которые можно сделать автоматически – в корне не верно, в
данном случае хотя бы по тому, что это сказывается либо на разработке,
либо на продакшене (дополнительные действия).
Как говорят «никогда не переписывайте то, что можно просто вырезать и наклеить» ;)
Сжатие
Чем
меньше объем «прокачиваемых» файлов, тем соответственно меньше время
тратится на загрузку. Даже если эти файлы сжаты и мы тратим некторое
время на распаковку – при современных вычислительных мощностях на
клиенте эта временная затрата практически не существенна.
Большинство современных браузеров поддерживают метод сжатия deflate,
иногда называемый gzip по имени стандартной *nix утилиты, осуществляющей
это дело.
Что можно и нужно сжимать в веб? Любые текстовые запросы, как то: JS / CSS / JSON / HTML.
Есть
замечательный модуль для Апача mod-deflate, которым можно прямо из
.htaccess указать чего сжимать и чего не сжимать, очень прост в
использовании, но увы и ах! – обычно запрещенный на стандартных
хостингах по причине того, что они (хостеры) опасаются за свое
процессорное время.
Доля разумного в этом конечно есть – этот
модуль жмет все «на лету» и если не принять некоторых хитростей, каждый
раз грузя страничку для нового пользователя он будет
заново пережимать все CSS / JS и т.д.
Если же у вас VDS и Вы – сам себе хозяин – используйте mod-deflate, ибо он хорошо отлажен и примеров применения в сети масса.
А
мы вернемся к обычным хостигам – есть ли выход? Даже если Вас съели, у
вас всегда есть два выхода — есть выход и здесь. Причем эта задача очень
хорошо ложиться на предыдущую – сейчас объясню почему.
Большинство
JS / CSS и других текстов – это статика, т.е. они не меняются в
процессе функционирования сайта — есть смысл их объеденить, чтобы
удовлетворить пункту о «склейке» + сразу же сжать.
Полученные
файлы мы положим в некий кэш, откуда наш Апач будет их брать и отдавать.
Причем процесс мы автоматизируем через mod-rewrite.
Алгоритм получится примерно такой:
запрашивается некий файл со специального URL
если клиент поддерживает сжатие и сжатый файл такого типа есть в нашем кэше – отдаем и завершаем обработку
если же сжатие не поддерживается и есть просто файл такого типа – отдаем его и заканчиваем обработку
иначе запускаем наш обработчик
Условимся, что срабатывать наша модель при обращении к URLу вида «/glue/….»,
А файлы будут лежать в «/static/glue/…».
В данном случае мы
убиваем еще одного зайца — файлы будут отдаваться через PHP всего один
раз — при формировании, а дальше будет все как у больших :) статику
должен и будет отдавать веб-сервер.
В принципе можно сделать так,
чтобы папка совпадала с URL-ом, тогда чуть упростится конфиг
mod-rewrite но будет не так интересно, вобщем упростить всегда можно :)
Надеюсь, что в корне Вашего сайта уже живет файл .htaccess с содержанием типа такого:
RewriteEngine On
RewriteBase /
RewriteRule ^.*$ index.php [QSA,L]
Ну либо похожий. Основное
условие, что если mod-rewrite не нашел чего сделать с пришедшим URL, он в
конце концов вызовет какой-то скриптовый файл.
В данном случае – index.php
Для добавления нашего алгоритма пропишем в .htaccess следующее:
Добавляем поддержку сжатых файлов .gz, а также .jz.gz и .css.gz
AddEncoding gzip .gz
< FilesMatch "\.js.gz$">
#для проксей
Header set Cache-control: private
Header append Vary User-Agent
ForceType "text/javascript"
Header set Content-Encoding: gzip
AddCharset windows-1251 .js.gz
< /FilesMatch>
< FilesMatch "\.css.gz$">
#для проксей
Header set Cache-control: private
Header append Vary User-Agent
ForceType "text/css"
Header set Content-Encoding: gzip
< /FilesMatch>
Добавляем правило отдачи наших файлов (разыменовывание URL в физическую папку)
RewriteCond %{ENV:REDIRECT_GZ} =1
RewriteCond %{REQUEST_URI} ^/glue/(.+)$
RewriteCond %{DOCUMENT_ROOT}/static/glue/%1 -f
RewriteRule . - [L]
Добавляем проверку на поддержку клиентом сжатия
RewriteCond %{REQUEST_URI} ^/glue/(.+)$
RewriteCond %{DOCUMENT_ROOT}/static/glue/%1.gz -f
RewriteCond %{HTTP:Accept-Encoding} ^.*?gzip.*$ [NC]
RewriteCond %{HTTP_USER_AGENT} !^konqueror [NC]
RewriteRule ^siteglue/(.*)$ /static/glue/$1.gz [L,E=GZ:1]
Если сжатие не поддерживается
RewriteCond %{REQUEST_URI} ^/glue/(.+)$
RewriteCond %{DOCUMENT_ROOT}/static/glue/%1 -f
RewriteRule . static/glue/%1 [L,E=GZ:1]
Теперь возьмемся за нашу самую главную магию – автоматическое формирование этих самых файлов.
Конечно же, это условности и Вы можете выбрать себе другие правила и другие разделители. Например можно использовать «,» или что-либо еще.
Однако я выбрал «-» из-за того что это вполне нормальный и часто встречающийся символ URL и с ним врядли могут быть всякие дурацкие проблемы типа вырезания его кривыми скриптами на проксях по пути от Вас до клиента.
В файле в index.php (или что там у вас запускается согласно .htaccess?) добавляем обработчик, который проверяет URL на соответствие нашему «/glue/.*» и в случе совпадения делает echo( Glue::generate( $str ) ), где $str — то, что у нас идет в URL после последнего слэша, т.е. для «/glue/a.js» это будет «a.js»
Сам класс Glue вот такой
class Glue {
static $allowedExt = array(
"js" => array( "check" => "/^js/.*?.js$/", "delimeter" => ";n", "mime" => "text/javascript”),
"css" => array( "check" => "/^css/.*.css$/", "delimeter" => "n", "mime" => "text/css” ),
);
static function generate( $str ) {
if ( !$str ) return null; //не нашли URL
$files = array();
preg_replace( "/((?:[a-z0-9_.]+-)+[a-z0-9_.]+.([a-z0-9]+))(?:--|$)/ie", "$files[]=str_replace( -, /, "1")", $str );
if ( count( $files ) == 0 ) return null; //не нашли ни одного файла в URL
$srcF = "/static”; //наша папка, откуда берется статика
$dstF = "/glue”; //папка, нашего кэша
$content = "";
$cext = substr( strrchr( $files[0], . ), 1 );
if ( $cext === false ) return null; //не смогли определить расширение
$fd = null;
foreach( self::$allowedExt as $k => $v ) {
if ( $k == $cext ) {
$fd = $v;
break;
}
}
if ( !$fd ) return null; //не нашли среди доступных расширений
$usedNames = array();
$fdC = &$fd["check"];
$fdD = &$fd["delimeter"];
foreach( $files as $name ) {
$ext = substr( strrchr( $name, . ), 1 );
if (
$ext === false ||
in_array( $name, $usedNames ) ||
$ext != $cext ||
!preg_match( $fdC, $name )
) return null; //не смогли найти расширения, файл ч таким именем уже есть или расширение отличается от первоначального либо имя не удовлетворяет проверке
$usedNames[] = $name;
$filec = file_get_contents( "{$srcF}/{$name}" );
if ( !$filec ) return null; //не смогли найти или прочитать файл
$content .= $content != "" ? $fdD . $filec : $filec;
}
//сохранили файл
file_put_contents( "{$dstF}/{$str}", $content );
//сохранили сжатый файл
$gzip = gzencode( $content, 9 );; //gzdeflate( $content, 9 );
if ( $gzip ) file_put_contents( "/{$dstF}/{$str}.gz",$gzip );
//мы должны отдать по данному запросу содержимое и mime-тип
header( "Content-type: " . $fd["mime"], true );
return $content;
}
}
Опять же, здесь лишь иллюстрируется один из способов КАК это сделать — не нравится статический класс — Вы можете выбрать любой другой способ — с блэкджеком и дамами не тяжелого поведения ;)
Вот в принципе все, осталось пробежаться по файлам проекта – все таки остался кусочек «ручной» работы :( — и прописать вместо кучи скриптов один, но по правилам, описанным чуть Выше.
Все – при первом запросе автоматически все соберется и начнет отдаваться.
Еще одно маленькое дополнение – а что делать с контентом, отдаваемым PHP? Его тоже надо сжать!
Для этого в то месте где Вы отдаете файлы текстового вида, там где отдается сформированный контент – например так echo( $content );
Сделать следующее:
if ( isClientSupportGzip() ) {
ob_start("ob_gzhandler");
echo( $content );
ob_end_flush();
} else echo( $content );
Это будет сжимать отдаваемый динамический контент, если клиент поддерживает сжатие. Функция, его проверяющая, взята с просторов интернета и выглядит так:
function isClientSupportGzip() {
if ( headers_sent() || connection_aborted() ) return false;
if ( stripos( getenv( "HTTP_ACCEPT_ENCODING" ), "gzip" ) === false ) return false;
if ( stripos( getenv( "HTTP_USER_AGENT" ), "konqueror" ) !== false ) return false;
return true;
}
Для девелоппинга рекомендую завести некую константу режима разработки и в случае установки ее в 1 просто не записывать файлы в кэш и не сжимать динамику – не придется при каждом изменении в каком-либо js файле лазить и очищать нашу директорию с кэшем.
Вот и все – мы чуть-чуть разогнали свой сайт ) По моим наблюдениям прирост в скорости отдачи может составлять 30-40%.
Если есть какие-либо корректировки, предложения или критика – милости прошу в комменты – буду очень признателен, ибо как говорится – век учись )
Быстрых Вам сайтов, максимального сжатия и радостных клиентов ;)
P.S.
Если вы используете какую либо библиотеку, например jquery, на всех страницах своего проекта с одним и тем же местом расположения, рекомендую все-таки вынести ее в отельный файл, то же касается единого css – т.о. она быстрее скэшируется, браузером.
При склейке JS помните особенность – склеивать надо через «;», т.к. в предыдущем файле после последней строчки может не оказаться «;»
При написании обработчика формирования кэша помните о хакерах – проверяйте все и вся, при неграмотном экранировании можно насклеивать и получить в качестве статики много чего интересного, на худой конец можно путем перебора насмерть засрать Вам дисковое пространство, так что даже мистер Пропер не поможет – аккуратней вобщем.
Если у Вас в сайт в самой непопулярной кодировке, чтобы все было шоколадно, замените ForceType «text/javascript» на ForceType «text/javascript; content=windows-1251» и добавьте: AddCharset windows-1251 .js и AddCharset windows-1251 .css
И еще маленький совет, придерживайтесь одинаковой очередности в указании склеиваемых файлов, ибо технически «/glue/a.js—b.js» и «/glue/b.js—a.js» это одно и тоже, а на практике вы получите два файла в кэше…