React и Code Splitting
С code splitting я познакомился очень давно, в году так 2008, когда Яндекс немного подвис, и скрипты Яндекс.Директа, синхронно подключенные на сайте, просто этот сайт убили. Вообще в те времена было нормой, если ваши «скрипты» это 10 файлов которые вы подключаете в единственно правильном порядке, что и до сих пор (с defer) работает просто на ура.
Потом я начал активно работать с картами, а они до сих пор подключаются как внешние скрипты, конечно же lazy-load. Потом уже, как член команды Яндекс.Карт, я активно использовал ymodules возможность tree-shaking на клиенте, что обеспечивало просто идеальный code splitting.
А потом я переметнулся в webpack
и React
, в страну непуганных идиотов, которые смотрели на require.ensure
как баран на новые ворота, да и до сих пор так делают.
Code splitting — это не «вау-фича», это «маст хэв». Еще бы SSR
не мешался…
Маленькое введение
В наше время, когда бандлы полнеют с каждым днем, code splitting становиться как никогда важен. В самом начале люди выходили из данной ситуации просто, создавая отдельные entrypoint, для каждой страницы своего приложения, что вообще хорошо, но для SPA работать не будет.
Потом появилась функция require.ensure
, сегодня известная как dynamic import
(просто import), посредством которой можно просто запросить модуль, который чуть позже и «получите».
Первой библиотекой «про это дело» для React был react-loadable, хайп вокруг которой мне до сих пор не очень понятен, и которая уже сдохла (просто перестала нравиться автору).
Сейчас более менее «официальным» выбором будут React.lazy
и loadable-components (просто @loadable
), и выбор между ними очевиден:
- React.lazy совсем никак не может SSR (Server Side Rendering), от слова вообще. Даже в тестах упадет без особых плясок с бубном, типа «синхронных промисов».
- Loadable SSR может, и при этом поддерживает Suspense, те ничем не хуже React.Lazy.
В том числе loadable поддерживает красивые обертки над загрузкой библиотек (loadable.lib, можно увести moment.js в React renderProp), и помогает webpackу на стороне сервера собрать список использованных скриптов, стилей и ресурсов на префетч (чего сам webpack не очень умеет). В общем читайте официальную документацию.
SSR
В общем — все проблема в SSR. Для CSR (Client Side Render) сгодиться или React.lazy или маленький скрипт в 10 строчек — этого точно будет вполне достаточно, и подключать большую внешнюю библиотеку смысла не имеет. Но на сервере этого будет совсем не достаточно. И если вам SSR особо не нужен — дальше можно не читать. У вас нет проблем, которые надо долго и упорно решать.
SSR это боль. Я (в неком роде) один из маинтейнеров loadable-components и это просто жуть сколько багов вылезает из разных мест. И с каждым обновлением webpack прилетает еще больше.
SSR + CSS
Еще больший источник проблем при SSR — это CSS.
Если у вас Styled-components — это не так чтобы больно — они поставляются с transform-stream
который сам добавит в конечный код что надо. Главное — должна быть одна версия SC везде, иначе фокус не получится. Буду честен — именно из-за этого ограничени он обычно и не получается.
C emotion попроще — их styled
адаптер просто выплюнет перед самом компонентом, и проблема решена. Просто, дешево и сердито. В принципе очень mobile-friendly, и сильно оптимизирует самый-первый-view. Но немного портит второй. Да и лично мне совесть не позволяет так стили инлайнить.
С обычным CSS (в том числе полученым из CSS-in-JS библиотек различной магией) все еще проще — информация о них есть в графе webpack, и «известно» какие CSS нужно подключить.
Порядок подключения
Вот тут собака и зарылась. Когда что надо подключать?
Смысл SSR friendly code splitting в том и заключается, что перед тем как звать ReactDOM.hydrate
надо загрузить все «составные части», которые уже присуствуют в ответе сервера, но не по силам скриптам загруженным на клиент в данный момент.
Посему все библиотеки предлагают некий callback который будет вызван когда все-все-все что надо загрузилось, и можно стартовать мозги. В этом и есть смысл работы SSR codesplitting библиотек.
JS можно загружать когда угодно, и обычно их список добавляют в конец HTML, а вот CSS, чтобы не было FOUC, надо добавлять в начало.
Все библиотеки умеют это делать для старого renderToString
, и все библиотеки не умеют это делать для renderToNodeStream
.
Это не беда если у вас только JS (такого не бывает), или SC/Emotion (которые сами себя добавят). Но — ежели у вас «просто CSS» — все. Или они будут в конце, или прийдется использовать renderToString
, или другую буферизацию, что обеспечит задержку TTFB (Time To First Byte) и чуток уменшит смысл вообще этот SSR иметь.
Ну и конечно — все это завязано на webpack и вообще никак иначе. Посему, при всем уважению к Грегу, автору loadable-components — предлагаю рассмотреть другие варианты.
Далее идет повестнование из трех частей, основная идея которого сделать что-то не убиваемое и не зависимое от бандлера.
1. React-Imported-Component
React-Imported-Component — не плохой «loader», с более менее станданым интерфейсом, крайне схожим с loadable-components, который может SSR для всего что шевелится.
Идея очень простая
Не надо парсить stats.json
, адаптируюсь под оптимизации webpack (concatenation, или common code) — надо просто сопоставить «метку» одного импорта в ключем в массиве и выполнить импорт еще раз. Как он будет выполнен в рамках конкретного бандлера, сколько файлов на самом деле будут загружены и от куда — не его проблема.
Минус — начало загрузки «использованных» чанков происходит после загрузки основного бандла, который хранит маппинг, что немного «позже» чем в случае loadable-components, который эту информацию добавит прямо в HTML.
Да с CCS это не работает от слова никак.
2. used-styles
А вот used-styles работает только с CSS, но примерно так же как react-imported-components.
- сканирует все css (в директории билда)
- запоминает где какой класс определен
- сканирует выходной renderToNodeStream (или ответ
renderToString
) - находит class='XXX', сопоставляет файл и выплевывает его в ответ сервера.
- (ну и потом телепортирует все такие стили в head, чтобы не сломать hydrate). Style Components работают точно также.
Задержки TTBT нет, к бандлеру не привязно — сказка. Работает как часики, если стили нормально написаны.
Пример работы react-import-component+used-styles+parcel.
Не самый очевидный бонус — на сервере обе библиотеки сделают «все что нужно» во время стартапа, до того момента как express server сможет принять первого клиента, и будут полностью синхроны и просто на сервере, и во время тестов.
3. react-prerendered-component
И замыкает тройку библиотека, которая делает «partial rehydration», и делает это настолько дедовским способом, что я прям диву даюсь. Она реально добавляет «дивы».
- на сервере:
- оборачивает некий кусок дерева в див с «известным id»
- на клиенте:
- конструкторе компонента находит «свой» див
- копирует его innerHTML, до того как «React» его пережует.
- использует этот HTML до тех пор, пока клиент не готов его
hydrate
- технически это позволяет использовать Hybrid SSR (Rendertron)
const AsyncLoadedComponent = loadable(() => import('./deferredComponent'));
const AsyncLoadedComponent = imported(() => import('./deferredComponent'));
// meanwhile you will see "preexisting" content
С loadable-components этот фокус не работает, так как он не возвращает из preload promise. Особо это важно для библиотек типа react-snap (и других «пререндеров»), которые имеют «контент», но не прошли через «настоящий» SSR.
С точки зрения кода — это 10 строк, плюс еще немного чтобы получить стабильные SSR-CSR UID с учетом рандомного порядка загрузки и испольнения кода.
Бонусы:
- можно не ждать «загрузки всех скриптом» перед стартом мозгов — мозги будут запускаться по мере готовности
- можно вообще не загружать мозги, так и оставляя данные SSR-ed (если SSR версии нет то мозги будут таки загружены). Прям как во времена jQuery.
- можно еще потоковое кеширование крупных рендер блоков реализовать (теоритически Suspence-compatible) — опять же средствами transform stream.
- и сериализацию/десериализацию стейта в/из HTML, как во время jQuery
В принципе сериализация и десериализация и были основной идеей создания библиотеки, чтобы решить проблему дублирования стейта (картинка из статьи про SSR). Кеширование прилетело потом.
Итого
Итого — три подхода, которые могут изменить ваш взгляд на SSR и code splitting. Первый работает с JS codesplitting, и не ломается. Второй работает с CSS codesplitting, и не ломается. Третий работает на уровне HTML упрощая и ускоряя некоторые процессы, и опять же, не ломается.
Ссылки на библиотеки:
Статьи (на английском)