Как только вы дойдете до определенной сложности в приложении Meteor, все начнет замедляться. Когда вы запускаете изменения в представлении, например, при изменении маршрутов, требуется некоторое время, чтобы новый материал появился на странице, а пока вы ждете, страница не отвечает - вы не можете прокручивать, касаться или пролистывать . Это происходит потому, что Blaze, который является частью Meteor, который реагирует на события браузера, запускает помощники ваших шаблонов, объединяет HTML и вставляет / удаляет HTML из DOM, выполняется в основном потоке, а когда Javascript выполняется в основной поток, браузер не может обновить страницу в ответ на ваши попытки прокрутки. Мне пришлось много поработать над скоростью рендеринга моего сложного мобильного приложения Meteor, Parlay, и я многому научился на этом пути ☺️ Мне даже выпала честь проанализировать его Пол Льюис. (Спасибо, Пол! Вы можете следить за ним и учиться у него на @aerotwist.) Если вы хотите проверить приложение на рабочем столе, URL будет https://parlay.io.

Саша попросил меня взглянуть на производительность рендеринга Telescope, его приложения Meteor с открытым исходным кодом, голосующего за новости, так что я взглянул и рассказал об этом в NYC Devshop. Я пытаюсь объяснять вещи по ходу дела, и ниже есть дальнейшие примечания. Комментируйте, задавайте вопросы, а я обновлю пост!

Основная рекомендация - оставлять заголовок в DOM при переключении маршрутов, поскольку повторная отрисовка меню категорий занимает большую часть односекундного ожидания смены маршрута. Рендеринг меню занимает некоторое время, потому что он вызывает помощников, которые выполняют множество запросов Minimongo для определения количества элементов в каждой категории и подкатегории. В Iron Router вы можете оставить заголовок в покое, используя layoutTemplate, в котором вы переходите к разделу, содержащему остальную часть страницы под заголовком. Вы также можете сделать это самостоятельно с помощью Template.dynamic.

