Pull to refresh

Разгони свой сайт – автоматическая СКЛЕЙКА + GZIP

Website development *
Разгонись

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

Алгоритм получится примерно такой:
  1. запрашивается некий файл со специального URL
  2. если клиент поддерживает сжатие и сжатый файл такого типа есть в нашем кэше – отдаем и завершаем обработку
  3. если же сжатие не поддерживается и есть просто файл такого типа – отдаем его и заканчиваем обработку
  4. иначе запускаем наш обработчик

Условимся, что срабатывать наша модель при обращении к 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%.


Если есть какие-либо корректировки, предложения или критика – милости прошу в комменты – буду очень признателен, ибо как говорится – век учись )

Быстрых Вам сайтов, максимального сжатия и радостных клиентов ;)      I'm

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» это одно и тоже, а на практике вы получите два файла в кэше…

Из комментариев

Imenem и TrueDrago подсказали, что оптимальный уровень для deflate компрессии не 9, а 6
Tags:
Hubs:
Total votes 80: ↑60 and ↓20 +40
Views 15K
Comments 85
Comments Comments 85