Прикажи Vue.js не тратить попусту время и рендерить быстрее!

Roman Bushkoffsky
6 min readSep 28, 2018

--

Или “Как отучить VueJS делать множество настроек для отслеживания данных, которые никогда не изменятся”

Перевод статьи Jason Pettett: Tell Vue.js to stop wasting time, and render faster!

Предыстория.

Сегодня я работал над страницей которая отображала карту используя VueJS. Всякий раз когда загружалась страница был ужасный момент, когда она переставала отвечать на несколько секунд. Я новичок во VueJS, поэтому я был более чем уверен, что делаю что-то не так.

Это не относится к проблеме, но для понимания контекста — я использую Leaflet, чтобы отображать метки на карте. Поначалу я смотрел в сторону vue2-leaflet, но так как документация была недостаточно актуальна, я решил отказаться от этой идеи. Когда работаешь над коммерческим проектом, использовать библиотеку без легко доступной документации вряд ли будет хорошей идеей.

Проблема.

VueJS, как и React невероятно полезен для отслеживания изменений в данных. Когда данные меняются, фреймворк тут же обновляет вашу страницу. Но что случится, если у вас есть МНОГО данных? Скажем, 13 MB вложенных массивов и объектов которые не собираются изменяться, кроме как, может быть, полного их удаления.

Как я обнаружил эту проблему?
Вкладка Performance Profiling инструментов разработчика (Я использую Chrome, но и в Firefox есть что-то похожее).

Скриншот инструмента Performance Profiling запущенного на странице

В этой статье я не хочу вдаваться в детали того как использовать данный инструмент, но укажу вам на что необходимо обратить внимание. На скриншоте выше показан таймлайн событий, которые произошли, когда я загрузил свою страницу и какая из функций выполняется в какой момент времени. Области обведенные красным указывают на “лишнюю” работу VueJS.

У нас есть три AJAX запроса которые запрашивают данные с сервера во время загрузки страницы. Но что происходит со всеми этими вызовами “Observer”? Ответы от всех трёх AJAX запросов запускают излишнюю установку наблюдателей, а последний из них занимает около 6 секунд.

В течение этого времени страница не отвечает!

Добавление наблюдателей и слежение.
Что же, таким образом, выяснилось что VueJS тщательно наблюдает за изменением каждого кусочка данных. В большинстве случаев это как раз то, что нужно, но для того, чтобы следить за данными VueJS добавляет наблюдателей (observers).

Для начала будем думать о наблюдателе VueJS как о маленьком отслеживающем устройстве. Представьте что VueJS прикрепляет эти маленькие устройства везде для наблюдения за изменением данных.

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

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

Давайте посмотрим что VueJS делает на самом деле.
На скриншоте ниже мы остановили выполнение нашего скрипта после получения ответа от AJAX запроса. Часть кода Leaflet-специфична и не на все 100% относится к нашей проблеме, но это то место, в котором я обнаружил проблему.

Мы поместили данные в слой GeoJSON с помощью метода L.geoJson(). После этого мы передаём объект слою Controller используя layerControl.addOverlay()

Здесь наш код получает данные и передаёт их в Leaflet

Затем вызывается внутренняя функция Leaflet _addLayer()

Leaflet внутри себя управляет слоями, пока всё хорошо.
Затем создает объект содержащий наш слой (объект GeoJSON) и некоторые другие свойства.

Сейчас мы подошли к месту, где приходим к интересному выводу. Leaflet вызывает this._layers.push(), который должен просто добавить новый объект в конец обычного массива this._layers. Но на следующем скриншоте мы видим, что во VueJS мы получаем:

VueJS изменил данные во время добавления нового элемента в массив.
Как мы оказались внутри кода VueJS? Что же, когда VueJS запускается, он присоединяется к обычным методам массива, таким образом, каждый раз пользуясь push, pop, shift, unshift, splice, sort или reverseVueJS узнает об этом с помощью ob.dep.notify().

Это имеет смысл в большинстве случаев, например для элементов в todo-листе. Если вы вызываете reverse для массива todo-листа обычно вы ожидаете что VueJS заметит это изменение и зеркально отразит список на вашей странице.

