Один из самых сложных аспектов работы инженера — это понимание того, когда нужно вводить абстракции, а когда следует сохранять простоту. Сколько раз вы вкладывали время в создание масштабируемой системы, планируя ее рост, который так и не был реализован? Нам, конечно, не чужда эта извечная инженерная дилемма.

Однако в конце 2020 г. мы решили, что пришло время инвестировать в масштабируемые системы, которые помогут продвинуть наши маркетинговые усилия в будущее. Наша контент-команда начала часто публиковать посты в блогах, руководства и создавать новые целевые страницы. Мы создавали большое количество контента для поддержки быстро растущей базы пользователей, но в то же время были обеспокоены тем, что проблемы с производительностью не позволяли нам охватить всю аудиторию.
Мы перестроили весь наш маркетинговый сайт с нуля, решив использовать статически генерируемую архитектуру вместо прежнего подхода, основанного исключительно на клиентском рендеринге. Спустя два месяца и 109 компонентов React мы полностью перешли на выбранный нами фреймворк Next.js и не можем нарадоваться своему решению. Вот как мы к этому пришли.
С чего мы начинали
В самом начале работы над Ноушен мы решили создать маркетинговый сайт как продолжение нашего основного приложения. Мы использовали существующие компоненты и инфраструктуру, что позволило нам создать большой сайт силами небольшой команды. Такой подход хорошо работал около двух лет, но со временем преимущества в производительности были сведены на нет множеством технических и пользовательских проблем, связанных с этим подходом.
Основным преимуществом нашего первоначального подхода, ориентированного на клиента, было удобство для разработчиков. Наше приложение и маркетинговый сайт имели одну большую папку с компонентами React. Если нам требовалось всплывающее меню на маркетинговом сайте, то, скорее всего, можно было использовать существующий компонент из приложения. Общий код также позволил реализовать некоторые интересные возможности для пользователей, например, встроить полное приложение в маркетинговый сайт в качестве живой демонстрации.

В итоге опыт разработчиков ухудшился. Мы чувствовали, что при реализации даже небольших вещей на маркетинговом сайте мы наследуем сложности из приложения. Например, мы могли импортировать из приложения компонент кнопки, который выглядел следующим образом:
<Button variant="marketingPrimary"onClick={() => soSomething()} mobileFeedback={() => doSomething()} allowTextSelection={() => doSomething()} onDoubleClick={() => doSomething()} onTouchStart={() => doSomething()} onTouchEnd={() => doSomething()} onTouchCancel={() => doSomething()} onContextMenu={() => doSomething()} >
Когда на самом деле для маркетинговых целей нам нужна была лишь кнопка с несколькими реквизитами, как здесь:
<Button variant="primary"onClick={() => doSomething()} >.
Дошло до того, что мы почувствовали, что всю нашу кодовую базу будет легче поддерживать, если разделить приложение и маркетинговый сайт. Маркетинговые команды должны быть оперативными, а существующая система не позволяла нам этого сделать.
Помимо технических проблем, наша реализация вызывала целый ряд проблем, связанных с работой пользователей. Вот лишь некоторые из них:
- Размер пакета JS — при первоначальной загрузке маркетингового сайта посетителям приходилось загружать файл app.js размером 9,1 мб, содержащий код всего приложения. Очень небольшая часть этого кода была связана с маркетингом.
- SEO — поскольку страницы отображались только на клиенте, возможность их просмотра поисковыми системами была в лучшем случае сомнительной. Google стал лучше справляться с распознаванием JS на стороне клиента, но ничто не может сравниться со статической или серверной страницей.
- Управление контентом — без системы сборки при каждом посещении клиента приходилось делать запросы к API нашей системы управления контентом (Contentful). Это приводило к миллионам ненужных обращений к API и загрузкам на простых маркетинговых страницах.
- Производительность — по вышеуказанным причинам наш показатель Google Lighthouse для маркетинговых страниц находился в районе 50/100.
Сочетание этих инженерных и пользовательских проблем привело к тому, что нам потребовался новый, более масштабируемый подход.
Поиск решений
Как и все важные решения в компании Ноушен, наше решение об интеграции генератора статических сайтов началось с RFC (запрос на комментарий, когда мы просим более широкую команду высказать свое мнение) в нашей базе данных документации.


