На Хабре есть несколько статей про изоморфность, которые на вопрос, что такое изоморфность, отвечают: “Изоморфность — это когда один и тот же код используют и на клиенте, и на сервере”. Да, это так, но не совсем понятно, для чего она вообще нужна. Собственно на этой почве и была рождена эта статья.

Фреймворки, используемые на стороне клиента — великолепны. Они могут помочь вам построить интерактивное и быстрое веб-приложение, которое будут обожать пользователи.

К сожалению, этот мир не идеален, и у них существует несколько недостатков. Один из главных — это скорость начальной загрузки.

image

Клиентские фреймворки получают очень маленький HTML файл, в котором подключены стили и пару скриптовых файлов, содержащий единственный div для монтирования приложения. Однако файл с JavaScript (bundle), содержащий даже минифицированый код зачастую очень большой и может достигать нескольких мегабайт.

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

С другой стороны, традиционные веб сайты рендерят все на сервере и, как только HTML будет доставлен на клиентскую машину, пользователь увидит страницу. Более того, большинство веб серверов могут сделать рендер страницы быстрее, чем машина пользователя. В результате всего этого начальная загрузка страницы происходит очень быстро.

Спасительный React


Конечно же, вы бы хотели иметь преимущества обоих подходов. Быстрая начальная загрузка и быстрое и интерактивное приложение.
React может помочь с этим.

Вот как это работает. React имеет возможность отрендерить любой компонент, включая его данные на стороне сервера. То есть React на стороне сервера собирает дерево компонентов и превращает их в HTML каркас. Все это мы кладем в index.html, который отправиться пользователю. Также этот файл хранит в себе данные приложения для рендера на клиенте.

Как только HTML будет доставлен пользователю, браузер отобразит страничку. В это время все еще за кулисами будет подгружаться “тяжелый” файл JavaScript и, как только он загрузится, React сделает такие же расчеты для рендера локально. Умный алгоритм React понимает, что результат локального рендера соответствует тому, что уже было отображено на странице. В результате этого сравнения React не будет делать никаких изменений, а просто добавит необходимые обработчики событий к элементам.

В этом случае HTML файл, конечно, становиться немного “тяжелее”, но это просто не сравнить с размером файла, содержащего JavaScript. В результате HTML файл доставляется очень быстро.

Насколько это быстро? Разве мы не делаем почти одно и то же в двух местах? Да, но почти здесь ключевое слово.

Во первых — как только сервер ответит на запрос браузера, пользователь тут же увидит страничку.

Во вторых — React идентифицирует, что изменения в DOM ненужны, и поэтому не трогает его, а ведь это самая медленная часть рендеринга.

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

image

  1. Начало загрузки страницы.
  2. HTML файл загружен, начало загрузки стилей и js.
  3. Загружены стили и пользователь уже видит красивую страничку, JS файл тем временем продолжает загружаться за кулисами (страничка безжизненна).
  4. JS файл загружен и реакт начинает рендер и сравнение виртуального DOM с отображенным.
  5. React понимает что различий нет и просто подключает обработчики событий к уже существующей разметке. Пользователю доступна полноценная страничка.


Возможно ли такое, что, пока JavaScript загружается, а страничка уже отображена, пользователь не сможет взаимодействовать с ней, потому что обработчики событий еще не прикреплены на страничку?

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

