Главная/Статьи/Битрикс: ресайз изображений на лету

Битрикс: ресайз изображений на лету

CFile::ResizeImageGet для создания миниатюр. Типы масштабирования, кэширование, оптимизация производительности.

ДМ
Дмитрий Мещеряков
📅 13 марта 2019 г.📖 8 мин чтения

CFile::ResizeImageGet() — метод, который знают все и почти никто не читал до конца. Отсюда две типовые проблемы: непонимание, что означает четвёртый аргумент, и директория upload/resize_cache/ весом в сорок гигабайт, которую однажды находят по алерту о свободном месте.

Разберём метод по аргументам, а потом — как не превратить кэш ресайза в свалку.

Зачем нужен ресайз на лету

При загрузке изображения в инфоблок часто нужны разные размеры:

  • Превью для списка (300×200)
  • Миниатюра для корзины (80×80)
  • Средний размер для карточки (600×400)
  • Полный размер для галереи

CFile::ResizeImageGet() создаёт уменьшенные копии автоматически и кэширует их в /upload/resize_cache/.

Базовое использование

php
<?php
// ID изображения из инфоблока
$imageId = $arItem['PREVIEW_PICTURE'];

// Получаем ресайзнутое изображение
$resized = CFile::ResizeImageGet(
    $imageId,
    ['width' => 300, 'height' => 200],
    BX_RESIZE_IMAGE_PROPORTIONAL,
    true  // $bInitSizes: вернуть реальные width/height созданного файла
);

if ($resized) {
    echo '<img src="' . $resized['src'] . '" 
               width="' . $resized['width'] . '" 
               height="' . $resized['height'] . '" 
               alt="' . htmlspecialchars($arItem['NAME']) . '">';
}
⚠️ Важно

Четвёртый аргумент — не про скорость. Это $bInitSizes, и он определяет, будут ли в ответе заполнены реальные width и height созданного изображения. Ничего не ускоряет — наоборот, требует прочитать размеры файла.

Ставить его в true при этом нужно почти всегда, и вот почему: без реальных размеров вы не сможете проставить атрибуты width и height в теге <img>, а без них браузер не знает, сколько места зарезервировать под картинку, и вёрстка прыгает при загрузке. Это напрямую метрика CLS в Core Web Vitals.

Полная сигнатура для справки: ResizeImageGet($file, $arSize, $resizeType, $bInitSizes, $arFilters, $bImmediate, $jpgQuality).

Типы масштабирования

КонстантаОписаниеКогда использовать
BX_RESIZE_IMAGE_PROPORTIONALВписывает в размер с сохранением пропорцийКарточки, превью
BX_RESIZE_IMAGE_EXACTОбрезает до точного размераАватары, квадратные превью
BX_RESIZE_IMAGE_PROPORTIONAL_ALTАльтернативный пропорциональныйВертикальные изображения

Примеры результатов

Исходное изображение: 1200×800

php
// BX_RESIZE_IMAGE_PROPORTIONAL — вписывает в 300×200
// Результат: 300×200 (пропорции сохранены, может быть меньше)

// BX_RESIZE_IMAGE_EXACT — обрезает до 300×200
// Результат: ровно 300×200 (лишнее обрезано по центру)

Хелпер-функция

php
<?php

/**
 * Получение URL ресайзнутого изображения
 */
function getResizedImage(
    $imageId,
    int $width,
    int $height,
    int $resizeType = BX_RESIZE_IMAGE_PROPORTIONAL
): ?array {
    if (empty($imageId)) {
        return null;
    }

    // Если передан массив — извлекаем ID
    if (is_array($imageId)) {
        $imageId = $imageId['ID'] ?? $imageId['id'] ?? null;
    }

    if (!$imageId) {
        return null;
    }

    $resized = \CFile::ResizeImageGet(
        (int) $imageId,
        ['width' => $width, 'height' => $height],
        $resizeType,
        true,
        false, // Не использовать фильтры
        false, // Не выбрасывать исключения
        85     // Качество JPEG
    );

    return $resized ?: null;
}

/**
 * Получение srcset для responsive изображений
 */
function getResponsiveSrcset($imageId, array $sizes): string
{
    $srcset = [];

    foreach ($sizes as $width) {
        $resized = getResizedImage($imageId, $width, 0, BX_RESIZE_IMAGE_PROPORTIONAL);
        
        if ($resized) {
            $srcset[] = $resized['src'] . ' ' . $resized['width'] . 'w';
        }
    }

    return implode(', ', $srcset);
}

