SVG4 — каталог векторных иконок
SVG4 — каталог векторных иконок

Больше года назад я запустил SVG4 — каталог векторных иконок. Сейчас в нём больше 500 000 SVG‑файлов. Первая версия была написана на Laravel, и на тот момент выбор был вполне логичным: Laravel я хорошо знал, на нём можно было быстро собрать рабочий проект и проверить, нужен ли он вообще кому‑нибудь. Проект оказался нужен, начал расти, а вместе с ним росли база, количество страниц, поисковый трафик и нагрузка. Старая версия в итоге работала на сервере с 4 ядрами и 6 ГБ оперативной памяти, а после переписывания на Go весь проект работает на 2 ядрах и 2 ГБ RAM.

При этом я не ставил задачу доказать, что Go быстрее PHP. За время переписывания изменилась не только технология, но и сама архитектура приложения: часть слоёв исчезла, запросы к базе были пересмотрены, кеширование стало проще, а работа приложения — более предсказуемой. Поэтому корректнее рассматривать переход не как прямое сравнение двух языков, а как вторую реализацию уже понятного мне проекта. Именно об этом и хочу рассказать.

Что представлял собой проект до переезда

Со стороны пользователя SVG4 выглядит довольно просто: человек приходит на сайт, вводит запрос, выбирает нужную иконку и скачивает SVG. Но за этим уже накопилось достаточно много логики. В каталоге больше 500 000 SVG, есть наборы и коллекции, категории, теги, поиск, страницы отдельных иконок, лицензии, избранное, пользовательские папки, конвертация форматов, SEO‑страницы и административная часть.

Кроме обычных пользователей есть поисковые роботы, и для такого каталога это существенная часть нагрузки. Сотни тысяч страниц должны стабильно открываться и сразу отдавать полноценный HTML. На старой версии я постепенно пришёл к серверу с 4 ядрами и 6 ГБ RAM. Этого хватало, сайт работал, но мне всё меньше нравилось соотношение сложности приложения и количества ресурсов, которые нужны для его работы.

Почему я вообще начал смотреть в сторону Go

Не было одного запроса, который внезапно начал выполняться десять секунд и заставил меня срочно менять стек. Проблема была накопительной. В PHP‑приложении приходится учитывать PHP‑FPM, количество воркеров, память каждого процесса, OPcache, загрузку фреймворка и поведение всей этой системы при росте параллельных запросов. Laravel здесь ни при чём — это обычная модель работы PHP‑приложения.

Можно настраивать PHP‑FPM, добавлять Redis, оптимизировать запросы, кешировать результаты и увеличивать сервер. Так я некоторое время и делал. Но постепенно возник другой вопрос: нужен ли конкретно моему проекту весь этот объём инфраструктуры, если основная работа сайта довольно прямолинейна?

получить HTTP-запрос
        ↓
проверить параметры
        ↓
получить данные
        ↓
отрендерить HTML
        ↓
вернуть ответ

Для такого приложения модель Go показалась мне подходящей. Один постоянно работающий процесс, внутри него маршрутизатор, соединения с базой, кеш, бизнес‑логика и шаблонизатор. Приложение не нужно загружать заново для каждого запроса. Я сделал несколько экспериментов и в итоге решил переписывать проект.

Я не стал переводить Laravel‑код на Go

Это было принципиальное решение. Самый очевидный путь выглядел бы примерно так:

Controller → Handler
Service → Service
Repository → Repository
Model → Model

Но в таком случае я фактически перенёс бы существующую архитектуру на другой язык. Вместо этого Laravel‑проект стал для меня спецификацией. Я смотрел на каждую страницу и выяснял, какой URL должен существовать, какие данные ей нужны, какие проверки выполняются, какое действие ожидает пользователь и что должно произойти после этого действия.

Внутреннюю структуру я собирал заново. В результате часть слоёв просто исчезла. Если обработчику нужно сделать запрос в базу и передать результат в шаблон, я не вижу обязательной необходимости пропускать данные через несколько промежуточных интерфейсов только ради архитектурной симметрии. Текущий стек в итоге выглядит довольно просто:

Backend
├── Go
├── chi
├── Bun
├── Jet
├── MySQL
└── Redis

Frontend
├── Tailwind CSS
├── HTMX
└── Alpine.js

Отдельного frontend‑приложения нет.

Почему я оставил серверный рендеринг

На SVG4 много публичных страниц: иконки, наборы, коллекции, категории, результаты поиска. Мне нужно, чтобы сервер сразу возвращал готовый HTML, поэтому основной путь запроса остался максимально обычным.

HTTP request
    ↓
chi
    ↓
handler
    ↓
MySQL / Redis
    ↓
Jet
    ↓
HTML

SPA здесь не решало проблему, которая у меня была. Наоборот, пришлось бы добавить ещё один слой: API, клиентское состояние, отдельное JavaScript‑приложение, сборку и синхронизацию данных между frontend и backend. При этом полный reload страницы для каждого небольшого действия тоже не нужен, поэтому для таких сценариев я использую HTMX.

