[Перевод] Лучшие методики проектирования производительных мобильных API

Habrahabr 4

В сети есть множество информативных статей о высокой производительности на мобильных устройствах, и столько же об общем проектировании API. Но очень мало обсуждаются архитектурные решения, необходимые для оптимизации производительности бэкендных API, предназначенных для использования мобильными клиентами. Безусловно, необходимо оптимизировать производительность самих мобильных приложений. Но и мы, инфраструктурные инженеры, многое можем сделать для того, чтобы мобильные клиенты надёжно и быстро обеспечивались данными и программными ресурсами — тем самым поддерживая положительный опыт использования мобильных приложений.

Вот что нужно учитывать при проектировании мобильного ПО:

  • Ограниченный размер экрана. Мало место для данных, мелкие изображения.
  • Меньше одновременных подключений. Это важно, поскольку в отличие от настольных браузеров, способных выполнять много одновременных асинхронных запросов, у мобильных браузеров ограниченное количество подключений к одному домену.
  • Сеть медленнее. На производительность сети очень сильно влияет общий уровень приёма сигнала, обслуживание многочисленных абонентов (и хотя кто-то из них сидит на Wi-Fi, некоторые сети становятся перегруженными и выполняют дополнительные операции поиска, если пользователь подключается к другой базовой станции).
  • Ниже вычислительная мощность. Интенсивные клиентские вычисления, отрисовка 3D и активное использование JavaScript могут сильно уменьшить производительность.
  • Меньше кэши. Мобильные клиенты в целом ограничены по памяти, так что ради повышения производительности лучше не слишком полагаться на закэшированный контент.
  • «Особые» браузеры. Экосистема мобильных браузеров во многом напоминает фрагментированную среду настольных браузеров несколько лет назад, когда разработчики выпускали всё новые версии с фатальными недоработками и несовместимостями.
Существует много способов решения описанных затруднений, но эта статья по большей части посвящена тому, что можно сделать с API или бэкендом для улучшения производительности (или её восприятия) мобильных клиентов. Мы рассмотрим два основных вопроса:
  1. Минимизация сетевых соединений и потребности в передачи данных. Эффективная обработка мультимедиа, эффективное кэширование и использование более длительных операций, ориентированных на обработку данных и с меньшим количеством подключений.
  2. Отправление по сети «правильных» данных. Проектирование таких API, которые только возвращают нужные/запрошенные данные, а также оптимизация под разные типы мобильных устройств.
Хотя статья посвящена мобильному сегменту, многие уроки и идеи можно применять и к API-клиентам в других сферах.

Минимизация подключения и передачи данных по сети

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

Изображения

Если делать по одному запросу на каждое изображение на странице, то вы сможете улучшить скорость и воспользоваться преимуществами кэширования отдельных картинок. Настольный браузер способен обрабатывать запросы быстро и параллельно, так что их количество не сильно снижает производительность (а благодаря кэшированию запас «прочности» становится ещё выше). Однако на мобильных устройствах большое количество запросов может стать смертельным.

Минимизация обращений за изображениями может снизить общее количество запросов, а в каких-то случаях — и объём передаваемых данных (что тоже благоприятно сказывается на производительности). Каких стратегий можно придерживаться?

Спрайты

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

Применение CSS вместо изображений

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

Поддержка адаптивных изображений

Адаптивные изображения — распространённый способ доставки правильной картинки на соответствующее устройство. Apple делает это, загружая обычные изображения, а потом с помощью JavaScript заменяя их на картинки с более высоким разрешением. Есть и ряд других подходов, но мы ещё далеки от решения этой задачи.

Для использования адаптивных изображений удостоверьтесь в том, что они поддерживаются на сервере, и что API поддерживают разные версии одного изображения. А конкретная реализация будет зависеть от клиентских решений.

Уменьшение дополнительных запросов с помощью использования URI данных для инлайнинга изображений

