За время работы с 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 не описать в данной статье. Вы можете узнать о них больше в документации.

