Больше года назад я запустил 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 после всей этой миграции хочется замечать как можно реже. Для меня это и есть хороший результат: он просто работает, а я могу заниматься самим продуктом.
Автор: baturinaleksei