После документирования возникших проблем мы столкнулись с двумя несовпадающими путями развития.
1. Оптимизация существующей кодовой базы клиентской части
В этом случае мы будем опираться на то, что уже имеем, а не прокладывать совершенно новый путь. Эта работа будет включать в себя:
- Использование разделения кода для уменьшения размера маркетингового JS-пакета.
- Реализация более эффективного кэширования активов для снижения веса страницы.
- Создание разграничения между маркетинговыми компонентами и компонентами приложения для снижения сложности наследования и риска влияния маркетинговых ошибок на приложение.
- Придерживаясь рендеринга на стороне клиента в надежде, что только повышение производительности улучшит SEO.
2. Переход на генератор статических сайтов
Такой подход предполагает создание сайта «с нуля», чтобы в полной мере использовать все преимущества статического сайта. Работа будет включать в себя:
- Перестройка маркетингового сайта с помощью генератора статических сайтов на основе JS, например Gatsby или Next.js.
- Перенесено около 109 компонентов React.
- Настройка нового процесса сборки и развертывания.
- Переосмысление редакционных процессов.
- Маршрутизация запросов к notion.so между отдельным клиентским и маркетинговым маршрутизатором.
Независимо от того, какой путь мы выбрали, нам предстояла работа. Взвесив все «за» и «против», мы решили, что дополнительные инвестиции, необходимые для перехода на полностью статическую версию, принесут долгосрочные дивиденды как для удобства пользователей, так и для производительности разработчиков. Это позволит нам быть более гибкими.
Выбор генератора статических сайтов
Мы начали следующую фазу процесса RFC с написания наших пожеланий к новому статическому сайту. Запись наших потребностей помогла выявить проблемы, которые мы надеялись решить. Мы не хотели выбирать самый блестящий новый инструмент с полки, если он не соответствовал нашим целям.
Наш список пожеланий к статическому сайту
- React-based — наши приложения работают на основе React. Наш маркетинговый сайт должен использовать ту же технологию.
- Поддержка TypeScript — вся наша кодовая база значительно выиграла от статической типизации. Эту возможность следует распространить и на маркетинговый сайт. Кроме того, у нас есть фрагменты кода и логики, которые все еще должны быть общими для приложения и маркетингового сайта.
- Интеграция с Contentful — наш контент живет здесь и должен быть легко интегрирован.
- Локализация — значительная часть пользователей Ноушен проживает за пределами США. Наш маркетинговый сайт должен быть полностью локализован, чтобы создать более удобные условия для этих пользователей.
- Полная поддержка CSS — нам необходима возможность использования псевдоселекторов и современных приемов, которые не могут быть выражены с помощью встроенных стилей.
- Рабочий процесс публикации — нашим создателям контента необходимо иметь возможность предварительного просмотра своих работ перед публикацией.
- Перспективность — речь идет о крупных инвестициях, и мы должны быть уверены, что выбранная нами система будет работать и дальше.
Эти параметры, естественно, сузились до нескольких претендентов.
Почему мы выбрали Next.js

