Ни для кого не секрет, что самым узким местом веб-приложений чаще всего являются HTTP-запросы к внешним серверам. Так, время загрузки данных запроса API много больше чем время, необходимое для выполнения большинства самых сложных скриптов веб-приложения.
За время работы с API Facebook я накопил несколько рецептов оптимизации запросов: как увеличить скорость работы скриптов, уменьшить их количество и ресурсоёмкость.

Способы, изложенные в этой статье, работают только с API Facebook. Но я не исключаю, что они могут быть применимы и в других сервисах, предоставляющих API.
Наверное, у вас есть идея веб-приложения, которое работает с данными пользователя и его социального графа Facebook. И вы уже не раз открывали документацию и даже успели написать пару скриптов. Пусть это будет приложение “подарок на день рождения”, и первое что вы должны сделать — получить список друзей пользователя и их даты рождения.
Все просто, думаете вы:
Так как ваш скрипт работает на стороне сервера, вы должны получить все данные до их вывода пользователю. И тут вы сталкиваетесь с первой и самой важной проблемой: один запрос к API занимает 1-2 секунды. Так, чтобы только получить список всех друзей вам понадобится 2-4 секунды (в зависимости от количества друзей).
Попробуем посчитать, сколько времени будет выполняться скрипт который выводит дни рождения 10-и друзей.
Мне понадобилось 13 секунд для этого! Будет ли ждать ваш пользователь столько времени? Не думаю. А если количество друзей измеряется сотнями, или даже пару тысяч…
Давайте начнем оптимизацию.
Когда вы запрашиваете данные пользователя (объект User в Graph API), Facebook по умолчанию передает вам все поля, к которым у вас есть доступ. Вы можете избавиться от избыточной информации, указав какие поля вам нужно вернуть. Для этого в запросе используется параметр fields, в котором необходимые поля перечисляются через запятую. Например, для нашего случая это будет запрос: {user_id}?fields=id,name,birthday

Запросы с параметром fields работают быстрее, так как возвращают только нужные данные. Они быстрее передаются по сети, так как имеют меньший размер. Обрабатываются они тоже быстрее, и используют меньше памяти.
Хотя на общее время выполнения этот способ оптимизации в данной программе практически не влияет, но именно его никогда не стоит забывать. Читайте только необходимые поля для каждого запроса!
Используя параметр ids вы можете выбрать несколько объектов, которые хотите получить. При этом вы можете использовать и параметр fields чтобы ограничить только нужные поля объектов (объекты должны быть одного типа). Например, запрос ?ids=4,501012028 позволит получить открытые данные сразу двух пользователей ;)
Количество объектов, которые вы можете указать через запятую, ограничено только максимальной длинной URL. При этом не забывайте, что размер ответа на запрос тоже возрастает. Поэтому разумно ограничивайте количество объектов в одном запросе. Например, вот так выглядит ответ, если использовать первый и второй способ.

Чтобы использовать этот способ в нашем примере, нужно переписать скрипт.
Теперь вместо 10 запросов мы используем всего один, соответственно время работы скрипта уменьшилось до 4-5 секунд. Но нам все еще нужно получить данные всех друзей, а не только 10-и.
Пагинация — это всего лишь разбиение информации на “страницы”. С помощью параметра limit мы получали информацию только о 10 друзьях. Чтобы получить такими же порциями информацию об остальных, существует параметр offset. Например, первая десятка друзей: me/friends?limit=10&offset=0, вторая десятка друзей: me/friends?limit=10&offset=10, и т.д.
Вы можете оптимизировать работу скриптов следующим образом:
Теперь для вывода информации вам не нужно ждать пока обработаются все части запросов. При этом для первой порции у вас уже будет получено одним запросом достаточный объем данных.
Перейдем к более серьезным способам, позволяющим за один запрос получить в десятки раз больше данных (а может и все одним разом).
FQL дает вам возможность использовать SQL-style интерфейс для запросов к данным. С его помощью можно проще реализовать некоторые запросы, не доступные через Graph API. Форма запроса следующая: SELECT [fields] FROM [table] WHERE [conditions]. Но FQL имеет много ограничений (если сравнить его с SQL). Так, например, в FROM можно использовать только одну таблицу. Но вы можете использовать вложенные запросы. В FQL можно использовать логические операторы, конструкции ORDER BY и LIMIT и некоторые другие операторы.
Чтобы получить имя и дату рождения пользователя, нужно сделать FQL запрос к таблице user. Например, в Graph API Explorer это будет запрос fql?q=SELECT uid, name, birthday_date FROM user WHERE uid = me(). А для того, чтобы получить список id своих друзей, используем запрос к таблице friend fql?q=SELECT uid2 FROM friend WHERE uid1 = me().
Давайте попробуем объединить два запроса в один, используя вложенный запрос. Все просто: fql?q=SELECT uid, name, birthday_date FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = me()). Вместо десятков запросов мы получили всю необходимую нам информацию всего за один!