Например, пользователь добавляет иконку в избранное. Backend изменяет состояние и возвращает уже готовый HTML‑фрагмент с обновлённой кнопкой. Не JSON, который потом нужно отдельно разобрать и вручную изменить DOM, а сразу тот HTML, который должен оказаться на странице. Для SVG4 такого подхода хватает почти везде, а Alpine.js остаётся для небольшой клиентской логики, ради которой вообще нет смысла обращаться к серверу.

Доступ к базе я тоже упростил

Для MySQL использую Bun, но стараюсь не прятать SQL настолько глубоко, чтобы потом приходилось угадывать, какой запрос реально выполняется. Простой запрос выглядит примерно так:

var icons []Icon

err := db.NewSelect().
    Model(&icons).
    Where("status = ?", StatusActive).
    OrderExpr("created_at DESC").
    Limit(50).
    Scan(ctx)

if err != nil {
    return fmt.Errorf("load icons: %w", err)
}

По коду сразу понятно, что происходит. Если выборка сложная, запрос тоже остаётся явным. Я отказался от идеи делать универсальный repository на все случаи жизни, потому что на практике такие конструкции довольно быстро превращались в ещё один API, который нужно помнить поверх SQL.

Где получилась экономия ресурсов

До миграции SVG4 работал на машине с 4 CPU и 6 ГБ RAM. Сейчас новая версия работает на 2 CPU и 2 ГБ RAM. То есть сервер стал вдвое меньше по CPU и втрое по памяти, причём речь идёт о рабочем сайте, а не о benchmark на тестовом handler.

Для меня это гораздо полезнее результатов вида «Laravel — 120 ms, Go — 8 ms». Такие цифры легко получить в лабораторном тесте и почти так же легко сделать из них неправильный вывод. В реальном проекте есть MySQL, Redis, сеть, шаблоны, диски, поисковые роботы и пользовательские запросы, поэтому я смотрел не на скорость выполнения одной функции, а на то, какой сервер нужен проекту целиком.

При этом приписывать всю разницу только Go было бы неправильно. Одновременно я сменил runtime, убрал часть архитектурных слоёв, пересмотрел запросы, ограничил пул соединений, изменил кеширование, переписал рендеринг и удалил ненужную логику. Поэтому корректнее сказать так: новая реализация SVG4 на Go требует заметно меньше ресурсов, чем предыдущая реализация на Laravel. Это не то же самое, что утверждать, будто Go сам по себе в три раза экономнее Laravel.

Самым полезным оказался не прирост скорости

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

Если растёт CPU, я вижу один процесс. Если растёт память, я вижу один процесс. Если запрос работает медленно, можно пройти путь от handler до SQL и понять, где уходит время. Количество движущихся частей стало меньше, и для небольшого проекта, который я сам разрабатываю и обслуживаю, это оказалось важнее нескольких миллисекунд в синтетическом тесте.

Но Go не решил проблемы базы данных

После перехода на Go медленный SQL не становится быстрым. Если запрос сканирует несколько сотен тысяч строк без нужного индекса, смена PHP на Go его не спасёт. Более того, на некоторых страницах после миграции именно база стала самым заметным ограничением.

В каком‑то смысле это даже полезно. Когда стоимость самого приложения уменьшается, реальные узкие места становятся заметнее. Дальше пришлось заниматься обычной работой: смотреть EXPLAIN, добавлять индексы, убирать лишние JOIN, сокращать количество запросов, контролировать размер результатов и решать, что имеет смысл кешировать. Никакой магии Go здесь нет.

Что оказалось сложнее самого переписывания

Самая неприятная часть миграции оказалась связана не с Go, а с совместимостью со старым сайтом. Если проект уже получает поисковый трафик, нельзя одновременно с заменой backend решить полностью поменять URL, структуру страниц и остальные внешние признаки сайта.

Для поисковика смена backend вообще не должна быть заметна. Если раньше существовал определённый URL, после миграции он тоже должен существовать. То же касается canonical, status code, redirects, sitemap, meta‑тегов, внутренних ссылок и структуры страниц. Поэтому заметная часть времени ушла не на разработку новой версии, а на проверку того, что снаружи она ведёт себя так же, как старая.

Именно здесь сформировался один из главных принципов миграции: внутри можно поменять почти всё, а снаружи на первом этапе лучше менять как можно меньше.

Что я бы сейчас сделал иначе

Сейчас я бы раньше разделил задачу на две части: сначала перенести существующее поведение, а уже потом улучшать продукт. В начале очень хочется совместить миграцию с генеральной уборкой. Раз уж переписываем backend, почему бы сразу не переделать поиск, дизайн, URL, базу и систему пользователей.

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

Получается, Laravel был ошибкой?

Нет. Без первой версии на Laravel, скорее всего, не было бы и текущей версии на Go. На старте мне нужно было быстро собрать проект и проверить саму идею, а Laravel это позволил.

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

Если бы я с самого начала несколько месяцев строил идеальный высокопроизводительный backend для сайта без пользователей, это было бы гораздо большей ошибкой.

Стоило ли переписывать