// Использование
$imageId = $arItem['PREVIEW_PICTURE'];

$preview = getResizedImage($imageId, 400, 300, BX_RESIZE_IMAGE_EXACT);
$srcset = getResponsiveSrcset($imageId, [400, 800, 1200]);

echo '<img 
    src="' . $preview['src'] . '" 
    srcset="' . $srcset . '"
    sizes="(max-width: 600px) 100vw, 400px"
    alt="' . htmlspecialchars($arItem['NAME']) . '"
    loading="lazy"
>';

Класс для работы с изображениями

php
<?php

namespace Local\Image;

class ImageResizer
{
    /**
     * Стандартные размеры
     */
    public const SIZE_THUMB = ['width' => 100, 'height' => 100];
    public const SIZE_PREVIEW = ['width' => 300, 'height' => 200];
    public const SIZE_MEDIUM = ['width' => 600, 'height' => 400];
    public const SIZE_LARGE = ['width' => 1200, 'height' => 800];

    private int $quality = 85;

    /**
     * Ресайз до конкретного размера
     */
    public function resize(
        $imageId,
        int $width,
        int $height,
        int $type = BX_RESIZE_IMAGE_PROPORTIONAL
    ): ?array {
        $imageId = $this->normalizeImageId($imageId);
        
        if (!$imageId) {
            return null;
        }

        return \CFile::ResizeImageGet(
            $imageId,
            ['width' => $width, 'height' => $height],
            $type,
            true,
            false,
            false,
            $this->quality
        ) ?: null;
    }

    /**
     * Квадратное превью (аватар)
     */
    public function square($imageId, int $size = 100): ?array
    {
        return $this->resize($imageId, $size, $size, BX_RESIZE_IMAGE_EXACT);
    }

    /**
     * Превью для списка
     */
    public function preview($imageId): ?array
    {
        return $this->resize(
            $imageId,
            self::SIZE_PREVIEW['width'],
            self::SIZE_PREVIEW['height']
        );
    }

    /**
     * Средний размер
     */
    public function medium($imageId): ?array
    {
        return $this->resize(
            $imageId,
            self::SIZE_MEDIUM['width'],
            self::SIZE_MEDIUM['height']
        );
    }

    /**
     * Набор размеров для srcset
     */
    public function getSrcset($imageId, array $widths = [400, 800, 1200]): string
    {
        $srcset = [];

        foreach ($widths as $width) {
            $resized = $this->resize($imageId, $width, 0);
            
            if ($resized) {
                $srcset[] = $resized['src'] . ' ' . $resized['width'] . 'w';
            }
        }

        return implode(', ', $srcset);
    }

    /**
     * WebP версия (если поддерживается)
     */
    public function resizeWebp($imageId, int $width, int $height): ?array
    {
        $resized = $this->resize($imageId, $width, $height);
        
        if (!$resized) {
            return null;
        }

        // Проверяем наличие WebP версии
        $webpPath = preg_replace('/\.(jpe?g|png)$/i', '.webp', $resized['src']);
        $webpFullPath = $_SERVER['DOCUMENT_ROOT'] . $webpPath;

        if (file_exists($webpFullPath)) {
            $resized['webp'] = $webpPath;
        }

        return $resized;
    }

    /**
     * Picture элемент с WebP
     */
    public function getPictureHtml($imageId, int $width, int $height, string $alt = ''): string
    {
        $image = $this->resizeWebp($imageId, $width, $height);
        
        if (!$image) {
            return '';
        }

        $html = '<picture>';
        
        if (!empty($image['webp'])) {
            $html .= '<source srcset="' . $image['webp'] . '" type="image/webp">';
        }
        
        $html .= '<img src="' . $image['src'] . '" ';
        $html .= 'width="' . $image['width'] . '" ';
        $html .= 'height="' . $image['height'] . '" ';
        $html .= 'alt="' . htmlspecialchars($alt) . '" ';
        $html .= 'loading="lazy">';
        $html .= '</picture>';

        return $html;
    }

    /**
     * Установка качества
     */
    public function setQuality(int $quality): self
    {
        $this->quality = max(1, min(100, $quality));
        return $this;
    }