Способов оптимизации запросов FQL довольно много. Изучите все таблицы в документации. Но FQL это не вершина оптимизации запросов.
Представьте, что вам необходимо получить список последних постов пользователя, его ленту новостей, все непрочитанные уведомления и новые сообщения. И все это, и многое другое, нужно сделать быстро. Можно, конечно, распараллелить запросы, сделав их ассинхронными. Но это не наш способ.
С помощью Graph API вы можете обрабатывать данные всего одного запроса, даже если это FQL-запрос. Batch Request позволяет отправить на сервер в одном запросе — пачку из нескольких разных запросов. Единственное ограничение — до 20-и запросов в одном вызове batch. Запросы могут быть GET, POST и DELETE.
Например, вот такой “пакет” запросов:
Batch запросы должны отправляться методом POST, поэтому в коде выше задействован cURL. Также с его помощью проще работать с возможными ошибками.
Все способы применения Batch Requests не описать в данной статье. Вы можете узнать о них больше в документации.
За время работы с API Facebook я накопил несколько рецептов оптимизации запросов: как увеличить скорость работы скриптов, уменьшить их количество и ресурсоёмкость.

Способы, изложенные в этой статье, работают только с API Facebook. Но я не исключаю, что они могут быть применимы и в других сервисах, предоставляющих API.
Введение
Наверное, у вас есть идея веб-приложения, которое работает с данными пользователя и его социального графа Facebook. И вы уже не раз открывали документацию и даже успели написать пару скриптов. Пусть это будет приложение “подарок на день рождения”, и первое что вы должны сделать — получить список друзей пользователя и их даты рождения.
Все просто, думаете вы:
- авторизую пользователя (получаю id пользователя и access_token, которые использую в дальнейших запросах);
- получаю список его друзей (на выходе имею массив с id и name друзей);
- для каждого id делаю запрос данных пользователя (чтобы получить данные birthday, при авторизации нужно запросить friends_birthday permission).
Так как ваш скрипт работает на стороне сервера, вы должны получить все данные до их вывода пользователю. И тут вы сталкиваетесь с первой и самой важной проблемой: один запрос к API занимает 1-2 секунды. Так, чтобы только получить список всех друзей вам понадобится 2-4 секунды (в зависимости от количества друзей).
Попробуем посчитать, сколько времени будет выполняться скрипт который выводит дни рождения 10-и друзей.
<?php
$start = microtime(true);
$app_id = "YOUR_APP_ID";
$app_secret = "YOUR_APP_SECRET";
$my_url = "YOUR_URL";
session_start();
$code = $_REQUEST["code"];
if(empty($code)) {
$_SESSION['state'] = md5(uniqid(rand(), TRUE)); //CSRF protection
$dialog_url = 'https://www.facebook.com/dialog/oauth?client_id='
. $app_id . '&redirect_uri=' . urlencode($my_url) . '&scope=user_birthday,friends_birthday&state=' . $_SESSION['state'];
echo("<script> top.location.href='" . $dialog_url . "'</script>");
}
if($_REQUEST['state'] == $_SESSION['state']) {
$token_url = 'https://graph.facebook.com/oauth/access_token?'
. 'client_id=' . $app_id . '&redirect_uri=' . urlencode($my_url)
. '&client_secret=' . $app_secret . '&code=' . $code;
$response = file_get_contents($token_url);
$params = null;
parse_str($response, $params);
$graph_url = "https://graph.facebook.com/me?access_token=" . $params['access_token'];
$user = json_decode(file_get_contents($graph_url));
$uid = $user->id;
// получаем список друзей пользователя
$graph_url = 'https://graph.facebook.com/'.$uid.'/friends?limit=10&access_token=' . $params['access_token'];
$friends = json_decode(file_get_contents($graph_url));
$time = microtime(true) - $start;
printf('Авторизация и получение списка друзей %.4F сек.<br/>', $time);
$n = sizeof($friends->data);
for ($i = 0; $i < $n; $i++) {
$graph_url = 'https://graph.facebook.com/' . $friends->data[$i]->id . '?access_token=' . $params['access_token'];
$friend_data = file_get_contents($graph_url);
// декодировать json ответ и вывести данные
//$friend = json_decode($friend_data);
//echo($i.' '.$friend->name.' - '.$friend->birthday);
}
}
else {
echo("The state does not match. You may be a victim of CSRF.");
}
$time = microtime(true) - $start;
printf('Скрипт выполнялся %.4F сек.', $time);
Мне понадобилось 13 секунд для этого! Будет ли ждать ваш пользователь столько времени? Не думаю. А если количество друзей измеряется сотнями, или даже пару тысяч…
Давайте начнем оптимизацию.
Способ 1: Читаем только необходимые поля
Когда вы запрашиваете данные пользователя (объект User в Graph API), Facebook по умолчанию передает вам все поля, к которым у вас есть доступ. Вы можете избавиться от избыточной информации, указав какие поля вам нужно вернуть. Для этого в запросе используется параметр fields, в котором необходимые поля перечисляются через запятую. Например, для нашего случая это будет запрос: {user_id}?fields=id,name,birthday
- Разрешите необходимые permissions в Graph API Explorer, нажав Get Access Token и установив все флажки в User Data Permissions и Friends Data Permissions.
- Введите запрос me в поле, после https:graph.facebook.com/
- Сравните предыдущий вывод с запросом me?fields=id,name,birthday

