Кэширование и оптимизация работы сайта на базе Zend Framework

17 Мая 2010 — Иван
Zend Framework
14
комментариев

После того, как вы создали сайт, его хорошо бы заоптимизировать и настроить кэширование. Выгода от всего этого, безусловно, огромная:

  • у пользователей сайт загружается гораздо быстрее;
  • уменьшается количество запросов к серверу, вследствие чего уменьшается нагрузка на него;
  • многие ресурсоемкие запросы к БД не выполняются каждый раз заново, а просто берутся из кэша и возвращаются запросившему их скрипту, благодаря чему снижается нагрузка на базу данных;
  • если на странице нету никаких изменяющихся данных, то закэшировав ее целиком вы добьетесь того, что не будет грузиться даже сам огромный Zend Framework с кучей библиотек, хелперов, моделей и контроллеров, а браузеру отдастся сразу же готовый html-код.

Этот список можно продолжать и продолжать, потому — приступаем к оптимизации. На готовом сайте делать оптимизацию я рекомендую именно в том порядке, в котором она описана в статье.

Объединение и сжатие всех стилей и скриптов

Обычно на более-менее крупных сайтах используется несколько файлов со стилями на каждой странице. А уж для яваскриптов это вообще абсолютно обычная ситуация: файл с jquery, пара расширений к нему, свои скрипты и либы и т.д. На некоторых наших сайтах доходит до подключения 4 файлов стилей и 8 яваскриптов. Разумеется, это очень удобно для разработки, так как все разбито по файлам и структурировано, но очень плохо для посетителей Ваших сайтов. Ведь в приведенном выше примере браузеру пользователя придется направить целых 12 запросов к серверу, чтобы получить все нужные файлы. К тому же, в яваскриптах и стилях всегда есть разные отступы, лишние пробелы, комментарии. Это, опять же, очень удобно для разработчика, но совсем не нужно пользователям — вес файла то увеличивается.

Для выхода из ситуации видится одно прекрасное решение — объединить все файлы стилей в один, а скриптов — в другой, удалив из них предварительно все отступы, комментарии и т.д. Можно это делать и вручную, если на всех страницах сайта используется один и тот же набор стилей и скриптов и вы знаете, что он точно не будет меняться. Но если ваш проект сделан на Zend Framework и вы частенько дорабатываете свои сайты, то гораздо лучше сделать этот процесс автоматическим с помощью специальных помощников вида MagicHeadScript и MagicHeadLink и библиотек для сжатия js и css файлов.

Со статьей автора этих библиотек вы можете ознакомиться по адресу http://habrahabr.ru/blogs/zend_framework/85324/ Я же немного доделал эти библиотеки для большего, как мне кажется, удобства использования. Полный набор всех библиотек, используемых в статье, можно скачать по ссылке в конце статьи.

В статье автором описан принцип их работы, но я тоже расскажу о нем в двух словам и поясню их использование конкретным примером. Итак, чтобы эти хелперы заработали корректно, должны быть выполнены следующие условия:

  • все скрипты и стили должны быть подключены с помощью помощников вида headScript() и headLink()
  • нужно подключить классы с MagicHeadLink, MagicHeadScript, а также классы Minify и JSMin
  • нужно создать папки для хранения кэшей и указать их скрипту, соответственно, строчками: My_MagicHeadScript::setConfig(’/public/cache/scripts’) и My_MagicHeadLink::setConfig(’/public/cache/styles’)
  • требуется в шаблоне вызвать $this->magicHeadLink() и $this->magicHeadScript()

Все довольно просто. А теперь, для лучшего понимания, приведу пример и расскажу, как это делаю я. При создании приложения я всегда, первым делом, создаю базовый абстрактный контроллер, называю его firstController и все последующие контроллеры наследую от него. Это довольно удобно, особенно когда во всех контроллерах есть какой-то одинаковый функционал. В этом случае его очень удобно выносить в этот firstController и потом просто вызывать из других контроллеров. Но, повторюсь, выносить туда стоит только тот функционал, который Вам требуется во всех остальных контроллерах. В методе этого же контроллера (я его называю initMain) очень удобно будет добавить через

01
02
$this->view->headScript()->appendFile('/public/scripts/script.js');
$this->view->headLink()->appendStylesheet('/public/styles/style.css');

Основные стили и скрипты, которые используются на всех страницах сайта. Например, библиотеку jQuery. Далее, в этом же методе, задаем папки для хранения кэшей скриптов и стилей:

01
02
My_MagicHeadScript::setConfig('/public/cache/scripts');
My_MagicHeadLink::setConfig('/public/cache/styles');

