Статьи

Улучшение восприятия производительности: изменение размера изображения по требованию

Эта статья является частью серии по созданию примера приложения — блога галереи с несколькими изображениями — для оценки производительности и оптимизации. (Посмотреть репо здесь.)


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

Задача

Есть два этапа этого улучшения.

  1. Мы должны сделать все изображения отзывчивыми, где бы это ни было полезно. Одно место — это миниатюры на домашней странице и на страницах галереи, а другое — полноразмерное изображение при нажатии на отдельное изображение в галерее.
  2. Нам нужно добавить логику изменения размера в наше приложение. Суть в том, чтобы генерировать измененное изображение на лету, как это требуется. Это предотвратит загрязнение нашего жесткого диска непопулярными изображениями и обеспечит, чтобы популярные изображения по последующим запросам обслуживались в оптимальных размерах.

Адаптивные изображения?

Как объясняется в этом посте , изображения в современном Интернете невероятно сложны. Вместо того, чтобы просто <img src="mypic.jpg"> из <img src="mypic.jpg"> времен, у нас теперь есть нечто сумасшедшее, подобное этому:

 <picture> <source media="(max-width: 700px)" sizes="(max-width: 500px) 50vw, 10vw" srcset="stick-figure-narrow.png 138w, stick-figure-hd-narrow.png 138w"> <source media="(max-width: 1400px)" sizes="(max-width: 1000px) 100vw, 50vw" srcset="stick-figure.png 416w, stick-figure-hd.png 416w"> <img src="stick-original.png" alt="Human"> </picture> 

Комбинация srcset , picture и sizes необходима в сценарии, в котором вы сомневаетесь, что если вы используете одно и то же изображение для экрана меньшего размера, основной объект изображения может стать слишком маленьким по размеру. Вы хотите отобразить другое изображение (более сфокусированное на основном объекте) с другим размером экрана, но по-прежнему хотите отображать отдельные активы одного и того же изображения на основе соотношения пикселей на устройстве и хотите настроить высоту и ширину изображения на основе в окне просмотра.

Поскольку наши изображения являются фотографиями, и мы всегда хотим, чтобы они находились в положении, заданном по умолчанию DOM, заполняя максимум родительского контейнера, нам не нужна picture (что позволяет нам определить альтернативный источник для другого разрешения или поддержки браузера — как попытка визуализации SVG, затем PNG, если SVG не поддерживается) или sizes (что позволяет нам определить, какую часть области просмотра должно занимать изображение). Мы можем обойтись только с помощью srcset , который загружает версию одного и того же изображения в другом размере в зависимости от размера экрана.

Добавление srcset

Первое место, где мы сталкиваемся с изображениями, находится в home-galleries-lazy-load.html.twig , частичном шаблоне, который отображает список галерей на домашнем экране.

 <a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}" class="gallery__leading-image card-img-top"> </a> 

Здесь мы видим, что ссылка на изображение извлекается из фильтра Twig, который можно найти в файле src/Twig/ImageRendererExtension.php . Он берет идентификатор изображения и имя маршрута (определенные в аннотации в маршруте serveImageAction ImageController ) и генерирует URL-адрес на основе этой формулы: /image/{id}/raw -> заменяя {id} на указанный идентификатор:

 public function getImageUrl(Image $image) { return $this->router->generate('image.serve', [ 'id' => $image->getId(), ], RouterInterface::ABSOLUTE_URL); } 

Давайте изменим это на следующее:

 public function getImageUrl(Image $image, $size = null) { return $this->router->generate('image.serve', [ 'id' => $image->getId() . (($size) ? '--' . $size : ''), ], RouterInterface::ABSOLUTE_URL); } 

Теперь все URL-адреса наших изображений будут иметь --x , где x — их размер. Это изменение мы применим и к нашему тегу img в форме srcset . Давайте изменим это на:

 <a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}" srcset=" {{ gallery.images.first|getImageUrl('1120') }} 1120w, {{ gallery.images.first|getImageUrl('720') }} 720w, {{ gallery.images.first|getImageUrl('400') }} 400w" class="gallery__leading-image card-img-top"> </a> 

Если мы обновим домашнюю страницу сейчас, мы заметим новые размеры srcset в списке:

Srcset

Это не очень нам поможет. Если у нас широкая область просмотра, то будут запрашиваться полноразмерные изображения, несмотря на то что они являются миниатюрами Поэтому вместо srcset лучше использовать фиксированный маленький размер миниатюры:

 <a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}"> <img src="{{ gallery.images.first|getImageUrl('250') }}" alt="{{ gallery.name }}" class="gallery__leading-image card-img-top"> </a> 