Запросы с параметром fields работают быстрее, так как возвращают только нужные данные. Они быстрее передаются по сети, так как имеют меньший размер. Обрабатываются они тоже быстрее, и используют меньше памяти.
Хотя на общее время выполнения этот способ оптимизации в данной программе практически не влияет, но именно его никогда не стоит забывать. Читайте только необходимые поля для каждого запроса!
Способ 2: Запрашиваем данные нескольких объектов в одном запросе
Используя параметр ids вы можете выбрать несколько объектов, которые хотите получить. При этом вы можете использовать и параметр fields чтобы ограничить только нужные поля объектов (объекты должны быть одного типа). Например, запрос ?ids=4,501012028 позволит получить открытые данные сразу двух пользователей ;)
Количество объектов, которые вы можете указать через запятую, ограничено только максимальной длинной URL. При этом не забывайте, что размер ответа на запрос тоже возрастает. Поэтому разумно ограничивайте количество объектов в одном запросе. Например, вот так выглядит ответ, если использовать первый и второй способ.

Чтобы использовать этот способ в нашем примере, нужно переписать скрипт.
$n = sizeof($friends->data);
$graph_url = 'https://graph.facebook.com/?ids=';
for ($i = 0; $i < $n; $i++) {
$graph_url .= $friends->data[$i]->id . ',';
}
$graph_url = substr($graph_url, 0, -1);
$graph_url .= '&access_token=' . $params['access_token'];
$friend_data = file_get_contents($graph_url);
// декодировать json ответ и вывести данные
Теперь вместо 10 запросов мы используем всего один, соответственно время работы скрипта уменьшилось до 4-5 секунд. Но нам все еще нужно получить данные всех друзей, а не только 10-и.
Способ 3: Используем фильтрацию и пагинацию
Пагинация — это всего лишь разбиение информации на “страницы”. С помощью параметра limit мы получали информацию только о 10 друзьях. Чтобы получить такими же порциями информацию об остальных, существует параметр offset. Например, первая десятка друзей: me/friends?limit=10&offset=0, вторая десятка друзей: me/friends?limit=10&offset=10, и т.д.
Вы можете оптимизировать работу скриптов следующим образом:
- Пишите клиентскую часть с Ajax запросами к серверу.
- Получаете данные первых 10-и друзей и выводите их.
- Пока данные выводятся на экран, подгружаете следующую “порцию” данных.
Теперь для вывода информации вам не нужно ждать пока обработаются все части запросов. При этом для первой порции у вас уже будет получено одним запросом достаточный объем данных.
Перейдем к более серьезным способам, позволяющим за один запрос получить в десятки раз больше данных (а может и все одним разом).
Способ 4: Строим запросы FQL (Facebook Query Language)
FQL дает вам возможность использовать SQL-style интерфейс для запросов к данным. С его помощью можно проще реализовать некоторые запросы, не доступные через Graph API. Форма запроса следующая: SELECT [fields] FROM [table] WHERE [conditions]. Но FQL имеет много ограничений (если сравнить его с SQL). Так, например, в FROM можно использовать только одну таблицу. Но вы можете использовать вложенные запросы. В FQL можно использовать логические операторы, конструкции ORDER BY и LIMIT и некоторые другие операторы.
Чтобы получить имя и дату рождения пользователя, нужно сделать FQL запрос к таблице user. Например, в Graph API Explorer это будет запрос fql?q=SELECT uid, name, birthday_date FROM user WHERE uid = me(). А для того, чтобы получить список id своих друзей, используем запрос к таблице friend fql?q=SELECT uid2 FROM friend WHERE uid1 = me().
Давайте попробуем объединить два запроса в один, используя вложенный запрос. Все просто: fql?q=SELECT uid, name, birthday_date FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = me()). Вместо десятков запросов мы получили всю необходимую нам информацию всего за один!