В моём случае — да. Сейчас SVG4 работает на сервере с 2 ядрами и 2 ГБ оперативной памяти вместо прежних 4 ядер и 6 ГБ. Приложение стало проще в обслуживании, поведение — более предсказуемым, а инфраструктура — компактнее.

Но использовать этот результат как аргумент «перепишите Laravel на Go — и сервер станет в три раза дешевле» было бы неправильно. У меня совпало сразу несколько факторов. Проект уже существовал, его поведение было понятно, я знал реальные запросы, реальные узкие места и мог выбросить то, что оказалось ненужным. Вторая версия системы почти всегда имеет преимущество перед первой хотя бы потому, что при её разработке ты уже знаешь, где ошибся.

И, наверное, главный вывод из всей этой истории вообще не про Go и не про Laravel.

Не стоит слишком бояться переделывать то, что уже работает. Первая версия проекта не обязана быть идеальной и не обязана пережить следующие десять лет. Её задача — начать работать, попасть к пользователям и дать вам реальные данные о том, что вообще нужно делать дальше.

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

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

Поэтому сейчас мой подход гораздо проще: сначала сделать так, чтобы продукт работал и им начали пользоваться. Потом смотреть на реальные проблемы и спокойно переделывать то, что перестало подходить.

Что дальше

После миграции оказалось, что производительность backend — далеко не самая интересная задача SVG4. Когда в каталоге больше 500 000 иконок, проблема уже не в том, как сохранить ещё 100 000, а в том, как среди них найти нужную.

Поэтому сейчас большая часть работы идёт вокруг поиска, релевантности и способов организовать такое количество контента. В следующей статье я постараюсь подробно показать, как сейчас устроен поиск в SVG4: как обрабатывается запрос, как используются названия, теги и синонимы, где появляется релевантность и какие проблемы возникают, когда иконок уже сотни тысяч.

А backend после всей этой миграции хочется замечать как можно реже. Для меня это и есть хороший результат: он просто работает, а я могу заниматься самим продуктом.

Комментарии (11)


  1. Vadiok
    21.09.2026 13:44

    Спасибо за статью, но как-то текста много, а информации мало. Хотелось бы понимать, почему вообще сервис на Laravel требовал 6Гб, что именно изменилось, что памяти стало требоваться в 3 раза меньше?

    Может я пропустил это? Просто, когда вижу, что одна мысль начинает повторяться несколько раз, предполагаю, что текст был написан нейронкой, и вчитываться в такой текст подробно не вижу смысла.


    1. init0
      21.09.2026 13:44

      Да тут очевидно, что как статью таки сам сервис писала/переписывала нейронка. И главная цель статьи - реклама своего сайта, прям с порога 2 ссылки, а мы брюзжим на писателей которые в конце свои телеги постят.


      1. baturinaleksei Автор
        21.09.2026 13:44

        «Раскусили! На самом деле этот коммент тоже пишет нейронка... Ладно, шучу :)

        Если серьезно: глупо писать статью про свой сервис и прятать ссылки.


    1. baturinaleksei Автор
      21.09.2026 13:44

      Да, ИИ тут приложил руку, каюсь :) Сейчас без него никуда. Вы абсолютно правы, за водой потерялась суть.

      Мысль статьи была больше философской: выкатывай MVP как есть, а докручивай потом, иначе погрязнешь в муках перфекционизма.


  1. sunnybear
    21.09.2026 13:44

    Можно сделать proxy_store, и весь проект будет отлично работать на 1/1


  1. jshapen
    21.09.2026 13:44

    Не работает с блокировщиком рекламы.


    1. init0
      21.09.2026 13:44

      Если uBlock добавьте в “Мои фильтры”:

      ||yandex.ru/ads/system/context.js$redirect=noop.js,important
      


    1. baturinaleksei Автор
      21.09.2026 13:44

      На сайте нет агрессивной рекламы, один блок показывается только в момент скачивания, пока идет конвертация.


  1. Metotron0
    21.09.2026 13:44

    Недавно мне нужны были две svg-иконки банального чекбокса: пустое состояние и состояние с галочкой внутри, чтобы рамка у обоих была одинаковой, добавлялась только галочка. Первые 40 минут я потратил на обход сайтов, которые выдал поиск, на регистрацию в них, попытки найти парные иконки. На части сайтов SVG было только за деньги, а мне предлагали скачать png, это тоже могло бы подойти, если бы нашлась пара нужных (заказчик потом попросил перекрасить рамку, поэтому всё же не вполне подошло бы). Ещё на части сайтов давали скачать одну SVG, а дальше за деньги (пожалуй, можно было бы скачать с чекбоксом и потом руками убрать его, получив пустой вариант). По прошествии 40 минут я просто попросил chatgpt сгенерировать мне нужные svg. Знал бы — сразу туда и пошёл.


    1. Metotron0
      21.09.2026 13:44

      ~ то есть, не с чекбоксом скачать вариант, а "с чеком".


    1. baturinaleksei Автор
      21.09.2026 13:44

      на svg4 можно скачать без регистрации в любом формате, лимитов нет