Теперь у нас есть эскизы по запросу, но они кэшируются и выбираются, когда они уже созданы.

Давайте сейчас srcset других местах srcset .

В templates/gallery/single-gallery.html.twig мы применяем то же исправление, что и раньше. Мы имеем дело с миниатюрами, поэтому давайте просто уменьшим файл, добавив параметр size в наш фильтр getImageUrl :

 <img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}" class="single-gallery__item-image card-img-top"> 

А теперь, наконец, для реализации srcset !

Отдельные изображения отображаются с помощью модального окна JavaScript в нижней части того же представления с одной галереей:

 {% block javascripts %} {{ parent() }} <script> $(function () { $('.single-gallery__item-image').on('click', function () { var src = $(this).attr('src'); var $modal = $('.single-gallery__modal'); var $modalBody = $modal.find('.modal-body'); $modalBody.html(''); $modalBody.append($('<img src="' + src + '" class="single-gallery__modal-image">')); $modal.modal({}); }); }) </script> {% endblock %} 

Есть вызов append который добавляет элемент img в тело модала, поэтому srcset должен идти наш атрибут srcset . Но поскольку URL-адреса наших изображений генерируются динамически, мы не можем вызвать фильтр Twig из script . Одна альтернатива — добавить srcset в эскизы, а затем использовать его в JS, скопировав его из элементов большого пальца, но это не только приведет к загрузке полноразмерных изображений на фоне миниатюр (поскольку у нас широкая область просмотра) , но это также вызвало бы фильтр 4 раза для каждой миниатюры, замедляя процесс. Вместо этого давайте создадим новый фильтр Twig в src/Twig/ImageRendererExtension.php который будет генерировать полный атрибут srcset для каждого изображения.

 public function getImageSrcset(Image $image) { $id = $image->getId(); $sizes = [1120, 720, 400]; $string = ''; foreach ($sizes as $size) { $string .= $this->router->generate('image.serve', [ 'id' => $image->getId() . '--' . $size, ], RouterInterface::ABSOLUTE_URL).' '.$size.'w, '; } $string = trim($string, ', '); return html_entity_decode($string); } 

Мы не должны забыть зарегистрировать этот фильтр:

 public function getFilters() { return [ new Twig_SimpleFilter('getImageUrl', [$this, 'getImageUrl']), new Twig_SimpleFilter('getImageSrcset', [$this, 'getImageSrcset']), ]; } 

Мы должны добавить эти значения в пользовательский атрибут, который мы будем называть data-srcset на каждой отдельной миниатюре:

 <img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}" data-srcset=" {{ image|getImageSrcset }}" class="single-gallery__item-image card-img-top"> 

Теперь у каждого отдельного эскиза есть атрибут data-srcset с необходимыми значениями srcset , но это не srcset , потому что он находится в пользовательском атрибуте, данные для последующего использования.

сгенерированный источник данных