// получаем список друзей пользователя и их дни рождения
$graph_url = 'https://graph.facebook.com/fql?q=SELECT uid, name, birthday_date FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = '.$uid.')&access_token=' . $params['access_token'];
$frnds = file_get_contents($graph_url);
// декодировать json ответ и вывести данные
Способов оптимизации запросов FQL довольно много. Изучите все таблицы в документации. Но FQL это не вершина оптимизации запросов.
Представьте, что вам необходимо получить список последних постов пользователя, его ленту новостей, все непрочитанные уведомления и новые сообщения. И все это, и многое другое, нужно сделать быстро. Можно, конечно, распараллелить запросы, сделав их ассинхронными. Но это не наш способ.
Способ 5: Batch Requests
С помощью Graph API вы можете обрабатывать данные всего одного запроса, даже если это FQL-запрос. Batch Request позволяет отправить на сервер в одном запросе — пачку из нескольких разных запросов. Единственное ограничение — до 20-и запросов в одном вызове batch. Запросы могут быть GET, POST и DELETE.
Например, вот такой “пакет” запросов:
- Отправить новый статус с ссылкой.
- Получить информацию о последнем статусе (новый не учитывается).
- Лайкнуть последний статус и написать комментарий.
$batched_request = '[{"method":"POST","relative_url":"me/feed","body":"message=Скоро новый пост&link=http://habrahabr.ru/"}';
$batched_request .= ',{"method":"GET","name":"get-post","relative_url":"me/feed?limit=1"}';
$batched_request .= ',{"method":"POST","relative_url":"{result=get-post:$.data.0.id}/likes"}';
$batched_request .= ',{"method":"POST","relative_url":"{result=get-post:$.data.0.id}/comments","body":"message=Новый автоматический коммент"}';
$batched_request .= ']';
$url = 'https://graph.facebook.com/';
$param = array();
$param['access_token'] = $params['access_token'];
$param['batch'] = $batched_request;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
$ret = curl_exec($ch);
// обработка ответа
if($ret == 'false') echo '=false=';
curl_close($ch);
Batch запросы должны отправляться методом POST, поэтому в коде выше задействован cURL. Также с его помощью проще работать с возможными ошибками.
Все способы применения Batch Requests не описать в данной статье. Вы можете узнать о них больше в документации.