Все, теперь все настроено и остается только во всех остальных контроллерах не забывать вызывать этот базовый метод. Если в каких-то других контроллерах потребуется добавить еще скрипты или стили, то можете точно так же добавлять их через помощники headScript() и headLink(). В результате, при выводе они так же подхватяться помощниками magicHeadScript и magicHeadLink и соберутся в один файл. Ну и чтобы все выводилось, надо в файле сосновным шаблоном (у меня это \application\views\scripts\index.phtml) вставить строчки My_MagicHeadScript::setConfig(’/public/cache/scripts’); My_MagicHeadLink::setConfig(’/public/cache/styles’); Сами файлы скриптов magicHeadScript и magicHeadLink можно хранить в любом месте, но я предпочитаю папку /libs/My/ Таким образом, если вы хотите, чтобы библиотеки подключались автоматически при вызове, вы должны добавить папку /libs в основные пути.

Настройка серверного кэширования на базе Zend Framework

За кэширование в Zend’е отвечает модуль Zend_Cache. Модуль довольно гибкий и удобный, обладает кучей возможностей, про которые очень хорошо написано в официальном мануале. В статье же рассмотрим конкретные примеры и один подводный камень, который встречается при использовании этого компонента.

Грубо говоря, кэшировать с помощью Zend можно двумя путями: либо страницу целиком, либо какие-то ее части/блоки. Целиком хорошо кэшировать, когда страница создается динамически из разных данных, с использованием запросов к БД, но реально изменяется довольно редко. Хороший пример — это страница с Портфолио на нашем сайте. Работы добавляются не очень часто и чтобы не грузить базу данных лишними запросами можно закэшировать страницу и отдавать пользователю готовый html-код. При этом даже не будет грузиться весь Zend. Но бывает, что целиком кэшировать — не очень удобно. Допустим, когда на странице есть блок с редко изменяющейся информацией и есть блок с новостями. Тогда кэширование целой страницы нам уже не подходит. Но можно же сделать это только для того редко изменяющегося блока, а новости будут так же браться каждый раз из БД динамически. При этом нагрузка все равно снизится, ведь к той же базе данных будет идти все равно меньше запросов. Теперь разберемся на конкретных примерах, как это реализовывать.

Кэширование страницы целиком

Для этого мы будем использовать фронтенд Page. В официальном руководстве, казалось бы, все очень хорошо расписано про этот случай. Есть даже пример. Одна беда — пример этот не работает. И проблема заключается в том, что когда мы вызываем $cache->start(), то Zend, почему-то, не может подставить сам идентификатор и на основе его создать кэш. Потому если вы попробуете применить пример из руководства, то все запуститься, код отработает, но файл в папочке с кэшами не появится. Почему так происходит — не известно. Поэтому я предлагаю передавать Zend’у этот идентификатор вручную. Он должен быть уникальным, поэтому в случае кэширования страниц целиком, будет вполне логично передавать хэш урла страницы. Для каждой страницы ведь используется свой урл. Таким образом, на каждую страницу у нас будет свой файлик с кэшем, причем только один — как раз то, чего мы и хотим. Но возникает небольшая проблемка — адрес может быть набран как http://site.ru/about, так и http://site.ru/about/ Хотя, фактически, это одно и то же, но урлы различаются на один символ и в кэше будет лежать уже два одинаковых файла для этих урлов, но с разными именами. Нам это не нужно, потому при создании идентификатора будем просто отбрасывать последний символ «/» (если он есть).

Вроде со всем разобрались и остается только один вопрос — где и как подключать это кэширование? Тут вариантов несколько, но мне больше всего нравится подключение в плагине Front контроллера. Если вы не работали с ними, то советую ознакомиться с этой статьей. Вообще говоря, плагины Front-контроллера штука очень мощная и интересная, может много где помочь. Но вернемся к нашей теме: в папочке /lib/My/ создаете файл Cache.php и пишете в него следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class My_Cache extends Zend_Controller_Plugin_Abstract{
  public function dispatchLoopShutdown(){
    $frontendOptions = array(
      'lifetime' => 2592000,
      'debug_header' => false,
      'default_options' => array(
        'cache' => true
      )
    );

    $backendOptions = array(
      'cache_dir' => './tmp/',
    );
        
    $cache_id=$_SERVER['REQUEST_URI'];
        
    $lastSymbol = $cache_id{strlen($cache_id)-1};
    
    if($lastSymbol=='/'){
      $cache_id = substr($cache_id, 0, -1);
    }
        
    $cache = Zend_Cache::factory('Page', 'File', $frontendOptions, $backendOptions);
      
    $cache->start(md5($cache_id));
  }
}

А в Вашем Bootstrap-файле вызываете этот плагин следующим образом:

01
02
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new My_Cache());

Также не забудьте создать в корне папку /tmp.