Альтернатива спрайтам — использование URI данных для инлайнинга изображений в HTML. В результате картинки становятся частью всей страницы. И хотя их размер в байтах может увеличиться, но такие изображения лучше сжимаются с помощью Gzip, что компенсирует увеличение объёма передаваемой информации.

Совет: если используете URI, то:

  • Уменьшайте картинки до нужного размера прежде, чем встраивать в URI.
  • Проверьте, чтобы Gzip сжимал ответы на запросы (чтобы воспользоваться преимуществами сжатия).
  • Обратите внимание, что встроенные в URI картинки являются частью CSS страницы, а значит кэшировать отдельные изображения будет труднее. Поэтому избегайте инлайнинга, если есть веские причины для локального кэширования изображений (например, если они часто используются на разных страницах).

Использование локального хранилища и кэширования

Поскольку мобильные сети могут работать медленно, HTML, CSS и картинки можно хранить в локальном хранилище (localStorage). Вот отличное исследование по улучшению производительности Bing с помощью локального хранилища, при использовании которого удалось снизить размер HTML-документа с ≈200 Кб до ≈30 Кб.

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

Совет: данные, которые вряд ли будут изменяться при обновлениях приложения (например, категории или основная навигация), лучше поставлять внутри приложения, чтобы не тратить время и ресурсы на передачу по сети.

В идеале, данные нужно передавать по мере необходимости, и заранее подгружать, когда это оправдано. Если пользователь не увидит изображение или контент, то и не отправляйте его (это особенно важно для адаптивных сайтов, поскольку некоторые просто «прячут» какие-то элементы). Отличное применение для предварительной подготовки изображений — галерея поисковых результатов. Лучше сразу подгружать следующее и предыдущее изображения, чтобы ускорить работу интерфейса. Но не увлекайтесь, не надо подгружать слишком много картинок, потому что пользователь может их даже не посмотреть.

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

Совет: если встраивать CSS и JavaScript прямо в отдельный запрос, потом сохранять ссылки на эти файлы и передавать их серверу посредством куки, то клиенту не придётся заново скачивать эти ресурсы (по сети будут передаваться только новые файлы). Это даст большую экономию времени и является отличным инструментом для использования локального кэширования. Подробнее о том, как напрямую встраивать и ссылаться на упомянутые файлы, написано здесь: http://calendar.perfplanet.com/2011/mobile-ui-performance-considerations/.

Неблокирующий ввод-вывод

Когда речь заходит о клиентской оптимизации, рекомендуется следить за блокирующим JavaScript-исполнением, которое может сильно ухудшать производительность. Но для API это ещё важнее. Если у вас есть длительный вызов API, к примеру, с обращением к стороннему ресурсу, способным завершиться таймаутом, то важно реализовать вызов как неблокирующий (или даже сделать долго ждущим) и выбрать либо polling-, либо triggering-модель.
  • Polling (pull-модель): в polling-API клиент делает запрос, а затем периодически проверяет наличие результата, при необходимости снижая частоту проверок.
  • Triggering (push-модель): в trigger-API вызов генерирует запрос, а затем слушает в ожидании ответа сервера. Тот предоставляет коллбек, который инициирует событие, благодаря которому вызывающий узнаёт о появлении результата запроса.
Обычно triggering API труднее реализовать, поскольку мобильные клиенты ненадёжны. Так что чаще всего лучше использовать polling-модель.

К примеру, в мобильном приложении Decide на страницах продуктов отображались местные цены для стран, в которых эти товары были доступны. Поскольку результаты предоставлялись сторонним сервисом, с помощью polling API мы смогли делать запросы, а потом получать результаты, не останавливая работу приложения и не поддерживая открытое подключение в ожидании результатов.

Проверяйте, чтобы ваши API отвечали быстро и не блокировали выполнение в ожидании результатов, поскольку у мобильных клиентов ограниченное количество подключений.