После тщательного изучения и создания пробного концепта в Next.js мы поняли, что в нем есть много интересного:
- Фреймворк является легким и декларативным по своей природе. Он обрабатывает важные вещи: маршрутизацию, разбиение кода, генерацию статики, локализацию и оптимизацию изображений. После этого он уходит с дороги.
- Полная поддержка TypeScript.
- Документация и примеры кода превосходны, и мы почувствовали поддержку в процессе миграции.
- Серверный рендеринг изначально не был нам нужен, но мы были рады иметь его в нашем арсенале инструментов для использования в будущем.
- Интернационализированная маршрутизация поставляется «из коробки». Это значительно экономит время.
- Компонент изображения может интегрироваться с CloudFlare для кэширования активов и повышения производительности.
Практически во всех областях Next.js соответствовал нашим техническим целям.
Создание статического сайта
Перенос нашего маркетингового сайта на совершенно новый фреймворк был гигантской задачей. В течение двух месяцев над ним постоянно работали три инженера. Вместе мы выполняли миграцию и рефакторинг:
- 200 тыс.+ строк кода
- 109 Компоненты React
- 23 статические страницы
- 129 динамически генерируемых страниц
- 2 локали
Процесс миграции прошел гладко — в основном это было копирование/вставка кода или модификация функциональности в соответствии с лучшими практиками Next.js. Однако было несколько областей, которые требовали повышенного внимания.
Контроль версий
Вся наша кодовая база живет в одном монорепо. Веб-приложение, десктопные приложения, мобильные приложения — все. Мы недолго думали о том, чтобы завести новое репо специально для маркетингового сайта. Основное преимущество заключалось в том, что команда маркетологов могла бы автономно развертываться на платформе статического хостинга, такой как Vercel.
Мы хотели разделить приложение и маркетинговый сайт, но сочли раздельные репозитории излишними. В итоге мы осуществили свою мечту — выделили отдельный набор компонентов только для маркетинга, но нам все равно потребовался доступ к некоторым общим ресурсам. Такие вещи, как события аналитики, API и вспомогательные методы, необходимо было синхронизировать. Поэтому мы решили оставить монорепо и заняться развертыванием самостоятельно.
Маршрутизация
Добавление Next.js в наш стек означало, что нам нужно было сделать так, чтобы наш основной клиентский маршрутизатор знал о наших новых статически генерируемых маршрутах. Наше клиентское приложение и маркетинговый сайт находятся на одном домене, что несколько усложняет задачу. К счастью, решение оказалось довольно простым.
Мы настроили обратный прокси, который работает следующим образом:
- В систему notion.so поступает запрос.
- Сервер API проверяет запрос и разбирает путь и пользовательский агент.
- Мы проверяем путь запроса на соответствие разрешающему списку известных маркетинговых подпутей.
- Если путь и пользовательский агент квалифицируются как маркетинговый маршрут, мы направляем запрос в нашу маркетинговую службу.
- Если путь и пользовательский агент квалифицируются как маршрут приложения, api-сервер обрабатывает запрос напрямую.
Такой подход позволяет нам сохранить пользовательскую маршрутизацию в клиентском приложении и в то же время использовать преимущества динамически генерируемых маркетинговых маршрутов Next.js.
Хостинг и развертывание
Чтобы воспользоваться лучшими возможностями Next.js, наш маркетинговый сайт размещен в собственном контейнере Docker и развернут на AWS ECS. Наличие полноценной серверной среды позволяет использовать такие возможности, как режим предварительного просмотра, интернационализированная маршрутизация и SSR.
Вход в систему на производстве оказался более сложным, чем ожидалось. Мы создали пользовательскую точку входа на сервер специально для обработки ошибок и мониторинга производительности.
CSS
Одной из самых болезненных точек нашей предыдущей кодовой базы маркетинга была работа со стилями. В приложении в основном использовались реквизиты стилей React. Стили обычно возвращаются из функций:
class Button extends React.Component { // isHovered хранится в состоянии компонента здесь render() { return ( <button onMouseEnter={setIsHovered(true)} onMouseLeave={setIsHovered(false)} style={getButtonStyle(isHovered)}> Log in </button> ) } private getButtonStyle(isHovered: boolean): CSSProperties { return { height: 45, background: isHovered ? this.theme.buttonHoverColor : this.theme.buttonColor, fontSize: 16, fontWeight: bold, } } }
Этот подход отлично подходит для таких сложных приложений, как Ноушен, где большая часть стилей должна вычисляться во время выполнения, но он менее подходит для маркетингового сайта с более традиционным сценарием публикации.
В нашей кодовой базе маркетинга вместо встроенных стилей теперь используется Styled JSX. Это выглядит следующим образом:
const Button: FunctionComponent = () => { const theme = useTheme() return ( <> <button>Log in</button> <style jsx>{` button { height: 45px; background: ${theme.buttonColor}; font-size: 16px; font-weight: bold; } button:hover { background: ${theme.buttonHoverColor}; } `}</style> </> ) }
Мы были ошеломлены количеством доступных отличных вариантов CSS-in-JS, и нам было трудно выбрать только один. Мы остались очень довольны Styled JSX по нескольким причинам:
- Мы получаем возможность писать полноценный, настоящий CSS! Такие вещи, как псевдоселекторы и медиа-запросы, «просто работают».
- Стили по умолчанию являются компонентно-копируемыми, что устраняет неприятные ошибки специфичности.
- Он работает «из коробки» в Next.js. Никаких дополнительных пакетов не требуется.
- При необходимости мы все равно можем интерполировать значения из JS.
Новый подход к CSS сократил время разработки целевых страниц вдвое и позволил нам создавать более выразительные стили.
Переключение

Наиболее сложным аспектом этого проекта был его масштаб. Нам предстояло не только перестроить значительную часть сайта, но и одновременно поддерживать и обновлять существующий сайт.
Для того чтобы это стало возможным, необходимо было учесть несколько технических моментов:
- Новый статический сайт разрабатывался в том же репозитории. Все внешние зависимости оставались синхронизированными на протяжении всей сборки.
- Мы спрятали новую логику маршрутизации за экспериментом, что позволило нам включить новый сайт в нашей dev-среде и не включать его в production.
- Когда пришло время запуска, мы в течение двух недель наращивали трафик эксперимента, чтобы убедиться, что все работает как надо.
Такой подход позволил сделать переход абсолютно бесшовным и свести к нулю время простоя.
Результаты
После двух месяцев напряженной работы по миграции было очень приятно поделиться этим сообщением с командой в день запуска:

Это было забавное сообщение, потому что невооруженным глазом было видно, что на нашем сайте ничего не изменилось. Мы сознательно решили ограничить рамки этого проекта миграцией и улучшением производительности. Добавление дополнительных элементов дизайна усложнило бы процесс и усложнило бы оценку результатов.
Каковы же были результаты? Целый ряд невероятных количественных и качественных улучшений.

- Производительность — теперь у нас один из самых эффективных маркетинговых сайтов во всей отрасли. Наш предыдущий показатель Google Lighthouse для большинства страниц колебался в районе 50/100. Новый показатель для notion.so/product составляет 97/100. Мы планируем следить за этим показателем и улучшать его еще больше.
- Удобство для пользователей — больше нет единого загрузочного спиннера на всем маркетинговом сайте. Все предварительно рендерится, кэшируется CDN и доставляется мгновенно. Производительность — это особенность!
- Вес страницы — размер первоначально требуемого JavaScript уменьшился на 93% с 9,1 Мб до 847 Кб. Аналогичные улучшения наблюдаются на всем сайте. Общий размер файла notion.so/product уменьшился на 75% с 12,5 до 3,1 Мб.
- SEO — Теперь Google может полностью просматривать и индексировать наши маркетинговые страницы.
- Производительность разработчиков — теперь мы можем вносить масштабные изменения в кодовую базу маркетинга, не опасаясь, что они приведут к проблемам в приложении. Мы можем писать полноценный, современный CSS. И самое главное — мы можем смело запрашивать любой контент из нашей CMS, зная, что большая часть данных будет получена во время сборки, а не во время запроса.
С новой прочной основой мы хотим значительно ускорить процесс отгрузок маркетинговой команды. У нас большие планы, и нам нужна небольшая помощь, чтобы воплотить их в жизнь. Хотите присоединиться к нам? Посетите сайт notion.so/careers, чтобы узнать о текущих вакансиях.