Я намагався мігрувати продакшен‑застосунок NextJS з важкого рантайму Node.js standalone на полегшену архітектуру. Метою було використати NextJS output: 'export' для генерування статичних ресурсів, які потім обслуговував би кастомний, легкий Go‑сервер, упакований у той самий Docker‑образ.
Застосунок був складним, інтернаціоналізованим вебсайтом із підтримкою восьми окремих локалей. Головною вимогою було зберегти універсальну збірку. Це означало, що Docker‑образ мав бути незалежним від змінних середовища, аби дозволити розгортання на різних етапах без повторної збірки. Основна мета — позбутися споживання RAM важким рантаймом NextJS. Рантайм NodeJS/Bun споживав ~ 200-450MB RAM у режимі standalone і ~140MB для статичного експорту.
Щоб досягти універсальної збірки, змінні середовища прибрали з процесу збірки. Замість того щоб вшивати кінцеві точки API у статичний фронтенд, використовували статичні шляхи на кшталт /api/graphql. Відповідальність за обробку цих запитів поклали на вбудований Go‑сервер, який виступав проксі й переспрямовував трафік до відповідних бекенд‑сервісів, визначених змінними середовища під час виконання.
Оптимізацію зображень так само відокремили від рантайму NextJS. Реалізували кастомний завантажувач зображень, який переписував запити на зображення на виділений шлях CDN, щоб статичний експорт лишався незалежним від логіки обробки зображень.
Виникла суттєва перешкода з обробкою динамічних маршрутів, наприклад дописів блогу, до яких звертаються через slug. Статичний експорт NextJS вимагає generateStaticParams, щоб визначити всі шляхи під час збірки. Щоб уникнути неефективності передрендерингу кожного можливого допису, архітектуру змінили так, щоб використовувати параметри запиту замість динамічних сегментів маршруту. Це дозволило віддавати один універсальний статичний HTML‑файл для всіх запитів контенту, а клієнтська програма вже наповнювала його конкретними даними.
Зрештою проєкт довелося покинути через нерозв’язний конфлікт із пошуковою оптимізацією (SEO), а саме з вимогами до метаданих Canonical URL.
Хоч більшість метаданих можуть залишатися статичними без суттєвого впливу, канонічний тег вимагає абсолютного URL, включно з повним доменом, щоб працювати коректно. В app‑директорії NextJS метадані слід визначати через generateMetadata або статичний експорт metadata — обидва виконуються виключно під час збірки для статичних експортів. Вбудованого механізму відкласти це на рантайм без Node.js‑сервера немає. Відповідно, у універсальному статичному експорті ім’я домену неможливо знати під час збірки.
Щоб виправити це в запропонованій архітектурі, Go‑сервер мав би парсити статичний HTML і під час виконання динамічно замінювати домен або схоже на домен плейсхолдерне значення всередині рядка канонічного тега. Від цього підходу відмовилися, адже він фактично вимагає реалізувати шаблонізацію на Go поверх статичного експорту. Така маніпуляція неелегантна і створює значний ризик проблем із гідратацією, коли клієнтський застосунок React не збігається з модифікованим HTML, надісланим сервером.
Якщо вам не потрібен SEO‑рейтинг, легкий рантайм із мульти‑env Docker‑образом цілком досяжний. Інакше це можливо лише дуже кострубуватими рішеннями, на кшталт заміни плейсхолдерів ENV усередині статичних .html і .js через кастомний рушій шаблонів.
Для застосунків NextJS, де потрібні динамічні змінні середовища на кшталт BACKEND_URL, API_ENDPOINT або кастомний rewrite, можна використати легкий кастомний сервер на Golang. Це просто і працює (~13 MB використання RAM). Є чимало обхідних шляхів для різних функцій.
Для сайтів, де SEO не важливий, є багато серверів, які можуть хостити ваш статичний експорт. Наприклад, в одному з моїх проєктів я використовую Rust-based static-web-server (~8 MB використання RAM).
Ось як ця спроба виглядала в цифрах:
Якби я прагнув будь‑якою ціною досягти бажаного результату, оновлення потребувало б приблизно у 4× більше доданих рядків і змін у ~100 файлах.
