Пока нет официальной поддержки в ядре я придумал 2 варианта реализации: с опцией SQL_CALC_FOUND_ROWS, с отдельным запросом.

Выбирать данные для примера мы будем конечно из таблицы b_iblock_element (\Bitrix\Iblock\ElementTable);

У меня есть инфоблок с id 24 с 4062 активными элементами. Над ним и будем проводить эксперименты, какой способ быстрее.

Вариант №1 с использованием опции SQL_CALC_FOUND_ROWS

Всё сводится к использованию опции SQL_CALC_FOUND_ROWS в секции SELECT запроса. Говорят, что опция довольно спорная и проседает в скорости, когда элементов много. Если есть подходящие индексы для WHERE и ORDER запроса, то возможно решение с двумя отдельными запросами, будет быстрее, чем один с SQL_CALC_FOUND_ROWS.

Поехали:

<?php
 
use Bitrix\Main;
use Bitrix\Iblock;
 
Main\Loader::includeModule('iblock');
 
$params = array(
    '=IBLOCK_ID' => 24,
    '=ACTIVE' => 'Y',
);
 
// Подготовим параметры для постраничной навигации
$navParams = array(
    'NAV_NUM' => 1,
    'PAGE_NUMBER' => 1,
    'PAGE_ELEMENT_COUNT' => 30,
);
 
$navParams['PAGE_NUMBER'] = isset($_GET['PAGEN_' . $navParams['NAV_NUM']]) && intval($_GET['PAGEN_' . $navParams['NAV_NUM']])
    ? intval($_GET['PAGEN_' . $navParams['NAV_NUM']])
    : 1;
 
// Инициируем запрос
$query = new Main\Entity\Query(Iblock\ElementTable::getEntity());
 
// Формируем запрос. Устанавливаем фильры, сортировку, лимит.
// Через ExpressionField-то и добавляем опцию SQL_CALC_FOUND_ROWS (важно её первой ставить в select)
$query
    ->setSelect(array(
        new Main\Entity\ExpressionField('FOUND_ROWS', 'SQL_CALC_FOUND_ROWS %s', 'ID'),
        'ID',
        'NAME',
        'SORT',
        'CODE',
        'PREVIEW_PICTURE',
        'DETAIL_PICTURE',
        'IBLOCK_SECTION_ID',
    ))
    ->setFilter($params)
    ->setOrder(array('ID' => 'DESC'))
    ->setOffset(($navParams['PAGE_NUMBER'] - 1) * $navParams['PAGE_ELEMENT_COUNT'])
    ->setLimit($navParams['PAGE_ELEMENT_COUNT']);;
 
$t = microtime(true);
 
// Выполняем основной запрос
$resultItems = $query->exec();
 
// Вторым запросом и получаем количество элементов в предыдущем запросе с установленной опцией SQL_CALC_FOUND_ROWS
$total = 0;
if ($resultTotal = Main\Application::getConnection()->queryScalar('SELECT FOUND_ROWS() as TOTAL')) {
    $total = $resultTotal;
}
 
$t = sprintf('<strong>Время выполнения 2 sql запросов: %.05f s.</strong>', microtime(true) - $t);
 
$elements = array();
while($item = $resultItems->fetch()) {
    $elements[] = sprintf('%s [%d]', $item['NAME'], $item['ID']);
}
 
// Сформируем html для вывода постранички
$navResult = '';
if ($total > 0) {
    $dbResult = new CDBResult();
 
    $dbResult->NavPageCount = ceil($total / $navParams['PAGE_ELEMENT_COUNT']);
 
    $dbResult->NavPageNomer = $navParams['PAGE_NUMBER'];
 
    $dbResult->NavNum = $navParams['NAV_NUM'];
 
    $dbResult->NavPageSize = $navParams['PAGE_ELEMENT_COUNT'];
 
    $dbResult->NavRecordCount = $total;
 
    ob_start();
 
    $APPLICATION->IncludeComponent('bitrix:system.pagenavigation', '', array(
        'NAV_RESULT' => $dbResult,
    ));
 
    $navResult = @ob_get_clean();
 
}
 
