Обычно пользователю нет дела до того, какие подходы мы применяем при разработке, как настроен сервер, какие клиентские и серверные фреймверки мы используем. Его может волновать на сколько сайт полезный, удобный и быстрый. Наша же задача заключается в том, чтобы не доставлять пользователю неудобства, радовать его, и тем самым заставлять его покупать наш мега-продукт или смотреть на наши замечательные баннеры. Эта статья о том, как создавать быстрые сайты.
Я пользовался только двумя сервисами для получения хоть какой-то формализованной и достоверной оценки: это плагин для FireBug «YSlow» и сайт webo.in (уверен, что подобных сервисов гораздо больше).
Оценка качества доставки контента с сервера клиенту, которую выдает YSlow — это сводный показатель, простой и понятный: «Performance Grade A» — все хорошо, «Performance Grade F» — все плохо. Эта оценка формируется из 13 других субпараметров, еще более простых, описанных на английском на сайте developer.yahoo.com
Результат работы сервиса webo.in — это две оценки, основанные на анализе следования разработчиками советам владельца сервиса и объема представленной на сайте информации и список рекомендаций для дальнейшей оптимизации. Плюс к тому на сайте есть большое количество статей по данной тематике.
Для получения полной картины, я бы посоветовал использовать оба способа оценки. Но нужно учитывать, что ощущения пользователя и автоматически сформированная оценка могут сильно отличаться.
Мы посещаем сайты ради информации и чем быстрее мы ее получим в читаемом виде, тем выше будет наше мнение о сайте. Таким образом, одна из первоочередных задач разработчика — это минимизация времени получения оформленной информации и исключение всего, что может отложить этот момент.
Фактически, я уже описал цель первого этапа разработки (серверная часть входит в этот первый этап =) Но до его подробного описания я бы хотел остановится на одном важном, по моему мнению, моменте.
На пути к оптимальному результату есть некоторое количество проблем, в основном — организационных, а техническая же сторона вопроса всегда легко формализуема, а значит — решаема.
Давайте выделим из всего технологического процесса «от креатива и ТЗ до начала эксплуатации», собственно, сам процесс разработки «от базы данных до полной загрузки страницы». Для серверных разработок разработан не один подход, метод или паттерн: возьмем, к примеру, MVC. Клиентская разработка это: HTML — структура (Structure), CSS — внешний вид (Presentation), JavaScript — поведение (Behavior). Итого получилось 6 частей: Model, View, Controller, Structure, Presentation, Behavior, из которых две — совпадают: серверное View — это клиентское Structure.
Я считаю, что необходимо рассматривать все 5 частей техпроцесса, как единое целое. В этом процессе все части проектируются для остальных, последующих за ними; а ошибки их проектирования и реализации приведут к временным потерям на следующих частях и к медленной или неправильной работе конечного результата.
Для минимизации вероятности ошибок, необходимо, чтобы каждая из частей техпроцесса выполняла только свои задачи. Например, в структуре документа не должно содержатся аттрибутов, выполняющих функции оформления или поведения:
style
,onclick
,onmouseover
; весь CSS и JavaScript должны быть вынесены во внешние файлы.
Для внедрения такой схемы можно выделить следующие специализации:
- M => C — проектировщик БД, разработчик модулей системы
- C => [V == S] — разработка CMS, автоматизация работы с шаблонами
- [V == S] => P — верстка и оформление
- [V == S] => B => P — JavaScript (тут я не знаю как назвать специализацию)
Это примерно 3 человека (админов, дизайнеров, менеджеров и директоров я не считаю =)
Итак,
На этом этапе разработчики должны сделать все возможное, чтобы не замедлить скорость загрузки страницы.
Уменьшение количества HTTP-запросов, Сжатие HTML и CSS, Размещение статичных компонент страницы на нескольких доменах, Уменьшение количества DOM-элементов, Размещение CSS в HEAD страницы, Настройка HTTP-заголовков (Expires, ETag, Set-Cookie), Уменьшение количества DNS запросов
Загрузка и/или использование JavaScript в процессе загрузки информации, Использование iframe-ов на странице
Я расположил пункты в порядке убывания важности. Все пункты предельно понятны. Я хочу остановится только на одном моменте (про сжатие и кэширование я напишу ниже)
Невыполнение этого требования приведет к задержке отображения страницы, т.к. CSS-файлы загружаются последовательно + на каждый HTTP-запрос уходит дополнительное время. Важность данного требования измеряется временем отклика сервера и скоростью доступа в интернет у клиента.
Требование, фактически, сводится к объединению всех CSS-файлов в один. Есть два варианта реализации: ручной и автоматический. Ручной способ выполним только в простых проектах, либо в проектах с однотипными страницами. Во всех остальных случаях сыграет роль человеческий фактор и люди просто не будут заниматься этой ерундой каждый раз после любого изменения. Результатом бездумного объединения вообще всех файлов могут стать десятки килобайт CSS-правил, из которых на каждой отдельно взятой странице использоваться будет лишь малая часть.
Идею (но не реализацию!!!) для автоматического варианта решения можно подчерпнуть здесь. Суть идеи в синтаксе запроса: для двух файлов /styles/a.css и /styles/b.css может быть сформирован один запрос вида /styles/a.css; b.css. Если такой файл уже есть на сервере, его можно отдать nginx-ом, иначе запрос должен быть проброшен дальше в backend, чтобы создать этот файл для следующих обращений к нему.
С таким подходом можно физически разделить CSS-правила на общие (требуемые на всех страницах) и частные (требуемые только на «нестандартных» страницах), и, при необходимости, соединять их.
Итог первого этапа — это доставленный и оформленный HTML. Нет никакого JavaScript (на этапе доставки основного контента он только мешает). Время от начала до завершения загрузки такой страницы при включенном и выключенном JS фактически будет одинаковым. Это и будет выигрышем в скорости загрузки!
Если выполнить требование «Уменьшение количества DOM-элементов» более-менее близко к идеальному, то побочным эффектом может стать возможность реализации мобильной версии сайта и версии для печати тем же самым HTML-кодом. Останется «всего лишь» реализовать наборы CSS правил для media="handheld"
и media="print"
.
На данном этапе разработчики должны обеспечить быструю загрузку других страниц сайта (если посетитель решит туда перейти). Этот этап должен проходить параллельно с первым.
Для решения задач кэширования и сжатия я рекомендую использовать только nginx, а Апачу останется только генерация динамических страниц и сборка CSS и JS файлов. Приведу конфигурацию nginx, которую я подготовил «по мотивам» нашей уже работающей системы:
server { listen 80; server_name www.site.org site.org; gzip on; gzip_min_length 1000; gzip_proxied expired no-cache no-store private auth; gzip_types text/plain application/xml; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /favicon.ico { expires max; root /home/project/img/; } location /s.gif { expires max; root /home/project/img/; } }
Здесь все запросы к www.site.com пробрасываются дальше в Apache (http://backend определяется директивой upstream. Исключения составляют только favicon.ico и прозрачный s.gif: они лежат в одном каталоге со всеми остальными интерфейсными картинками, а для броузера доступны как www.site.com/favicon.ico и www.site.com/s.gif. Такое исключение для s.gif сделано только для того, чтобы сократить размер html-кода.
Ответы апача с Content-Type
text/html
, text/plain
, application/xml
сжимаются на лету. Сжатие стоит отключить в случае, когда для проекта выделено небольшое количество ресурсов.
Все HTML-страницы считаются динамическими, поэтому они не кэшируются.
server { listen 80; server_name img.site.net; expires max; add_header Cache-Control public; location / { root /home/project/img/; } location ~ ^/\d+\.\d+\/.* { root /home/project/img/; rewrite (\d+\.\d+)/(.*) /$2 break; } }
Все интерфейсные картинки кэшируются навсегда. Пример: img.nnow.ru/interface/is.png
Для того, чтобы сбросить кэш броузеров, введено такое правило: если HTTP-запрос начинается со «слэш число точка число слэш», то картинки следует брать из корня. Пример: img.nnow.ru/2.0/interface/is.png
Это очень полезно, когда внутрь картинки-CSS-спрайта нам потребуется добавить еще одну иконку.
server { listen 80; server_name static.site.net; expires max; gzip_static on; location / { return 404; } location /jas/ { # javascript-and-stylesheets proxy_set_header Host $host; if (!-f $request_filename) { break; proxy_pass http://backend; } root /home/project/static/; } }
Принцип работы серверных механизмов по сборке CSS и JS был описан выше. Единственное, про что стоит отметить — это директива gzip_static. Эта директива поставляется с модулем ngx_http_gzip_static_module. Модуль позволяет отдавать вместо обычного файла предварительно сжатый файл с таким же именем и с суффиксом .gz
. По умолчанию модуль не собирается, нужно разрешить его сборку при конфигурировании параметром --with-http_gzip_static_module.
Сборщик файлов дополнительно должен уметь записывать файлы вида /jas/a.css; b.css.gz.
CSS и JS файлы кэшируются навсегда. Сброс кэша может быть реализован добавлением «версии файла» в название файла: /jas/ver2.0.css; a.css; b.css
NB! Мы реализовали сборщик таким образом, что файлы a.css и b.css в момент сборки включаются в конечный результат PHP-функцией include, т.е. фактически являются исполнимыми PHP-файлами. Это дает возможность избавится от хаков CSS или от анализа User-Agent броузера в JS:
При обращении к /jas/ver2.0.css; Firefox3.css; a.css; b.css Firefox3.css, запоминает в переменную PHP название и версию броузера, а последующие части составного файла-результата могут прочитать эту переменную и выдавать разное содержимое для разных браузеров. Например: "s.img.nnow.ru/jas/ver,1.0.js;Firefox3.js;habr,index.js" и "s.img.nnow.ru/jas/ver,1.1.js;IE6.js;habr,index.js"
Таким же образом меняется и «версия интерфейсной картинки» для img.nnow.ru/3.0/interface/logo_ni.gif (соответствующая переменная устанавливается в файле-версии CSS).
Цель данного этапа это — навешивание мышиных событий на различные DOM-элементы и реализация прочих Web-два-нольных прибамбасов, т.е. оживление страницы.
Говорят, что иногда «грамм видимости важнее килограмма сути» — это как раз про JS. Ведь именно на JavaScript можно реализовать механизмы, упрощающие действия пользователя; можно сделать много различных визуальных эффектов, подчеркивающих оформление, удобство и полезность сайта (а фактически всю работу, которую проделали разработчики на предыдущих этапах).
К этому моменту мы должны иметь оформленную HTML-страницу, на которой все ссылки и формы обязаны работать без JavaScript-а. Должны быть готовы серверные интерфейсы для Ajax-овых запросов; структура страницы должна такой, чтобы для аналогичных кусков HTML не приходилось реализовывать аналогичные, но не одинаковые куски JS-кода. Скорее всего должны быть созданы шаблоны страниц, где видно как будет выглядеть страница после какого-то действия пользователя.
По сути должен быть организован эффективный обмен информацией между всеми разработчиками. Как организовать такой диалог в команде, состоящей из десятков человек, я не знаю; но, для команды из трех-четырех человек это сделать вполне реально (хоть и сложно).
Чтобы не уменьшать скорость доставки контента и оформления, JS-файлы (лучше всего, конечно, один JS-файл) должны быть подключены перед закрытием тэга body. Жизнь начинается после загрузки HTML-кода:
Задача сводится к выполнению следующих действий:
- найти DOM-элементы, требующие «оживления» (далее — компоненты);
- определить, что это за компонент;
- обеспечить подключение необходимого кода JavaScipt;
- следить за очередностью подключения файлов;
- не позволять нескольких загрузок одного файла.
Если вы дочитали до этого места ;) то вам лучше всего продолжить чтение на странице Модульность в JavaScript, динамическая загрузка на сайте www.jsx.ru. Именно там я позаимствовал этот алгоритм. Свой вариант решения я лучше не буду рекламировать, т.к. фактически, в процессе выполнения задачи, был поставлен эксперимент, конечный результат которого ясен не на все 100% (пока, вроде бы все работает отлично =)
Расскажу о пунктах 3 и 4.
Поиск необходимых DOM-элементов должен нам дать список названий JS-компонент. Названия компонент должны однозначно соответствовать названиям файлов на сервере, в которых содержится код для них. Так же нам может понадобиться загрузить некоторые дополнительные CSS-правила для найденных компонент для каких-то визуальных эффектов, не необходимых на первом этапе загрузки контента и оформления.
Cписок названий компонент можно объединить в один запрос к серверу. В итоге после загрузки контента должны загрузиться файлы вида «static.site.net/jas/componentName1.css; componentName2.css» и «static.site.net/jas/componentName1.js; componentName2.js».
У данного подхода есть два недостатка: (1) в папке /jas/ через некоторое время может сгенериться очень много файлов, что теоретически может уменьшить время доступа к ним на сервере; (2) иногда на странице может оказаться очень много компонент, при чем так много, что длинна имени запрашиваемого объединенного файла перевалит за возможности файловой системы (например, 255 символов у Ext3) — в этом случае потребуется разбить один запрос на несколько последовательных.
Сначала именно так я хотел назвать статью, но затем она выросла в нечто большее, чем просто «оценка производительности сайта». Теперь я уверен, что такую оценку могут получить только страницы без рекламных баннеров и счетчиков или дефолтные страницы nginx или Apache с сообщением о том, что страница не найдена.
Невозможность достижения отличной оценки производительности не дает нам повод пренебрегать клиентской оптимизацией. Ведь шагВлево-шагВправо от оптимального пути может означать немедленный расстрел:
Главная страница моего блога на liveinternet.ru: Performance Grade: F (30)
Главная страница Хабра: Performace Grade F(38)
Главная страница моего блога на Я.ру: Performance Grade: F (42)
Пост в офицальном блоге Google: Performance Grade: F (56)
Главная страница моего блога в ЖЖ: Performance Grade: D (66)
Это не просто цифры — это вероятность того, что когда-нибудь кто-нибудь другой реализует свою идею, раскрутит проект и отберет вашу аудиторию, какими бы популярными сейчас ни были Хабр и ЖЖ.
Спасибо за внимание.