Статьи

Автор: Миндубаев Андрей aka Covex
Опубликована: 9 сентября 2008

Клиентская оптимизация и этапы разработки

Обычно пользователю нет дела до того, какие подходы мы применяем при разработке, как настроен сервер, какие клиентские и серверные фреймверки мы используем. Его может волновать на сколько сайт полезный, удобный и быстрый. Наша же задача заключается в том, чтобы не доставлять пользователю неудобства, радовать его, и тем самым заставлять его покупать наш мега-продукт или смотреть на наши замечательные баннеры. Эта статья о том, как создавать быстрые сайты.

Как оценить производительность сайта?

Я пользовался только двумя сервисами для получения хоть какой-то формализованной и достоверной оценки: это плагин для 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.

M => C => [V == S] => P => B

Я считаю, что необходимо рассматривать все 5 частей техпроцесса, как единое целое. В этом процессе все части проектируются для остальных, последующих за ними; а ошибки их проектирования и реализации приведут к временным потерям на следующих частях и к медленной или неправильной работе конечного результата.

Для минимизации вероятности ошибок, необходимо, чтобы каждая из частей техпроцесса выполняла только свои задачи. Например, в структуре документа не должно содержатся аттрибутов, выполняющих функции оформления или поведения: style, onclick, onmouseover; весь CSS и JavaScript должны быть вынесены во внешние файлы.

Разделение труда

Для внедрения такой схемы можно выделить следующие специализации:

  • M => C — проектировщик БД, разработчик модулей системы
  • C => [V == S] — разработка CMS, автоматизация работы с шаблонами
  • [V == S] => P — верстка и оформление
  • [V == S] => B => P — JavaScript (тут я не знаю как назвать специализацию)

Это примерно 3 человека (админов, дизайнеров, менеджеров и директоров я не считаю =)

Итак,

Этап 1: Доставка информации и оформления

На этом этапе разработчики должны сделать все возможное, чтобы не замедлить скорость загрузки страницы.

Способы ускорения получения информации

Уменьшение количества HTTP-запросов, Сжатие HTML и CSS, Размещение статичных компонент страницы на нескольких доменах, Уменьшение количества DOM-элементов, Размещение CSS в HEAD страницы, Настройка HTTP-заголовков (Expires, ETag, Set-Cookie), Уменьшение количества DNS запросов

Как можно испортить первое впечатление от сайта

Загрузка и/или использование JavaScript в процессе загрузки информации, Использование iframe-ов на странице

Я расположил пункты в порядке убывания важности. Все пункты предельно понятны. Я хочу остановится только на одном моменте (про сжатие и кэширование я напишу ниже)

Размещение CSS в HEAD странице: на странице должен быть только один внешний CSS-файл

Невыполнение этого требования приведет к задержке отображения страницы, т.к. 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".

Этап 2: Кэширование файлов оформления, сжатие HTML, CSS и JS

На данном этапе разработчики должны обеспечить быструю загрузку других страниц сайта (если посетитель решит туда перейти). Этот этап должен проходить параллельно с первым.

Для решения задач кэширования и сжатия я рекомендую использовать только nginx, а Апачу останется только генерация динамических страниц и сборка CSS и JS файлов. Приведу конфигурацию nginx, которую я подготовил «по мотивам» нашей уже работающей системы:

Основной домен проекта (www.site.org)

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-страницы считаются динамическими, поэтому они не кэшируются.

Домен для интерфейсных картинок (img.site.net)

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-спрайта нам потребуется добавить еще одну иконку.

Домен для CSS- и JS-файлов (static.site.net)

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).

Этап 3: Жизнь после загрузки страницы

Цель данного этапа это — навешивание мышиных событий на различные DOM-элементы и реализация прочих Web-два-нольных прибамбасов, т.е. оживление страницы.

Говорят, что иногда «грамм видимости важнее килограмма сути» — это как раз про JS. Ведь именно на JavaScript можно реализовать механизмы, упрощающие действия пользователя; можно сделать много различных визуальных эффектов, подчеркивающих оформление, удобство и полезность сайта (а фактически всю работу, которую проделали разработчики на предыдущих этапах).

К этому моменту мы должны иметь оформленную HTML-страницу, на которой все ссылки и формы обязаны работать без JavaScript-а. Должны быть готовы серверные интерфейсы для Ajax-овых запросов; структура страницы должна такой, чтобы для аналогичных кусков HTML не приходилось реализовывать аналогичные, но не одинаковые куски JS-кода. Скорее всего должны быть созданы шаблоны страниц, где видно как будет выглядеть страница после какого-то действия пользователя.

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

Чтобы не уменьшать скорость доставки контента и оформления, JS-файлы (лучше всего, конечно, один JS-файл) должны быть подключены перед закрытием тэга body. Жизнь начинается после загрузки HTML-кода:

Задача сводится к выполнению следующих действий:

  1. найти DOM-элементы, требующие «оживления» (далее — компоненты);
  2. определить, что это за компонент;
  3. обеспечить подключение необходимого кода JavaScipt;
  4. следить за очередностью подключения файлов;
  5. не позволять нескольких загрузок одного файла.

Если вы дочитали до этого места ;) то вам лучше всего продолжить чтение на странице Модульность в 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) — в этом случае потребуется разбить один запрос на несколько последовательных.

YSlow: Performance Grade: A (100)

Сначала именно так я хотел назвать статью, но затем она выросла в нечто большее, чем просто «оценка производительности сайта». Теперь я уверен, что такую оценку могут получить только страницы без рекламных баннеров и счетчиков или дефолтные страницы 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)

Это не просто цифры — это вероятность того, что когда-нибудь кто-нибудь другой реализует свою идею, раскрутит проект и отберет вашу аудиторию, какими бы популярными сейчас ни были Хабр и ЖЖ.

Спасибо за внимание.

Читать дальше

Все комментарии (habrahabr.ru)