Важное замечание, когда элемент добавлен в массив, до того как VueJS выполнит какие-либо действия основанные на данном изменении, фреймворк убедится что на этом элементе существует наблюдатель с помощью вызова observeArray(inserted).

VueJS отслеживает добавление нового элемента в массив

VueJS перебирает все элементы изArray.push() и вызывает observe() для каждого из них.

Сейчас обратите внимание на то как VueJS проверяет находится ли этот объект под наблюдением.

VueJS проверяет есть ли уже установленный Observer

Если на объекте уже есть атрибут под названием __ob__ и у него тот же тип что и у Observer VueJS, тогда VueJS знает что здесь уже есть наблюдатель для данных.

Решение в стиле: “Сделаем VueJS не настолько умным”.

Я остановился на очень простом решении. Во VueJS нет встроенного переключателя, чтобы указать “Нет, за этим следить не нужно”, но существует библиотека под названием “vue-nonreactive”.

Библиотека предоставляет удобную функцию Vue.nonreactive(…) которая берёт ваш объект и модифицирует его так, чтобы VueJS не отслеживал его. Очень интересно, как же она это делает? Это мы с вами и разберем далее.

Как работает Vue.nonreactive
Раздел этого пакета, под названием “Как это работает” очень хорошо описывает принцип действия, но если пояснение из документации не укладывается вам в голову, я разберу это здесь.

Когда VueJS устанавливает наблюдатели объекта, он проходит по всем атрибутам и конвертирует их в реактивные свойства. Включая также вложенные объекты .

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

Однако, VueJS пропустит объект, если поймёт что тот уже наблюдается.

VueJS не будет устанавливать два наблюдателя к одному и тому же объекту. Это было бы излишне.

Мы можем сделать объект “не реактивным” добавив поддельный observer, обманув VueJS.

Как предотвратить установку observer’a.
Библиотеку vue-nonreactive можно упростить до следующих строк кода.

Основной функционал vue-nonreactive

Функция makeNonreactive() выполняет ту же самую операцию что и вызов Vue.nonreactive() из vue-nonreactive, но извлеченная для того чтобы понять её работу.

Первая строка копирует функцию конструктора объекта Observer, создаёт поддельный объект Observer и записывает его в __ob__.

На этом всё. Вот и весь трюк. Но как же это работает?

Предположим мы запустили код выше для наших данных, сразу же после получения их из AJAX запроса. Затем взглянем на скриншот ниже, где VueJS добавляет наблюдателей.

Здесь VueJS проверяет не установлен ли уже Observer

На выделенной строке VueJS проверяет есть ли Observer двумя условиями:

Первое: VueJS проверяет наличие атрибута __ob__.
Наша функция makeNonreactive добавляет этот атрибут строкой value.__ob__ = new Observer({});

Второе: VueJS проверяет является ли value.__ob__ экземпляром Observer’а. С помощью строки
const Observer = (new App()).$data.__ob__.constructor; мы пройдём и эту.

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

К слову, если бы мы не добавили “поддельный” Observer в __ob__, то мы бы не прошли все условия и VueJS бы добавил настоящий наблюдатель (что включало бы в себя глубокое погружение в данные с добавлением observer’ов везде где только можно)

На этом всё? Мы решили проблему?
Если мы вызовем эту функцию (или используем библиотеку vue-nonreactive) для наших данных сразу после их получения все получится, не так ли?

Снимок графика быстродействия после использования нашей функции.

На картинке выше, мы видим результат нашей работы. Обработка третьего AJAX запроса начинается после 8000 мс. До изменений, выполнение этого кода занимало 6 секунд, но сейчас это менее чем пол секунды!

Я называю это успехом. Надеюсь сейчас вы стали лучше понимать принцип работы наблюдателей во VueJS и то как избавить фреймворк от лишней работы.

Спасибо за чтение!

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

Jason

Примечание переводчика: эта статья отличный пример того, что не нужно бояться заглядывать в код самого фреймворка/библиотеки, чтобы разобраться как внутри работает та или иная его часть.

Так же хотелось бы отметить такие возможные варианты решения данной проблемы как привязка полученных данных к внешней переменной и использование Object.freeze() который препятствует изменению свойств объекта (см. документацию).

--

--