Последний шаг — обновление JS, чтобы воспользоваться этим:

 {% block javascripts %} {{ parent() }} <script> $(function () { $('.single-gallery__item-image').on('click', function () { var src = $(this).attr('src'); var srcset = $(this).attr('data-srcset'); var $modal = $('.single-gallery__modal'); var $modalBody = $modal.find('.modal-body'); $modalBody.html(''); $modalBody.append($('<img src="' + src + '" srcset="" + srcset + '" class="single-gallery__modal-image">')); $modal.modal({}); }); }) </script> {% endblock %} 

Добавление Glide

Glide — это библиотека, которая делает то, что нам нужно — изменение размера изображения по требованию. Давайте установим это.

 composer require league/glide 

Далее, давайте зарегистрируем это в приложении. Мы делаем это, добавляя новый сервис в src/Services со следующим содержимым:

 <?php namespace App\Service; use League\Glide; class GlideServer { private $server; public function __construct(FileManager $fm) { $this->server = $server = Glide\ServerFactory::create([ 'source' => $fm->getUploadsDirectory(), 'cache' => $fm->getUploadsDirectory().'/cache', ]); } public function getGlide() { return $this->server; } } 

Служба использует уже объявленную службу FileManager, которая автоматически вводится из-за нового подхода Symfony к автоматическому подключению. Мы объявляем как входной, так и выходной путь как uploads , присваиваем выходному каталогу суффикс cache и добавляем метод для возврата сервера. Сервер в основном является экземпляром Glide, который выполняет изменение размера и возвращает измененное изображение.

Нам нужно сделать метод getUploadsDirectory в FileManager общедоступным, так как он в настоящее время FileManager :

 public function getUploadsDirectory() { return $this->path; } 

Наконец, давайте serveImageAction метод serveImageAction в ImageController, чтобы он выглядел следующим образом:

 /** * @Route("/image/{id}/raw", name="image.serve") */ public function serveImageAction(Request $request, $id, GlideServer $glide) { $idFragments = explode('--', $id); $id = $idFragments[0]; $size = $idFragments[1] ?? null; $image = $this->em->getRepository(Image::class)->find($id); if (empty($image)) { throw new NotFoundHttpException('Image not found'); } $fullPath = $this->fileManager->getFilePath($image->getFilename()); if ($size) { $info = pathinfo($fullPath); $file = $info['filename'] . '.' . $info['extension']; $newfile = $info['filename'] . '-' . $size . '.' . $info['extension']; $fullPathNew = str_replace($file, $newfile, $fullPath); if (file_exists($fullPath) && ! file_exists($fullPathNew)) { $fullPath = $fullPathNew; $img = $glide->getGlide()->getImageAsBase64($file, ['w' => $size]); $ifp = fopen($fullPath, 'wb'); $data = explode(',', $img); fwrite($ifp, base64_decode($data[1])); fclose($ifp); } } $response = new BinaryFileResponse($fullPath); $response->headers->set('Content-type', mime_content_type($fullPath)); $response->headers->set('Content-Disposition', 'attachment; filename="' . $image->getOriginalFilename() . '";'); return $response; } 

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

В демонстрационных целях мы идем дальше и создаем файлы вручную, добавляя к ним размер и сохраняя их в папке uploads . Следует отметить, что вы также можете использовать метод outputImage из Glide для непосредственного вывода изображения, и оно будет подано прямо из подпапки cache , не сохраняя его с суффиксом в основной папке upload . Вы также можете использовать метод makeImage чтобы просто создать изображение и позволить старой логике получения изображения вступить во владение. Это подход, который мы выбрали ниже:

 /** * @Route("/image/{id}/raw", name="image.serve") */ public function serveImageAction(Request $request, $id, GlideServer $glide) { $idFragments = explode('--', $id); $id = $idFragments[0]; $size = $idFragments[1] ?? null; $image = $this->em->getRepository(Image::class)->find($id); if (empty($image)) { throw new NotFoundHttpException('Image not found'); } $fullPath = $this->fileManager->getFilePath($image->getFilename()); if ($size) { $info = pathinfo($fullPath); $file = $info['filename'] . '.' . $info['extension']; $cachePath = $glide->getGlide()->makeImage($file, ['w' => $size]); $fullPath = str_replace($file, '/cache/' . $cachePath, $fullPath); } $response = new BinaryFileResponse($fullPath); $response->headers->set('Content-type', mime_content_type($fullPath)); $response->headers->set('Content-Disposition', 'attachment; filename="' . $image->getOriginalFilename() . '";'); return $response; } 

Наш бизнес по изменению размера изображения по требованию работает. Теперь все, что нам нужно сделать, это проверить вещи.

тестирование

Как только мы обновим домашнюю страницу, которая будет немного медленнее, изображения начнут генерироваться в папке var/uploads . Давайте проверим это, не прокручивая до второй страницы.

Сгенерированные изображения

Конечно же, теперь у нас есть крошечная миниатюрная версия каждого изображения на главной странице, и это изображение, которое получают. Обратите внимание на небольшие размеры файлов. Теперь давайте зайдем в галерею и нажмем на изображение, чтобы получить большую версию.

Большое изображение, созданное с помощью srcset

Да, наше изображение получено из оригинала.

Но как насчет мобильного? В современных браузерах достаточно легко включить мобильный режим. Давайте попробуем открыть изображение галереи в мобильном представлении и потом проверить папку с изображениями.

Изображение сгенерировано для мобильного

Что если мы изменим ориентацию и проверим папку?

Пейзаж мобильный

Успех, мобильный размер нашего изображения был успешно сгенерирован, а полноэкранное изображение использовалось повторно, потому что именно такой размер экрана у нашего «мобильного» в альбомном режиме. srcset требованию был успешно реализован!

Приложение с этими обновлениями было помечено как этот выпуск .

Вывод

В этом посте мы рассмотрели процесс оптимизации изображений для доставки на фото-ориентированном сайте. Мы сохранили миниатюры фиксированного размера для достижения наилучших результатов, и с полноэкранными изображениями мы сосредоточились на внедрении srcset — простого дополнения к любому современному веб-сайту — в паре с Glide, пакетом изменения размера изображений по требованию, который может сделать тяжелую работу для нас.

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