Примечания к временной шкале

  • Я использую Chrome Canary, потому что его DevTools обычно имеют лучшие функции, чем обычный Chrome.
  • Я увеличиваю / уменьшаю масштаб с помощью колеса прокрутки и панорамирую с помощью перетаскивания, но есть также сочетания клавиш.
  • Просмотр имен функций на графике пламени обычно понятен только при запуске приложения в режиме разработки, когда JS не минифицирован.
  • Цвета полосок графика пламени соответствуют файлам, поэтому, если вы найдете одну полосу с Template.foo.helpers.bar, вы можете поискать другие полосы того же цвета, чтобы найти других помощников foo-template.
  • Страница индекса в основном занята просмотром строк сообщений. Чтобы проанализировать это, я бы выделил только одну строку, сделав что-то вроде:
{{#if session showPost}} 
  {{> post}}
{{/if}}
// in Chrome console
console.time('starting render'); Session.set('showPost',true);

Console.time предоставит вам метку начало рендеринга на шкале времени. Затем посмотрите на график пламени, найдите полоски для помощников шаблонов и посмотрите, какие из них требуют много времени, а какие называются , которые не нужно вызывать. Например, вот как я обнаружил, что когда вы увеличиваете лимит курсора, например, при бесконечной прокрутке, все помощники в существующих шаблонах запускаются повторно: meteor / issues / 4960. Кроме того, когда у вас есть URL-адрес, определяющий контекст данных для маршрута, и вы изменяете URL-адрес без изменения маршрута, весь контекст данных обычно становится недействительным, что заставляет всех ваших помощников перезапускаться. Решением этой проблемы является пакет Blaze +.

Идти дальше

Что вы можете сделать после того, как прекратили запускать помощники, которые не нужно запускать, и оптимизировали помощники, которые вам нужны, но требуют много времени, и у вас по-прежнему плохое время загрузки?

Компенсируйте / отвлекайте с помощью анимации

Когда пользователь взаимодействует со страницей, у вас есть 100 мс на ответ (см. Модель производительности RAIL или этот замечательный Курс Udacity). Когда это взаимодействие инициирует рендеринг нового шаблона Blaze, может пройти более 100 мс, прежде чем этот шаблон появится на экране. А пока вам нужно сделать что-то еще с текущей страницей. Вот пара примеров:

  • Эффект волн: на вкладке профиля индекса Parlay у вас есть список ваших собственных экспрессов. Нажатие на заголовок списка запускает Blaze для отображения раскрывающегося списка параметров фильтра списка. На некоторых устройствах раскрывающееся меню появляется с заметной задержкой. Чтобы компенсировать это, мы добавили анимацию волн материального дизайна, чтобы обеспечить обратную связь с пользователем, сообщая ему, что, когда они нажимают на заголовок списка, приложение получает их ввод. Следует отметить, что иногда Blaze блокирует выполнение анимации (потому что из-за этого браузер загружен JS, поэтому у него нет времени на анимацию). В таких случаях вы можете подождать, прежде чем запускать Blaze:
'click #settings-menu-link': (e) ->
  Waves.ripple e.currentTarget
  Meteor.defer -> // equivalent to setTimeout 0
    Router.go 'settings'
  • Открытие профилей. Когда вы касаетесь аватара пользователя, рядом с профилем этого пользователя появляется оверлей. На рендеринг их профиля уходит до нескольких секунд, поэтому у нас есть длительный период времени, в течение которого нам нужно их отвлечь. В этом случае я решил перевести и увеличить аватар, переместив его в положение, в котором их аватар в высоком разрешении появляется как часть их профиля. Обязательно используйте свойства только для композитора при анимации (например, преобразование), чтобы обеспечить визуальную плавность. Полный список свойств см. На http://csstriggers.com/.

Если вы не можете придумать подходящую анимацию, а задержка превышает секунду, то, по крайней мере, сообщите пользователю, что приложение не заморожено, чтобы он не волновался, пытаясь нажать и провести пальцем. на неотвечающем экране - например, вы можете немного затемнить экран и отобразить «загрузка» с помощью счетчика. Мы должны делать это, например, в нашей бесконечной прокрутке на лентах домашнего экрана - как только вы прокрутите вниз достаточно далеко, чтобы увидеть счетчик и срабатывает событие прокрутки (что в iOS означает, что страница должна завершить прокрутку), рендеринг дополнительных запускается parlays, который на многих телефонах занимает несколько секунд, в результате чего экран остается замороженным, что означает, что мы должны сообщить пользователю, что он не может взаимодействовать с приложением.

Подождите, пока поступят данные

При рендеринге списка элементов по частям, поскольку данные для этих элементов поступают через DDP, кажется, что это может занять больше времени, чем если вы ждете прибытия всех элементов, а затем визуализируете весь список сразу. И хотя первый метод показывает что-то на экране раньше, это не очень удобно для пользователя, потому что прокрутка - это нудная работа, потому что Blaze продолжает использовать основной поток для отображения вновь поступающих элементов списка. В нашем приложении Cordova мы ждем, пока будут готовы соответствующие подписки, прежде чем запускать рендеринг индекса, и мы ждем, пока индекс onRendered (когда он завершит рендеринг), прежде чем убрать экран запуска приложения.

Предварительный рендеринг

Обычное действие в Parlay - это / new для создания нового экспресса. Однако для рендеринга шаблона / new требуется пара секунд. Поэтому вместо рендеринга / new при изменении маршрута мы визуализируем его при начальной загрузке страницы индекса (пока открыт экран запуска) и скрываем его до тех пор, пока маршрут не изменится.

Мы используем пакет percolate: momentum для создания CSS-анимации, с которой вы делаете что-то вроде:

{{#momentum plugin='css'}}
  {{#if shouldShow}}
    {{> foo}
  {{/if}}
{{/momentum}}

Momentum замечает, когда шаблон foo переключается, и добавляет классы CSS, чтобы вы могли анимировать добавление и удаление шаблона. Но если рендеринг foo занимает много времени, вы заметите задержку перед запуском анимации. Решение состоит в том, чтобы не использовать импульс, а вместо этого запустить его скрытым с помощью CSS:

{{> foo shown=false}}
<template name="foo">
  <div class="foo {{showClass}}"></div>
</template>
Template.foo.helpers
  showClass: ->
    unless this.shown
      'hidden'

Это подводит нас к:

Оставьте это в DOM

Один из вариантов - оставить визуализированные шаблоны в DOM и не удалять или повторно отображать их - просто скройте их, пока вы не вернетесь к этому маршруту. Вот эксперимент Кадиры с этим. Пара проблем, с которыми вы можете столкнуться:

  • Работа может замедлиться, когда у вас огромное количество узлов DOM.
  • Увеличивается использование памяти, что на телефонах может привести к сбою браузера или содержащего его приложения Cordova. Например, в Parlay, хотя мы оставляем index и / new в DOM, мы не покидаем другие страницы, даже если есть заметная задержка при переходе, например, к instance / friends или / notifications. Когда мы попытались оставить больше страниц в DOM, приложение вылетело.

Рендеринг вне DOM

Та же самая проблема сбоя на мобильном устройстве случилась с Rocket Chat, клоном Slack с открытым исходным кодом, созданным с помощью Meteor, когда они попытались кэшировать шаблоны в DOM: https://youtu.be/yzkId54vng8?t=5m16s

Их решение - рендеринг вне DOM, и вот как это работает:

room.dom = document.createElement 'div'
# create a new instance of the room template
room.template = Blaze._TemplateWith { _id: rid }, ...
# render the template, inserting as a child to the div we 
# created at top
Blaze.render room.template, room.dom
# Now room.dom is a virtual DOM tree with the room UI.
# room.dom is just a variable in Javascript memory – it is not
# in the DOM / in the browser page.
# when the user opens this chatroom, put room.dom in the DOM
mainNode = document.querySelector('.main-content')
mainNode.appendChild room.dom
# when the user leaves the chatroom, remove that tree from the DOM
mainNode.removeChild mainNode.children[0]
# keep room.dom around so that when the user returns to the room, 
# we don't have to call Blaze.render again
# if you're in a browser tab that stays open for a long
# time, periodically clean up old rooms that haven't been
# used for a while:
# stop all the Tracker computations
Blaze.remove room.template
# remove from JS memory
delete room.dom
delete room.template

Вызов Blaze.render вручную (см. RoomManager.coffee:getDomOfRoom), вставка этого дерева DOM на страницу и удаление его со страницы, когда пользователь покидает комнату.

Безреактивный рендеринг

Еще одно решение от Rocket Chat - это нереактивный рендеринг, и я позволю Габриэлю объяснить это: https://youtu.be/yzkId54vng8?t=7m6s

Вот пакет: https://github.com/Konecty/meteor-nrr

Введите это в консоль браузера, чтобы узнать, сколько вычислений у вас активно:

Object.keys(Tracker._computations).length

Что касается контрольных точек, таблица лидеров имеет 52, демонстрация RocketChat - 600, версия Telescope, которую я тестировал выше, имеет 13,5 КБ, Parlay на настольных компьютерах - 46 КБ, а рекомендуемый предел Габриэля - 50 КБ.

Слишком много вычислений также является проблемой, с которой Asana столкнулась с их фреймворком Luna, подобным Meteor: https://blog.asana.com/2015/05/the-evolution-of-asanas-luna-framework/ (их эквивалент называется rvalue). Это была одна из причин, по которой они перешли на React для Luna2.

Рендеринг меньшего количества вещей

В Parlay в индексе мы отображали 20 экспрессов для друзей и общедоступных каналов, а также все ваши собственные экспрессы. Это отлично работало на настольных компьютерах, но либо длилось вечно, либо давало сбой в мобильных браузерах. Таким образом, в мобильных браузерах мы обрабатываем только четыре ваших собственных экспресса, а остальные оставляем до тех пор, пока вы не прокрутите страницу вниз или не перейдете к своим друзьям или общедоступным каналам. Хотя вы можете выполнить проверку пользовательского агента, чтобы узнать, используете ли вы мобильное устройство, на самом деле мы делаем кое-что более сложное:

Есть ряд вещей, которые вы, возможно, захотите сделать по-другому на более медленных устройствах - не просто меньше рендерить, но, возможно, делать меньше анимаций, подписываться на меньшее количество данных или поддерживать столько же вычислений. Возможно, вы захотите постепенно изменить эти вещи более динамичным способом, чем просто проверка isMobile. Существуют настольные компьютеры с низким уровнем ресурсов, и существует широкий диапазон мобильных сред выполнения JS, от последних устройств iOS до старых устройств Android (см. Сообщение Джеффа Этвудса о том, насколько плохо в Android JS). Внесение устройств в белый или черный список было бы беспорядочным решением. Вместо этого измерьте производительность! Вот что мы делаем при загрузке самой первой страницы в браузере нового клиента:

  • подождите, пока браузер будет простаивать (в будущем мы надеемся увидеть поддержку requestIdleCallback, которая сделает это проще / надежнее)
  • start = Date.now ();
  • визуализировать сложный шаблон (в нашем случае шаблон, отображающий каждый элемент фида экспрессов)
  • в шаблоне onRendered: end = Date.now (); localStorage.setItem («продолжительность», конец - начало);

Затем, например, если продолжительность рендеринга одного элемента была более 200 мс, рендеринг 4 элемента; если оно было между 200 мс и 50 мс, рендеринг 8; а в противном случае - 20.

Переписать

А если ничего не помогает, вы можете попробовать переписать в Angular или React, которые обычно имеют лучшую производительность рендеринга 😂 Тем не менее, вы все равно столкнетесь с некоторыми из тех же проблем (например, вызов помощников Minimongo требует времени для запуска), и в некоторых случаях React работает не так хорошо, как Blaze; например, в настоящее время отсутствуют детализированные изменения курсора, которые Blaze выполняет с #each.

Больше информации

Лучший общий ресурс по рендерингу - http://jankfree.org/.

Рассказ о том, почему MixMax отключил одну из своих страниц в Meteor для выступления.

Если вы используете React, ознакомьтесь с этой статьей о производительности рендеринга React.

Что касается производительности рендеринга без использования JS, ознакомьтесь с другим моим анализом: Масштабные эмодзи: производительность рендеринга спрайтов CSS по сравнению с отдельными изображениями

И если вам понравился этот пост, пожалуйста, потратьте секунду, чтобы нажать на сердце внизу и, может быть, на кнопку подписки 😄

@Lorendsr в Твиттере