В моем примере кэшироваться будет весь сайт. Но если Вам нужны какие-то отдельные урлы, то можете воспользоваться опцией regexps и перечислить их. Такой пример уже есть в руководстве и описывать его я не буду.

Кэширование отдельных блоков

Тут будем использовать фронтенд Output. В руководстве, в принципе, написано про него, но его использование лучше будет пояснить на конкретном примере. Итак, в нужном вам контроллере пишете следующее:

01
02
03
$frontendOptions = array('lifetime' => 604800, 'automatic_serialization' => true);
$backendOptions = array('cache_dir' => './tmp/');
$cache = Zend_Cache::factory('Output', 'File', $frontendOptions, $backendOptions);

Этим мы настроили кэширование. Теперь о том как его использовать. Допустим, у Вас есть какой-то кусок кода, который запрашивает модель, получает от нее массив с данными и присваивает его переменной:

01
$this->view->newsArray=$modelNews->getAllNews() ;

Чтобы закэшировать результат обращения к модели меняем этот кусок на следующее:

01
02
03
04
05
if (!($newsArray = $cache->load('news'))) {
  $newsArray=$modelNews->getAllNews();
  $cache->save($newsArray);
}
$this->view->newsArray=$newsArray;

Здесь ’news’ — это уникальная метка для кэша (в предыдущем примере мы использовали в качестве нее урл страницы). Таким образом, скрипт смотрит, есть ли в кэше данные с идентификатором «news». Если есть — сразу присваивает их переменной $this->view->newsArray, а если нету — делает запрос к модели.

В руководстве описываются еще и другие фронтенды, но в 95% случаев для оптимального кэширования хватит этих двух.

Настройка клиентского кэширования

Этот пункт проще всего в реализации. Как известно, у каждого браузера имеется свой собственный кэш, в который он может помещать как отдельные картинки и файлы скриптов/стилей, так и сам html-код. При этом загрузка всего этого добра из кэша происходит мгновенно, и к тому же не идет нагрузка на сервер — браузер к нему вообще не обращается, а берет все ресурсы у самого себя. Но по умолчанию браузер этого не делает — надо ему об этом явно сообщить. Будем рассматривать случай, когда сайт стоит на сервере Apache. Тогда для наших целей нам отлично подходят модуль mod_expires и файл .htaccess, где мы, собственно, и будем прописывать настройки для него.

Теперь определимся с тем, что именно будем кэшировать и последствия этого. Клиентское кэширование очень сильно отличается от серверного — тут мы уже не сможем очистить темповую директорию для отображения исправленной страницы пользователю. Если браузеру дали команду закэшировать картинку logo.jpg на 90 дней, то она так и будет у него лежать 90 дней. И что бы вы ни делали на сайте, как бы ни изменяли этот самый logo.jpg, пользователь эти 3 месяца будет видеть старую картинку, однажды загруженную его браузером. Если, конечно, сам не решит очистить свой кэш. Точно такая же ситуация со стилями, скриптами, иконками и т.д. Но из этой ситуации есть выход. Пользователю все же можно показать новую картинку вместо закэшированной старой. Для этого, надо ее просто переименовать. Ну и, разумеется, прописать на сайте ссылку на новую картинку. То есть, если вы назовете исправленную картинку logo1.jpg, и пропишете на сайте путь к ней, то пользователь увидит уже ее, а не старую, так как кэширование идет именно по именам файлов! Как видите, не очень удобный способ, но в крайних случаях вполне подойдет.

С кэшированием же яваскриптов и стилей, если вы будете использовать описанную выше методику их сжатия и компоновки, такой проблемы не возникнет. Как я писал, имена сжатых файлов — это хэш, зависящий от имен группируемых файлов и их содержимого. Таким образом, если вы измените хоть одну буковку в каком-нибудь из скриптов или стилей, то хэш изменится и файл новой скомпанованной версии будет уже иметь другое имя, а следовательно и не будет браться из кэша браузера. А вот с кэшированием html-кода так не пройдет — придется менять имя страницы, чтобы выкинуть его из кэша. А это, очень часто, просто недопустимо.

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

01
02
03
04
05
06
07
ExpiresActive On
ExpiresByType text/javascript A7776000
ExpiresByType image/gif A7776000
ExpiresByType image/jpeg A7776000
ExpiresByType image/png A7776000
ExpiresByType text/css A7776000
ExpiresByType image/x-icon A7776000

Здесь мы включаем кэширование на 90 дней (7776000 секунд) для нужных файлов исходя из их MIME-типа, который задается сервером. Иногда этот метод работает криво, потому можно задать кэширование еще и по расширению файлов с помощью директивы FilesMatch:

01
02
03
04
05
<FilesMatch \.(css|js|ico|png|jpg|jpeg)$ >
  Header append Cache-Control public
  ExpiresActive On
  ExpiresDefault "access plus 90 days"
</FilesMatch >

Какой из этих методов использовать — решать Вам. Попробуйте их по очереди и протестируйте сайт с помощью плагина Yslow к браузеру Firefox. Какой покажет лучшие результаты — тот и применяйте. Хотя я, на своих проектах, всегда использую их оба — где не поможет первый метод, там сработает второй (или наоборот) и в результате закэшируются все нужные файлы.

Заключение

Итак, мы рассмотрели наиболее важные и базовые методики, которые сделают работу с сайтом для Ваших пользователей быстрой и приятной, а нагрузку на сервер снизят до чисто условных значений. Если вы дочитали до этого момента, то поздравляю Вас, так как тема это, все же, довольно непростая, особенно для новичков. И если у Вас появятся какие-то вопросы по статье — задавайте их в комментариях, постараюсь все объяснить.

Скачать в формате rarСкачать библиотеки в .rar (14 Кб)

Комментарии (14) написать комментарий
  • 2:39, 3 Октября 2010 — ifrond
    $cache->start() не срабатывает в том случае, если вы по умолчанию стартуете сессию и, соответственно, храните ключ сессии в куках.
    В принципе, это логично, потому что выдача страницы может зависеть от пользователя.
    Вы можете отключить такое стандартное поведение с помощью переключения cache_with_cookie_variables в true.
    ответить
    • 16:08, 3 Октября 2010 — Иван
      Да, вы правы. Но включать кэширование, если в массиве $_COOKIE есть переменные, не всегда удобно.
      ответить
  • 5:06, 3 Декабря 2010 — олимпиада сочи 2014
    Всем привет, Это было и со мной. Давайте обсудим этот вопрос.
    ответить
  • 14:14, 6 Декабря 2010 — Сочи олимпиада
    Супер, И что бы мы делали без вашей блестящей идеи
    ответить
  • 4:11, 12 Декабря 2010 — effective avafx
    Поздравляю, вас посетила отличная мысль
    ответить
  • 16:56, 12 Декабря 2010 — Максим
    Иван, сделал обьединение скриптов и стилей в один файл по рекомендациям. Выдает ошибку
    Fatal error: Uncaught exception 'Zend_Loader_PluginLoader_Exception' with message 'Plugin by name 'MagicHeadScript' was not found in the registry;
    ответить
    • 19:23, 14 Декабря 2010 — Иван
      Максим, видимо, у вас что-то не так с путями и Zend не может найти плагин MagicHeadScript. Советую вам разобраться с подключением помощников и плагинов. Про них все очень хорошо расписано в официальном руководстве: http://framework.zend.com/manual/ru/zend.view.helpers.html
      ответить
  • 17:44, 2 Марта 2011 — Max
    "... кэша происходит мгновенно, и к тому же не идет нагрузка на сервер — браузер к нему вообще не обращается, а берет все ресурсы у самого себяы. ..."

    Вы "ы" породили - вы ее и убейте!)
    ответить
    • 22:41, 2 Марта 2011 — Иван
      Спасибо за замечание! Убил "ы" ;-)
      ответить
  • 22:25, 12 Апреля 2011 — andrey
    c https://github.com/bubba-h57/zf-helpers сравнивали? В https://github.com/bubba-h57/zf-helpers больше классов в комплекте идёт. Нужны ли они? Какой предпочтительнее вариант - этот или ваш?
    ответить
    • 15:29, 13 Апреля 2011 — Иван
      Нет, не сравнивал, так как статью писал 17 мая 2010 г., а на github, как я понял, эти хелперы появились в феврале 2011.
      ответить
  • 19:09, 14 Апреля 2011 — andrey
    Как выделить tinymce или любую другую группу скриптов в отдельный файл? Сейчас при добавлении на одной определённой странице одного скрипта, перекешируется весь js.
    ответить
    • 15:15, 19 Апреля 2011 — Иван
      Сейчас хелпер не имеет такой возможности. Если вам надо выделить какую-то группу скриптов отдельно, то можете просто напрямую добавить ее в код макета, предварительно сжав. А различные яваскриптовые библиотеки, обычно, уже заранее сжаты.
      ответить
  • 17:26, 7 Августа 2013 — Коля
    Здесь ’news’ — это уникальная метка для кэша (в предыдущем примере мы использовали в качестве нее урл страницы). Таким образом, скрипт смотрит, есть ли в кэше данные с идентификатором «news». Если есть — сразу присваивает их переменной $this->view->newsArray, а если нету — делает запрос к модели.

    не понятно как указать $cache->save($newsArray); ключ news
    ответить
Написать комментарий