echo $t, '<br><br>', implode('<br>', $elements), '<br><br>', $navResult;

Время выполнения 2 sql запросов: 0.02865 s.

Вариант №2 с использованием отдельного запроса с помощью COUNT

Хочу отметить: данный способ поддерживает и группировку.

Перепишем код:

<?php
 
use Bitrix\Main;
use Bitrix\Iblock;
 
Main\Loader::includeModule('iblock');
 
$params = array(
    '=IBLOCK_ID' => 24,
    '=ACTIVE' => 'Y',
);
 
// Подготовим параметры для постраничной навигации
$navParams = array(
    'NAV_NUM' => 1,
    'PAGE_NUMBER' => 1,
    'PAGE_ELEMENT_COUNT' => 30,
);
 
$navParams['PAGE_NUMBER'] = isset($_GET['PAGEN_' . $navParams['NAV_NUM']]) && intval($_GET['PAGEN_' . $navParams['NAV_NUM']])
    ? intval($_GET['PAGEN_' . $navParams['NAV_NUM']])
    : 1;
 
// Инициируем запрос
$query = new Main\Entity\Query(Iblock\ElementTable::getEntity());
 
// Формируем запрос. Устанавливаем фильры, сортировку,
$query
    ->setSelect(array(
        'ID',
        'NAME',
        'SORT',
        'CODE',
        'PREVIEW_PICTURE',
        'DETAIL_PICTURE',
        'IBLOCK_SECTION_ID',
    ))
    ->setFilter($params)
    ->setOrder(array('ID' => 'DESC'))
;
 
// Сохраним sql запрос, не выполняя его, он нам ещё пригодится
$sql = $query->getQuery();
 
// Добавляем в запрос
$query
    ->setOffset(($navParams['PAGE_NUMBER'] - 1) * $navParams['PAGE_ELEMENT_COUNT'])
    ->setLimit($navParams['PAGE_ELEMENT_COUNT'])
;
 
$t = microtime(true);
 
// Выполняем основной запрос
$resultItems = $query->exec();
 
// Отдельным запросом получаем количество элементов
$total = 0;
if ($resultTotal = Main\Application::getConnection()->queryScalar(sprintf('SELECT count(*) FROM (%s) as TOTAL', $sql))) {
    $total = $resultTotal;
}
 
$t = sprintf('<strong>Время выполнения 2 sql запросов: %.05f s.</strong>', microtime(true) - $t);
 
$elements = array();
while($item = $resultItems->fetch()) {
    $elements[] = sprintf('%s [%d]', $item['NAME'], $item['ID']);
}
 
// Сформируем html для вывода постранички
$navResult = '';
if ($total > 0) {
    $dbResult = new CDBResult();
 
    $dbResult->NavPageCount = ceil($total / $navParams['PAGE_ELEMENT_COUNT']);
 
    $dbResult->NavPageNomer = $navParams['PAGE_NUMBER'];
 
    $dbResult->NavNum = $navParams['NAV_NUM'];
 
    $dbResult->NavPageSize = $navParams['PAGE_ELEMENT_COUNT'];
 
    $dbResult->NavRecordCount = $total;
 
    ob_start();
 
    $APPLICATION->IncludeComponent('bitrix:system.pagenavigation', '', array(
        'NAV_RESULT' => $dbResult,
    ));
 
    $navResult = @ob_get_clean();
 
}
 
echo $t, '<br><br>', implode('<br>', $elements), '<br><br>', $navResult;

Время выполнения 2 sql запросов: 0.06029 s.

Выводы

Как видите, в моём случае вариант с опцией SQL_CALC_FOUND_ROWS оказался в 2 раза быстрее. Поэтому выбирайте вариант в зависимости от вашей ситуации.

P.S. Для настройки постраничной навигации придётся поизучать свойства класса CDBResult (/bitrix/modules/main/classes/mysql/database_mysql.php и в официальной документации).