Совет: избегайте «болтливых» API. В случае медленной сети нужно избегать нескольких вызовов API. Хорошее практическое правило: все данные, необходимые для отрисовки возвращаемой страницы, помещать в один вызов API.

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

Избегание редиректов и минимизация DNS-запросов

Что касается запросов, то редиректы могут ухудшать производительность, особенно если это редиректы между доменами, которые требуют выполнения DNS-запроса.

К примеру, многие сайты работают со своими мобильными версиями с помощью клиентских редиректов. То есть, когда мобильный клиент обращается к URL основного сайта (например, katemats.com), тот перенаправляет на мобильный сайт m.katemats.com (это сплошь и рядом встречается там, где сайты построены на разных стеках технологий). Вот пример подобной схемы:

  1. Пользователь гуглит по запросу «yahoo» и кликает на первую ссылку в выдаче.
  2. Google захватывает клик с помощью своего URL отслеживания, а затем перенаправляет на www.yahoo.com [редирект]
  3. Ответ на редирект Google проходит через базовую станцию мобильного оператора, а затем попадает на клиентский телефон.
  4. Выполняется DNS-запрос для www.yahoo.com.
  5. Найденный IP передаётся через БС на телефон.
  6. Когда телефон обращается к www.yahoo.com, его распознают как мобильный клиент и перенаправляют на m.yahoo.com [редирект]
  7. Затем телефону снова нужно выполнить DNS-запрос, на этот раз для поддомена m.yahoo.com.
  8. Найденный IP передаётся через БС на телефон.
  9. Наконец, финальный HTML и необходимые ресурсы передаются через БС на телефон.
  10. Некоторые картинки на страницах мобильного сайта предоставляются через CDN по ссылкам на другой домен, скажем, l2.yimg.com.
  11. Телефон снова выполняет DNS-запрос — для поддомена l2.yimg.com.
  12. Найденный IP передаётся через БС на телефон.
  13. Картинки отрисованы, страница готова.
Как видите, здесь куча накладных расходов, которых можно избежать, используя редиректы на серверной стороне (то есть маршрутизируя через сервер и минимизируя количество DNS-запросов и редиректов на клиенте), либо используя адаптивные методики.

Совет: если нельзя избежать DNS-запроса, то для экономии времени попробуйте использовать предварительный DNS-запрос для известных доменов.

Конвейерная обработка HTTP и SPDY

Другая полезная методика — конвейерная обработка HTTP. Она позволяет объединять несколько запросов в один. Хотя я выбрала бы SPDY, который оптимизирует HTTP-запросы, чтобы они были гораздо эффективнее. Этот протокол поддерживается в браузере Amazon Kindle, Twitter и Google.

Отправка «правильных» данных

Разным клиентам требуется отправлять разные файлы, CSS и JavaScript. Даже количество результатов может меняться. Проектирование API, поддерживающих разные комбинации и версии результатов и файлов, даст вам максимальную гибкость при создании замечательного опыта взаимодействия.

Для получения результатов используйте limit и offset

Как и в случае с обычными API, извлечение результатов с помощью limit и offset позволяет клиентам запрашивать различные данные, необходимые для конкретного использования (для мобильных клиентов результатов будет меньше). Я предпочитаю нотацию limit и offset, поскольку она больше распространена (чем, скажем, start и next), хорошо понимается многими базами данных, а значит и легка в использовании.

/products?limit=25&offset=75

Выберите значение по умолчанию, соответствующее самому большому или маленькому общему кратному (common denominator), с помощью которого можно выделить наиболее важных для вашего бизнеса клиентов (меньше — если основу вашей аудитории составляют мобильные клиенты, больше — если к вам заходят, в основном, с настольных компьютеров, обычно это верно для В2В-сайтов и сервисов).

Поддержка частичного ответа и частичного обновления

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

Частичное обновление позволяет клиентам делать всё то же самое с данными, которые они пишут в API (и тогда не нужно определять все элементы в рамках классификации ресурса). Google поддерживает частичный ответ с помощью дополнительных опциональных полей в виде списка с запятыми-разделителями:

http://www.gоogle.com/calendar/feeds/[emаil prоtected]/private/full?fields=entry(title,gd:when)

Если для вызова задано entry, это означает, что вызывающий запрашивает только частичный набор полей.

Избегайте или сведите к минимуму использование кук

Каждый раз, когда клиент отправляет домену запрос, в него включаются все куки, полученные от этого домена — даже продублированные или посторонние значения. Поэтому ещё один способ уменьшить объём передаваемых данных и улучшить производительность — поддерживать небольшой размер кук (и не запрашивать их, если в них нет необходимости). Без нужды не используйте и не требуйте куки. Предоставляйте статичный контент, которому не нужны разрешения от домена без кук (например, изображения от статичного домена или из CDN). Вот описание некоторых методов работы с куками: https://developers.google.com/speed/docs/best-practices/request.

Создание профилей устройств для API

Учитывая разнообразие размеров и разрешений экранов у настольных компьютеров, планшетов и смартфонов, полезно создать профили, которые вы будете поддерживать. Для каждого профиля можно предоставлять разные изображения, данные и файлы, подходящие для конкретных устройств. Делается это с помощью медиа-запросов к клиенту.

Чем больше профилей, тем лучше опыт взаимодействия применительно к определённому устройству. Но тогда труднее будет сопровождать всевозможные поддерживаемые функции и сценарии (потому что устройства постоянно меняются и развиваются). Так что лучше поддерживать лишь абсолютно необходимое количество профилей. Что касается компромиссов и возможностей по созданию хорошего опыта взаимодействия, то рекомендую почитать эту статью: https://mobiforge.com/design-development/effective-design-multiple-screen-sizes.

Для большинства приложений достаточно трёх профилей:

  1. Мобильные телефоны: изображения меньше, доступно управление касаниями, низкая пропускная способность сети.
  2. Планшеты: изображения больше, но адаптированы для сети с низкой пропускной способностью, доступно управление касаниями, в запросе передаётся больше данных.
  3. Настольные компьютеры: изображения для планшетов и изображения с более высоким разрешением для Wi-Fi и настольных браузеров.
Нужный профиль можно выбирать на клиенте. API серверной части должны быть спроектированы так, чтобы брать эти профили и отправлять разную информацию в зависимости от того, какое устройство прислало запрос. Например, будут отправляться изображения меньше размера, или размер результатов будет уменьшен, или CSS и JavaScript будут инлайниться.

Допустим, если один из ваших API возвращает поисковые результаты, то профили могут быть такими:

/products?limit=25&offset=0

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

/products?profile=mobile&limit=10&offset=0

Возвращается 10 результатов, картинки с низким разрешением передаются в виде URI в одном HTTP-запросе.

/products?profile=tablet&limit=25&offset=0

Возвращается 20 результатов, картинки с низким разрешением, но крупнее размером передаются в виде URI в одном HTTP-запросе.

Вы даже можете создать профили для таких гаджетов, как фичефоны (feature phones). Они, в отличие от смартфонов, позволяют кэшировать файлы только постранично. Так что для таких клиентов лучше использовать профили, чем отправлять в каждом запросе CSS и JavaScript.

Рекомендуется вместо частичных ответов использовать профили, если ответы сервера сильно различаются в зависимости от профиля. Например, если в одном случае ответ содержит инлайненные URI-изображения и компактный макет, а в другом случае — уже нет. Конечно, профили можно задавать и с помощью «частичных ответов», хотя обычно они используются для определения части (или порции) стандартной схемы (например, подмножество более крупной классификации), а не всего другого набора данных, форматов и пр.

В заключение

Есть много способов сделать веб быстрее, в том числе и на мобильных устройствах. Надеюсь, эта статья будет полезным руководством для разработчиков API, которые проектируют серверные части для работы с мобильными клиентами.