28 |
29 |
30 |
101 |
--------------------------------------------------------------------------------
/src/router/routes.js:
--------------------------------------------------------------------------------
1 | import Home from '@/views/Home.vue'
2 | import Examples from '@/views/Examples.vue'
3 | import Example1 from '@/views/Example1.vue'
4 | import Example2 from '@/views/Example2.vue'
5 | import Example3 from '@/views/Example3.vue'
6 | import Example4p1 from '@/views/Example4p1.vue'
7 | import Example4p2 from '@/views/Example4p2.vue'
8 | import Example4p3 from '@/views/Example4p3.vue'
9 | import Example5 from '@/views/Example5.vue'
10 |
11 | export const routes = [
12 | {
13 | path: '/',
14 | name: 'home',
15 | component: Home
16 | },
17 | {
18 | path: '/examples',
19 | name: 'examples',
20 | component: Examples,
21 | children: [
22 | {
23 | path: '/example1',
24 | name: 'example1',
25 | component: Example1,
26 | meta: {
27 | title: 'Example 1. Object Watcher',
28 | description: `
29 | Item watcher over object-like variable can produce redundant runs.
30 | Creating the separate primitive-type computed especially for the watcher solves the issue.
31 | `,
32 | instructions: `
33 | Drag & drop items.
34 | There're 2 watchers: the first one over an itemIds object, the second one - over itemIdsTrigger string.
35 | Notice that both watchers run after drag & drop (console.log).
36 | Check any item.
37 | Notice that itemIds object watcher mistakenly detects the change during item check/uncheck while itemIdsTrigger string watcher runs only when it should.
38 | `
39 | }
40 | },
41 | {
42 | path: '/example2',
43 | name: 'example2',
44 | component: Example2,
45 | meta: {
46 | title: 'Example 2. Vuex + Object.freeze',
47 | description: `
48 | Vuex makes each object property observable (recursively) by default.
49 | In some cases that is not needed, and it leads to high memory usage.
50 | One solution could be using Object.freeze before storing objects in vuex.
51 | `,
52 | instructions: `
53 | Use chrome devtools memory tab. Add 100 items in a regular way. Take heap snapshot.
54 | Clear regular items, add 100 items using Object.freeze. Take another heap snapshot.
55 | Notice the difference.
56 | `
57 | }
58 | },
59 | {
60 | path: '/example3',
61 | name: 'example3',
62 | component: Example3,
63 | meta: {
64 | title: 'Example 3. Functional vs Map Getter',
65 | description: `
66 | Functional getters are not cached. They are just plain functions and revaluated every time they are called.
67 | `,
68 | instructions: `
69 | There's console.time invoked for itemById and itemByIds getters.
70 | Notice the first getter was called 10 times, the second one was called only once.
71 | `
72 | }
73 | },
74 | {
75 | path: '/example4p1',
76 | name: 'example4p1',
77 | component: Example4p1,
78 | meta: {
79 | title: 'Example 4.1. Incorrect Component Code Splitting',
80 | description: `
81 | Incorrect component-code splitting causes every component rerendering when only one item changes.
82 | `,
83 | instructions: `
84 | Add some items. Check an item, rename it or move.
85 | Notice that alongside with the target all the rest components trigger rerendering as well (use vue devtools performance component render tab).
86 | `
87 | }
88 | },
89 | {
90 | path: '/example4p2',
91 | name: 'example4p2',
92 | component: Example4p2,
93 | meta: {
94 | title: 'Example 4.2. Fixing Component Code Splitting - Stage 1',
95 | description: `
96 | Renaming can be fixed by referring to the original itemsByIds instead of the getter.
97 | But it does not solve rerendering that happens after checking an item.
98 | `,
99 | instructions: `
100 | Try to rename an item. It works correctly.
101 | Try to check an item. All the components mistakenly are rerendered.
102 | `,
103 | explanation: `
104 | Renaming works correctly because here we refer to the original itemsByIds object.
105 | Renaming mutation does not update the entire object - it updates only particular item inside.
106 | From the other side, isChecked computed refers to the entire checkedIds array while trying to find if the item is checked.
107 | `
108 | }
109 | },
110 | {
111 | path: '/example4p3',
112 | name: 'example4p3',
113 | component: Example4p3,
114 | meta: {
115 | title: 'Example 4.3. Fixing Component Code Splitting - Stage 2',
116 | description: `
117 | Rerendering can be fixed by providing exact related data by the parent component instead of searching the data from inside .
118 | `,
119 | instructions: `
120 | Try to rename, check or move an item. It works correctly - only the target item gets updated.
121 | `,
122 | explanation: `
123 | Here we provide the original item object and boolean isChecked prop by the parent list component.
124 | That granularity gives vue the possibility to detect if the has to be updated.
125 | `
126 | }
127 | },
128 | {
129 | path: '/example5',
130 | name: 'example5',
131 | component: Example5,
132 | meta: {
133 | title: 'Example 5. Intersection Observer',
134 | description: `
135 | Sometimes the DOM is heavy by itself.
136 | This trick shows how IntersectionObserver can be used to skip DOM updates for the nodes outside of the viewport.
137 | `,
138 | instructions: `
139 | Add some items. Each item toggles a heavy svg to blink every 500ms. Notice how lattency grows.
140 | Enable IntersectionObserver. It will hide svg-s outside of the viewport by using css display none.
141 | That helps performance a lot.
142 | `
143 | }
144 | }
145 | ]
146 | }
147 | ]
148 |
--------------------------------------------------------------------------------
/article.txt:
--------------------------------------------------------------------------------
1 | Улучшение производительности vue приложения
2 |
3 | У нас в TeamHood есть wiki.
4 | Там собралась коллекция рекоммендаций, в том числе, по улучшению производительности тяжелого фронтенда на vue.js.
5 | Улучшать производительность понадобилось, потому что в силу специфики наши основные экраны не имеют пагинации.
6 | Есть клиенты, у которых на одной kanban/gantt-доске больше тысячи вот таких вот карточек, все это должно работать без лагов.
7 |
8 | [img]
9 | - 2 rows
10 | - development board
11 | - subtasks + in progress, for pairtest
12 | -- resort rows;
13 | -- move task with 3 children to parent-child column
14 | -- assign first child
15 | -- resort children
16 | -- trigger submenu, assign a tag
17 | -- mark third child completed
18 |
19 | В статье разобрано несколько редко упоминаемых техник из нашей wiki, которые помогут сократить излишний рендеринг компонентов и улучшить производительность.
20 |
21 | https://kasheftin.github.io/vue-rerendering-optimization/
22 | https://github.com/Kasheftin/vue-rerendering-optimization
23 |
24 | Все примеры собраны в отдельном репозитории [link]. Это vue2 приложение, хотя все проверено и продолжает быть актуальным для vue3.
25 | По моему мнению, vue3 еще не production-ready. В vuex4 утекает память, исследовать соответствующие оптимизации там пока бессмысленно (что обнадеживает, затраты памяти там в разы меньше чем в vue2+vuex3).
26 | Примеры написаны на минимальном простейшем javascript, было искушение воткнуть vue-class-component, typescript, typed-vuex и остальную кухню реального проекта, но удержался.
27 |
28 | 1. (Deep) Object Watchers.
29 | Не использовать deep модификатор; использовать watch только для примитивных типов.
30 | Начнем с простого примера.
31 | Некий массив items приходит с сервера, сохраняется в vuex store, отрисовывается, возле каждого item есть чекбокс.
32 | Свойство isChecked относится к интерфейсу, хранится отдельно от item, однако есть getter, который собирает их вместе:
33 |
34 | ````
35 | export const state = () => ({
36 | items: [{ id: 1, name: 'First' }, { id: 2, name: 'Second' }],
37 | checkedItemIds: [1, 2]
38 | })
39 |
40 | export const getters = {
41 | extendedItems (state) {
42 | return state.items.map(item => ({
43 | ...item,
44 | isChecked: state.checkedItemIds.includes(item.id)
45 | }))
46 | }
47 | }
48 | ````
49 |
50 | Допустим, items могут быть отсортированы пользователем, и мы хотим сохранять порядок. Что-то вроде:
51 |
52 | ````
53 | export default class ItemList extends Vue {
54 | computed: {
55 | extendedItems () { return this.$store.getters.extendedItems },
56 | itemIds () { return this.extendedItems.map(item => item.id) }
57 | },
58 | watch: {
59 | itemIds () {
60 | console.log('Saving new items order...', this.itemIds)
61 | }
62 | }
63 | }
64 | ````
65 |
66 | В этом случае переключение чекбокса у любого item вызывается излишнее срабатывание сохранение порядка.
67 | Конструирование новых объектов - настолько естественный процесс, что даже в этом тривиальном примере мы делаем это дважды.
68 | Изменение checkedItemIds вызвает пересоздание массива extendedItems (и пересоздание каждого элемента этого массива), затем
69 | идет пересоздание объекта itemIds. Это может казаться контра-интуитивным, ведь создается массив, состоящий из тех же самых элементов в том же самом порядке.
70 | Однако, это природа javascript, [1,2,3] != [1,2,3].
71 |
72 | Решение - полный отказ от использования watcher для объектов и массивов.
73 | Для каждого сложного watcher создается отдельный computed примитивного типа.
74 | Например, если требуется отслеживать свойства {id, title, userId} в массиве items, можно сделать строку,
75 |
76 | ````
77 | computed: {
78 | itemsTrigger () { return JSON.stringify(items.map(item => ({ id: item.id, title: item.title, userId: item.userId }))) }
79 | },
80 | watch: {
81 | itemsTrigger () {
82 | // Здесь не нужен JSON.parse - дешевле пользоваться исходным this.items;
83 | }
84 | }
85 | ````
86 |
87 | Очевидно, чем точнее условие для срабатывания watcher, тем лучше, тем точнее он срабатывает.
88 | Объектный watcher - плохо, deep watcher - еще хуже.
89 | Использование deep в коде - частый признак неграмотности разработчика.
90 | Типа я не понимаю что делает этот код, какими объектами он оперирует, но что-то иногда не срабатывает, навешу-ка я deep - о вроде работает.
91 | Это что-то уровня.. (был у меня и такой проект).. в компоненте не срабатывала реактивность, и вместо того, чтобы найти ошибку, был повешен $emit('reinit'),
92 | по которому родительский компонент убивал данный и создавал его заново в $nextTick.
93 | Все это забавно мигало.
94 |
95 | 2. Ограничение реактивности (freeze).
96 | Использование Object.freeze на проекте TeamHood внезапно сократило потребление памяти в 2 раза.
97 |
98 | [Image before - after]
99 |
100 | Однако оно даже больше относится к моему второму основному проекту, StarBright, где используется nuxt и серверный рендеринг.
101 | Nuxt подразумевает, что некоторые запросы будут отрабатываться на сервере заранее.
102 | Ответы сохраняются в vuex store (и потом используются на клиенте).
103 | Таким образом, всю логику работы с запросами и кешированием данных удобнее держать в vuex.
104 | Компонент делает this.$store.dispatch('fetch', ...), а vuex отдает кеш или делает запрос.
105 | Следовательно, в vuex может содержаться большой объем данных.
106 | Например, пользователь вводил адрес, autocomplete загрузил массив городов, который был закеширован в store с целью избежать повторной загрузки.
107 | Данные статичны, однако vue по умолчанию делает реактивным каждое свойство каждого объекта (рекурсивно).
108 | Во многих случаях это приводит к высокому расходу памяти, и лучше пожертвовать реактивностью отдельных свойств.
109 | Вместо:
110 |
111 | ````
112 | state: () => ({
113 | items: []
114 | }),
115 | mutations: {
116 | setItems (state, items) {
117 | state.items = items
118 | },
119 | markItemCompleted (state, itemId) {
120 | const item = state.items.find(item => item.id === itemId)
121 | if (item) {
122 | item.completed = true
123 | }
124 | }
125 | }
126 | ````
127 |
128 | Делаем
129 |
130 | ````
131 | state: () => ({
132 | items: []
133 | }),
134 | mutations: {
135 | setItems (state, items) {
136 | state.items = items.map(item => Object.freeze(item))
137 | },
138 | markItemCompleted (state, itemId) {
139 | const itemIndex = state.items.find(item => item.id === itemId)
140 | if (itemIndex !== -1) {
141 | // Не получится делать item.completed = true (объект заморожен), нужно пересоздать весь объект;
142 | const newItem = {
143 | ...state.items[itemIndex],
144 | completed: true
145 | }
146 | state.items.splice(itemIndex, 1, Object.freeze(newItem))
147 | }
148 | }
149 | }
150 |
151 | Замечу, что замерять расход памяти нужно на build-версии (не в development).
152 |
153 | 3. Функциональные геттеры.
154 | Иногда это пропускают в документации (https://vuex.vuejs.org/guide/getters.html#method-style-access).
155 | Функциональные геттеры не кешируются. Вот это:
156 | ````
157 | // Vuex:
158 | getters: {
159 | itemById: (state) => (itemId) => state.items.find(item => item.id === itemId)
160 | }
161 | ...
162 | // Some component:
163 | computed: {
164 | item () { return this.$store.getters.itemById(this.itemId) }
165 | }
166 | ````
167 | будет делать items.find для каждого компонента .
168 | Вот это:
169 | ````
170 | getters: {
171 | itemByIds: (state) => state.items.reduce((out, item) => {
172 | out[item.id] = item
173 | return out
174 | }, {})
175 | }
176 | // Some component:
177 | computed: {
178 | item () { return this.$store.getters.itemsByIds[this.itemId] }
179 | }
180 | ````
181 | выполнит itemsByIds при первом обращении и закеширует результат. Таким образом, функциональные геттеры не имеют никаких преимуществ перед обычными методами/функциями.
182 |
183 | 4. Грамотное распределение на компоненты.
184 | Компоненты - ключевая часть экосистемы vue.
185 | Понимание жизненного цикла и критериев обновления (shouldComponentUpdate) необходимо для строительства эффективного приложения.
186 | Первое знакомство с компонентами проходит на интуитивно-логическом уровне: есть какие-то однотипные контейнеры, тогда наверное для контейнера лучше сделать отдельный компонент.
187 | Однако, кроме смыслового значения, компоненты - это мощный механизм, дающий контроль над гранулярностью обновлений, это штука, напрямую влияющая на производительность.
188 | Для примера возьмем itemByIds getter из предыдущего пункта и рассмотрим такой код:
189 | ````
190 | // App.vue
191 |
217 |
218 | Но это не поможет. vue вызывает обновление компонента, если его зависимости меняются. При изменении любого свойства любого item пересоздается
219 | объект itemsByIds, каждый элемент которого - это новый объект { ...item, isChecked: .. }, а поскольку {...item} !== {...item}, идет ререндеринг.
220 |
221 | Каждый vue компонент - это функция, которая отдает virtual DOM и кеширует его (memoization).
222 | Входные аргументы функции - зависимости - отлеживаются на этапе dry run и состоят из ссылок на переменные в props и $store.
223 | Если входной аргумент - поэлементно равный предыдущему новый объект, кеширование не срабатывает.
224 |
225 | Есть два варианта, как это победить - глупый и умный. Очевидно, если сделать
256 |
257 | Демонстрация правильной работы: пример 1, пример 2.
258 |
259 | 5. Применение Intersection Observer.
260 | Отрисовка большого DOM-дерева тормозит сама по себе. Мы применяем несколько техник для оптимизации.
261 | Например, на gantt схемах размеры и положения блоков заранее расчитаны, поэтому известно, что попадает в viewport. Невидимые элементы не отрисовываются.
262 | В других случаях размеры заранее неизвестны, тогда можно применить этот простой прием с intersection observer.
263 | В vuetify есть v-intersect директива, которая работает из коробки, однако она создает отдельный IntersectionObserver на каждый свой биндинг, поэтому не подходит для случая, когда объектов много.
264 | Вот пример, который будем оптимизировать. Там 100 элементов (на экране помещается 10), в каждом мигает тяжелая картинка, замеряется задержка между реальным миганием и расчетным.
265 | Создадим один экземпляр IntersectionObserver и пробросим его через директиву во все узлы, которые он будет отслеживать.
266 | Все, что нужно от директивы - добавиться в IntersectionObserver и запомнить это:
267 |
268 | export default {
269 | inserted (el, { value: observer }) {
270 | if (observer instanceof IntersectionObserver) {
271 | observer.observe(el)
272 | }
273 | el._intersectionObserver = observer
274 | },
275 | update (el, { value: newObserver }) {
276 | const oldObserver = el._intersectionObserver
277 | const isOldObserver = oldObserver instanceof IntersectionObserver
278 | const isNewObserver = newObserver instanceof IntersectionObserver
279 | if (!isOldObserver && !isNewObserver) || (isOldObserver && (oldObserver === newObserver)) {
280 | return false
281 | }
282 | if (isOldObserver) {
283 | oldObserver.unobserve(el)
284 | el._intersectionObserver = undefined
285 | }
286 | if (isNewObserver) {
287 | newObserver.observe(el)
288 | el._intersectionObserver = newObserver
289 | }
290 | },
291 | unbind (el) {
292 | if (el._intersectionObserver instanceof IntersectionObserver) {
293 | el._intersectionObserver.unobserve(el)
294 | }
295 | el._intersectionObserver = undefined
296 | }
297 | }
298 |
299 | Теперь известно, какие элементы списка не видны, вопрос, как их облегчать.
300 | Можно, например, менять тяжелый компонент на легкую заглушку или убирать какие-то части.
301 | Однако важно понимать, что сложный компонент сложно отрисовывать.
302 | При быстром скролинге список затупит из-за большого количества инициализаций и деинициализаций.
303 | Практика показывает, что хорошо работает скрытие на уровне css:
304 |
305 |
306 |