пятница, 27 января 2012 г.

Как разогнать свой сайт?


Как разогнать свой сайт
Есть куча советов как убыстрить отдачу сайта – это и статика через 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]

Теперь возьмемся за нашу самую главную магию – автоматическое формирование этих самых файлов.

Здесь еще одна есть хитрость, в данном случае скорее — еще одна условность – в файлах html мы будем писать запросы к css или js в следующтим виде: «/glue/1.css—2.css—3-4-5.css», где «-» — это замена «/», а «--» – это разделитель файлов. Кроме того в именах могут быть только английские буквы, цифры и символ «_», по мне — этого более, чем достаточно.

Конечно же, это условности и Вы можете выбрать себе другие правила и другие разделители. Например можно использовать «,» или что-либо еще.

Однако я выбрал «-» из-за того что это вполне нормальный и часто встречающийся символ 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» это одно и тоже, а на практике вы получите два файла в кэше…

Комментариев нет:

Отправить комментарий