Доброго времени суток уважаемый читатель. Хотелось бы немного поговорить об оптимизации наших с вам любимых WEB приложений, написанных на нашем горячо любимом и всеми уважаемом фреймворке Django. В частности речь в этой статье пойдёт об оптимизации изображений. А теперь по порядку.
А что там Google Lighthouse?
Если вы хоть раз нажимали правой кнопкой мыши на экране вкладки, открытой в Google Chrome, а затем щёлкали "Просмотреть код", то Вы могли видеть инструмент для анализа вашего приложение под названием Lighthouse:

Проведя тест Вашего приложения вы можете увидеть следующее:

Ссылка Learn more about modern image formats. ведёт на эту страницу. Если коротко, то нам предлагают избавится от изображений в старых форматах и приобщиться к более современным, в частности к формату webp. Более подробно вы можете ознакомится самостоятельно, перейдя по ссылке.
Django, ты как там?
Коль скоро речь заходит о хранении изображений на стороне Back-end, то мы не будем томить и приведём пример хранения изображения в нашей базе данных, допустим у нас будет логотип компании, который нам необходимо сохранить в формате webp. Для этого напишем в файле models.py следующее:
class CompanyLogo(models.Model):
logo = models.ImageField(
upload_to='images/logo',
verbose_name='Лого'
)
А чего тут нового, спросите вы и будете абсолютно правы. Это вполне себе стандартный синтаксис модели Django. Давайте напишем функцию, которая на входе принимает файл изображения, допустим jpeg или png, а возвращает изображение в формате webp, да ещё и подрезанное, как того требует макет скажем в Figma. По желанию, вы можете хранить этот код в отдельном файле проекта и импортировать в случае необходимости, я же добавлю эту функцию в модель CompanyLogo.
from django.db import models
from io import BytesIO
from PIL import Image
from django.core.files import File
from django.core.files.base import ContentFile
class CompanyLogo(models.Model):
logo = models.ImageField(
upload_to='images/logo',
verbose_name='Лого'
)
def compress_logo(self, image):
im = Image.open(image)
width, height = im.size[0], int(im.size[0] * 1.5)
x, y = 0, int((im.size[1] - height) // 2)
area = (x, y, x+width, y+height)
im = im.crop((area))
im = im.resize((200, 300))
im_bytes = BytesIO()
im.save(fp=im_bytes, format="WEBP", quality=100)
image_content_file = ContentFile(content=im_bytes.getvalue())
name = image.name.split('.')[0] + '.WEBP'
new_image = File(image_content_file, name=name)
return new_image
Следует упомянуть, что для работы с полем ImageField в Django, требуется модуль Pillow,
которой нужно предварительно установить в нашу виртуальную среду pip install Pillow.
Наверное надо немного пояснить, что конкретно делает этот код. Он принимает на входе загруженное изображение, затем берёт за основу его ширину и вычисляет требуемую высоту, пропорционально соотношению сторон (300/200=1.5). Это сделано для того, что бы загрузив даже квадратное изображение вы получили на выходе прямоугольное без потери качества и искажения. Затем мы обрезаем изображение и сжимаем его в соответствии с параметрами макета. Выбираем формат выходного изображения и его качество. Получаем байтовое значение файла. Присваиваем ему имя и сохраняем файл. Вот что получается:

Функция написана, теперь надо понять как и когда ее вызвать. И для этого нам потребуется переопределить метод save класса модели, а именно:
def save(self, *args, **kwargs):
try:
this = CompanyLogo.objects.get(id=self.id)
if this.logo != self.logo:
this.logo.delete(save=False)
try:
new_logo = self.compress_logo(self.logo)
self.logo = new_logo
super(CompanyLogo, self).save(*args, **kwargs)
except ValueError:
super(CompanyLogo, self).save(*args, **kwargs)
else:
super(CompanyLogo, self).save(*args, **kwargs)
except CompanyLogo.DoesNotExist:
try:
new_logo = self.compress_logo(self.logo)
self.logo = new_logo
super(CompanyLogo, self).save(*args, **kwargs)
except ValueError:
super(CompanyLogo, self).save(*args, **kwargs)
Возможно код выглядит немного громоздко, но он работает, если у кого то будут идеи, как его переписать более лаконично я буду этому рад.
Так, что же делает этот код, по мимо того, что вызывает нашу функцию для сжатия изображения при сохранении записи в базе данных? Он позволяет при обновлении или удалении файла (допустим через Django admin), а так же при удалении записи из базы данных удалить физический файл с сервера (ведь все мы стараемся сберечь место на SSD), и если предположить, что у вас очень большой проект, который может содержать огромное количество подобных изображений, то момент с удалением ненужных файлов с сервера будет весьма и весьма полезным.
Что в итоге?
Тоже самое изображение, обработанное по алгоритму из функции compress_logo
но имеющее разрешение png занимает на диске примерно 12,2 кб., когда как изображение в формате webp занимает всего 6 кб.

Вам может показаться, что в реальных цифрах разница не так велика, и вы подумаете, что экономите всего 6 килобайт, но на деле экономия чуть больше 50% процентов, согласитесь не плохо? А если представить, что вы разрабатываете крупную площадки по размещению объявлений, то реальная экономия может оказаться очень весомой.
Надеюсь, этот материал окажется для кого-то полезным. Спасибо, что дочитали текст до конца!