    /**
     * Нормализация ID изображения
     */
    private function normalizeImageId($imageId): ?int
    {
        if (is_array($imageId)) {
            $imageId = $imageId['ID'] ?? $imageId['id'] ?? null;
        }

        return $imageId ? (int) $imageId : null;
    }
}

Использование в шаблоне

php
<?php
$resizer = new \Local\Image\ImageResizer();

foreach ($arResult['ITEMS'] as $item):
    $preview = $resizer->preview($item['PREVIEW_PICTURE']);
    $srcset = $resizer->getSrcset($item['PREVIEW_PICTURE']);
?>
    <div class="product-card">
        <img 
            src="<?= $preview['src'] ?>" 
            srcset="<?= $srcset ?>"
            sizes="(max-width: 768px) 100vw, 300px"
            width="<?= $preview['width'] ?>"
            height="<?= $preview['height'] ?>"
            alt="<?= htmlspecialchars($item['NAME']) ?>"
            loading="lazy"
        >
        <h3><?= $item['NAME'] ?></h3>
    </div>
<?php endforeach; ?>

Сколько это стоит на списке из 50 товаров

Метод дёшев, когда файл уже создан, но «дёшев» не значит «бесплатен». На каждый вызов приходится как минимум проверка существования файла на диске, а если вы передали ID, а не готовый массив, — ещё и запрос к b_file.

Отсюда практический приём: если вы уже получили данные элемента через GetList и в выборке есть PREVIEW_PICTURE, передавайте в ResizeImageGet массив файла целиком, а не идентификатор. Пятьдесят товаров в списке — это пятьдесят сэкономленных запросов к базе на каждой отрисовке страницы.

Второй приём — не делать ресайз в шаблоне для того, что не видно. Картинки под loading="lazy" всё равно попадут в вёрстку, но если у вас есть блоки, отрисовывающиеся условно, ресайз для них считать не нужно.

Очистка кэша

php
<?php
// Очистка кэша ресайза для конкретного изображения
$imageId = 123;
\CFile::ResizeImageDelete($imageId);

// Очистка всего кэша (осторожно!)
// Удаление папки /upload/resize_cache/
bash
# Сначала посмотреть объём и распределение по датам
du -sh /home/bitrix/www/upload/resize_cache
find /home/bitrix/www/upload/resize_cache -type f -atime +90 | wc -l

# Удаление по времени последнего ОБРАЩЕНИЯ, а не изменения
find /home/bitrix/www/upload/resize_cache -type f -atime +90 -delete
find /home/bitrix/www/upload/resize_cache -type d -empty -delete
⚠️ Важно

-mtime и -atime — разные вещи, и здесь это принципиально. -mtime показывает, когда файл был создан ресайзом, и не имеет отношения к тому, нужен ли он сейчас: превью главного товара каталога, созданное год назад и запрашиваемое каждую минуту, попадёт под удаление наравне с мусором. -atime — время последнего обращения, что и есть искомый критерий «этой картинкой давно никто не пользовался».

Оговорка: многие серверы монтируют файловые системы с noatime или relatime ради производительности, и тогда -atime работает неточно или не работает вовсе. Проверьте вывод mount | grep atime перед тем, как полагаться на этот критерий.

И в любом случае — удалять пачками с проверкой: сначала | wc -l, потом -delete. Массовое удаление кэша ресайза на живом сайте означает, что все эти картинки начнут генерироваться заново по мере обращений, и вы получите всплеск нагрузки на CPU ровно в тот момент, когда пытались освободить диск.

Итоги

Ставьте $bInitSizes = true и проставляйте width/height в теге. Это одна строка, которая напрямую влияет на CLS.

Передавайте массив файла, а не ID, если данные уже выбраны, — экономия запросов на списках заметна.

Считайте кэш ресайза расходуемым ресурсом, а не вечным хранилищем. Директория растёт при каждом новом сочетании «файл + размер», а размеры добавляются при каждом редизайне: сменили сетку каталога — получили ещё один полный комплект превью для всего каталога, старый при этом никуда не делся.

Не удаляйте кэш целиком на живом трафике — регенерация даст всплеск нагрузки. Чистите порциями и по времени обращения.

💡 Совет

Про формат. ResizeImageGet отдаёт тот же формат, что и исходник: JPEG остаётся JPEG. Современные форматы дают 25–35% экономии веса при том же качестве — если оптимизация картинок делается ради скорости загрузки, конвертация в WebP принесёт больше, чем подбор размеров.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.