Как я к этому пришёл
Моя первая гипотеза была «много картинок». Я сжал изображения, перевёл их в WebP, настроил ленивую загрузку — и выиграл около ста миллисекунд из тысячи восьмисот. Работа полезная, но проблема была не там.
Переломный момент наступил, когда я перестал гадать и поставил профайлер. Оказалось, что база отвечает быстро, но её спрашивают девяносто раз подряд — по одному запросу на товар. В шаблоне это выглядело безобидно: {{ product.category.name }}. Каждое такое обращение — отдельный поход в базу.
Дальше было скучно и эффективно. select_related подтянул категории одним JOIN. prefetch_related забрал теги вторым запросом вместо сотни. EXPLAIN показал, что фильтр по цене и категории идёт последовательным сканированием — составной индекс закрыл этот случай.
Отдельная история — кэш. Наивная реализация «кэшировать на пять минут» означала, что менеджер меняет цену и не видит её ещё пять минут. Пришлось вешать инвалидацию на сигнал post_save товара.
Итог: 180 миллисекунд и 6 запросов вместо 1.8 секунды и 90.