В конечном счете ваше приложение будет всегда интерактивным и ни один из ваших пользователей не столкнется с проблемами, время загрузки страницы будет предельно маленьким, а пользователи бесконечно счастливыми!
Поделиться с друзьями
-->

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


  1. vintage
    18.05.2016 19:13
    +9

    Есть решение лучше — не писать мегабайты кода. :-)


    Насчёт реакта — вы теоретизируете или реально использовали его в таком сценарии? Я вот слышал про проблемы с проверкой чексум, из-за несовпадения которых реакт таки перерендеривает всё заново. Также есть проблема изоморфной подготовки данных (изоморфные модели?).


    1. Arilas
      18.05.2016 20:29
      +1

      Если использовать redux, то состояние стора спокойно ложится в какой-нибудь __INITIAL_STATE__ или что-то подобное. Поэтому проверка чексум пройдет.

      Насчет подготовки данных, то с этим не так все плохо, никто не запрещает на сервере сделать middleware наподобии:

      app.use(function(req, res) {
         req.store = configrureStore()
         res.renderMarkup = function() {
           res.send(renderFullPage(<App />, req.store.getState())
         }
      })
      


      И далее в каком-то роуте:
      app.get('/some', async function(req, res) {
        req.store.dispatch(await loadSome())
        req.store.dispatch(await doAnotherAsync())
        res.renderMarkup()
      })
      app.get('*', (req, res) => {
        res.renderMarkup()
      })
      


      Пример странный, но можно использовать асинхронные вещи.


      1. vintage
        19.05.2016 09:35

        У вас получается код одинакового наполнения стора разный для клиента и сервера. И вам необходимо поддерживать их точную эквивалентность.


        1. PSDCoder
          19.05.2016 12:59

          Я делаю сейчас по другому:

          Много кода
          class BlogPage extends Component {
              componentDidMount() {
                  this.props.fetchPosts(this.props.params.page);
              }
          }
          
          BlogPage.dispatchOnServer = [
              params => fetchPosts(params.page)
          ];
          

          и на сервере используется функция которая резолвит все dispatchOnServer компонентов:
          export default (dispatch, components, params) => {
              const needs = components.reduce((prev, current) =>
                  (current ? (current.dispatchOnServer || []).concat(prev) : prev),
                  []
              );
              const promises = needs.map(fn => {
                  let action = fn(params);
          
                  if (isFunction(action)) {
                      action = action(dispatch);
                  } else {
                      dispatch(action);
                  }
          
                  if (!('promise' in action)) {
                      throw new Error('dispatchOnServer action MUST have \'promise\' key');
                  }
          
                  return action.promise;
              });
          
              return Promise.all(promises);
          };
          

          При этом action'ы выглядят так:
          export function fetchPosts(offset) {
              return dispatch => dispatch({
                  type: actions.FETCH_POSTS,
                  offset,
                  promise: fetch(`${constants.API}/posts?offset=${offset}&limit=${constants.POSTS_PER_PAGE}`)
                      .then(responseData => dispatch({
                          type: actions.FETCH_POSTS_SUCCESS,
                          posts: responseData.data,
                          total: responseData.total
                      }))
                      .catch(error => dispatch({
                          type: actions.FETCH_POSTS_FAILURE,
                          error: apiHelpers.formatResponseError(error)
                      }))
              });
          }
          

          Ее использование:
          app.get('*', (req, res) => {
              const memoryHistory = createMemoryHistory(req.url);
              const store = initReduxStore(memoryHistory);
              const location = req.url;
              const history = syncHistoryWithStore(memoryHistory, store);
          
              match({ routes, location, history }, (error, redirectLocation, renderProps) => {
                  if (error) {
                      return res
                          .status(500)
                          .send(error.message);
                  }
          
                  if (redirectLocation) {
                      return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
                  }
          
                  if (renderProps) {
                      return fetchComponentsData(store.dispatch, renderProps.components, renderProps.params)
                          .then(renderTemplate.bind(null, store, renderProps))
                          .then(dataToServing => res
                              .status(dataToServing.status)
                              .end(dataToServing.html)
                          )
                          .catch(renderError => res.status(500).end(renderError.message));
                  }
          
                  return res.status(404).send('Not found');
              });
          });
          



  1. heilage
    18.05.2016 19:37
    +5

    Упоминание реакта в данном контексте несколько странно. Да, он может в изоморфность, однако это лишь одно из специфических решений для seamless-отрисовки как на клиенте, так и на сервере. Но на отрисовке свет клином не сошелся, изоморфность это также и про получение данных, и про их обработку (читай — бизнес-логику). И там тоже есть свои грабли, тысячи их.


  1. sosnovskyas
    18.05.2016 21:31
    +1

    Но! С другой стороны пример с React очень показателен, ибо он как известно именно V(iew), собственно то что мы видим, о чём собственно и шла речь выше — отображение.


  1. summerwind
    19.05.2016 02:36

    Я вот не совсем согласен, что главное достоинство изоморфности — это скорость начальной загрузки страницы. Потому что js-файл приложения будет грузиться только 1 раз, при самом первом открытии сайта пользователем. Во всех остальных случаях он будет подгружаться из кэша браузера, а с сервера будет запрашиваться только легкий json. И вот когда js уже в кэше нашего браузера, и мы жмем f5 для полной перезагрузки страницы, то далеко не факт, что рендеринг всей страницы на сервере будет быстрее, чем запросить json (вместо того чтобы тянуть с сервера весь html целиком) и вставить его на клиенте в нужные блоки.
    На мой взгляд, гораздо более полезная плюшка изоморфности — это решение проблем с поисковиками, потому что мы пишем код только один раз, и он дает один и тот же результат и поисковику и человеку в браузере. Это избавляет нас от костыльных решений типа генерации снапшотов страниц по крону, или хуже того, написания второго приложения специально для поисковиков.


    1. Tramway
      19.05.2016 09:30

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


      1. summerwind
        19.05.2016 14:22

        Если у браузера ресурсов мало, то и процесс будет долгий

        В том и дело, что слишком много «если». Особенно в 2016-м году, когда даже телефоны имеют 4-ядерные процессоры. А если у меня мощный компьютер, а интернет так себе? Тогда мне быстрее будет получить данные через json и отрендерить страницу у себя, чем тянуть полностью весь html с сервера (и опять же, не в 100% случаях это будет так).

        ничего не мешает кэшировать html, который отдает сервер

        Кэшировать отрендеренный сервером html на стороне клиента? В таком случае, никто не мешает и кэшировать html, отрендеренный клиентом при первой загрузке страницы.

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

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


    1. Balek
      19.05.2016 09:33

      Пользователи GMail с вами не согласны. Если нужно открыть несколько писем в новых вкладках, ждать придётся прилично долго. Поэтому, на мой взгляд, генерация html на сервере — часть юзабилити в такого рода веб-приложениях. Т.е. даже в случае, когда индексация поисковиками не нужна.


  1. i360u
    19.05.2016 10:00
    +4

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


  1. shanhaichik
    19.05.2016 10:01

    При работе с готовыми gui компонентами. Например material ui, есть проблема, была раньше, чексум из-за вендорных префиксов. На сервере их не было, на клиенте были, в связи с этим суммы не сходились.


  1. Riim
    19.05.2016 16:51
    +2

    Все эти схемочки очень интересны конечно, но в них не учитывается один момент — запросы к АПИ/базе при рендеринге на сервере. И эти запросы (их обычно много) обычно получаются заметно медленнее чем скачивание бандла (рендеринг происходит после самого долгого из них). То есть при обычном подходе пользователь скачивает бандл, дальше у него рендерятся какие-то блоки, которые отправляют запросы за своими данными, выводят лоадер и по мере получения данных раздупляются, тут пользователь уже получает какую-то обратную связь, он видит, что что-то грузится, в случае изоморфного приложения здесь пользователь всё ещё видит белый экран. Первые полностью готовые блоки обычно получается быстрее чем рендеринг на сервере, хотя полностью готовый прил в случае с изоморфностью будет конечно раньше. В общем, без прогрессивного рендеринга вся эта изоморфность может быть интересна только для SEO (и то слишком дорогое SEO выходит, проще по старинке), в ощущаемой скорости загрузки изоморфные приложения всё же проигрывают. А понимая как работает реакт я сомневаюсь, что в него можно нормально этот прогрессивный рендеринг вкорячить, пока видел только одну попытку, и явно неудачную. Вот в этой ветке https://habrahabr.ru/post/280636/#comment_8882284 я как раз встал на сторону реакта (для выяснения истины так сказать) и получил ровно то, чего сам и придерживаюсь.