Расскажите о случае, когда вы улучшали performance, accessibility, testing или maintainability в проекте на Vue.js.
В проекте на Vue.js оптимизировал производительность через lazy-loading маршрутов и виртуализацию списков, улучшил доступность добавив ARIA-атрибуты и фокус-менеджмент, покрыл компоненты тестами на Vitest и разбил монолитный компонент на составные части.
Улучшение производительности, доступности, тестирования и поддерживаемости в Vue.js-проекте
На одном из проектов — маркетплейс с большим каталогом товаров — я последовательно улучшил четыре аспекта качества кода и пользовательского опыта. Ниже описываю конкретные изменения по каждому направлению.
Производительность (Performance)
Главной проблемой было медленное первоначальное время загрузки: бандл весил 1.8 МБ. Я применил несколько техник:
- Lazy-loading маршрутов через динамический
import()в Vue Router — каждая страница превратилась в отдельный чанк. - Виртуализация списков с помощью библиотеки
vue-virtual-scrollerдля каталога из 10 000+ товаров. defineAsyncComponentдля тяжёлых модальных окон, которые подгружаются только при открытии.- Замена
v-ifнаv-showдля часто переключаемых блоков и наоборот — для редко видимых.
// router/index.js — lazy-loading страниц
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/catalog',
component: () => import('@/pages/CatalogPage.vue'),
},
{
path: '/product/:id',
component: () => import('@/pages/ProductPage.vue'),
},
]
export default createRouter({
history: createWebHistory(),
routes,
})
// components/HeavyModal.vue — defineAsyncComponent
import { defineAsyncComponent } from 'vue'
const HeavyModal = defineAsyncComponent(() =>
import('@/components/HeavyModal.vue')
)
Размер начального бандла снизился до 380 КБ, LCP улучшился с 4.2 с до 1.8 с.
Доступность (Accessibility)
Аудит с помощью axe DevTools выявил 23 нарушения WCAG 2.1. Основные правки:
- Добавил
aria-labelиroleк иконочным кнопкам без видимого текста. - Реализовал фокус-ловушку в модальных окнах: при открытии фокус переходит на первый интерактивный элемент, при закрытии — возвращается на триггер.
- Применил директиву
v-focusи хукonMountedдля управления фокусом программно. - Добавил
aria-live="polite"для уведомлений об изменениях содержимого (добавление в корзину).
// composables/useFocusTrap.js
import { onMounted, onUnmounted, ref } from 'vue'
export function useFocusTrap(containerRef) {
const previousFocus = ref(null)
onMounted(() => {
previousFocus.value = document.activeElement
const focusable = containerRef.value.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
if (focusable.length) focusable[0].focus()
})
onUnmounted(() => {
previousFocus.value?.focus()
})
}
Тестирование (Testing)
Покрытие юнит-тестами было около 12%. Я настроил Vitest + @vue/test-utils и написал тесты для критических компонентов и composables:
// tests/useCart.test.js
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '@/stores/cart'
describe('useCartStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('добавляет товар в корзину', () => {
const cart = useCartStore()
cart.addItem({ id: 1, name: 'Widget', price: 100 })
expect(cart.items).toHaveLength(1)
expect(cart.total).toBe(100)
})
it('не дублирует товар, а увеличивает количество', () => {
const cart = useCartStore()
cart.addItem({ id: 1, name: 'Widget', price: 100 })
cart.addItem({ id: 1, name: 'Widget', price: 100 })
expect(cart.items).toHaveLength(1)
expect(cart.items[0].quantity).toBe(2)
})
})
Покрытие выросло до 68%, что позволило отловить три регрессии при рефакторинге.
Поддерживаемость (Maintainability)
Компонент ProductCard.vue вырос до 600 строк. Я разбил его по принципу Single Responsibility:
ProductCardImage.vue— изображение с lazy-load и fallback.ProductCardBadges.vue— ярлыки «Хит», «Скидка».ProductCardActions.vue— кнопки «В корзину» и «В избранное».- Логику работы с корзиной вынес в
composables/useCartActions.js.
После декомпозиции каждый файл стал <150 строк, PR-ревью ускорились в 2 раза.
Подводные камни
- Чрезмерный lazy-loading: если дробить на слишком маленькие чанки, браузер делает десятки мелких запросов — хуже, чем один большой бандл. Группируйте логически связанные маршруты через
webpackChunkNameилиrollupOptions.output.manualChunks. - Виртуализация и динамическая высота строк:
vue-virtual-scrollerплохо работает с элементами переменной высоты без явного указанияmin-item-size— список прыгает при скролле. - Фокус-ловушка и порталы: если модальное окно рендерится через
Teleport,containerRefможет указывать не туда — нужно явно передавать ссылку на телепортируемый DOM-узел. - ARIA и динамический контент:
aria-liveне работает, если элемент добавляется в DOM вместе с текстом — регион должен существовать в DOM до изменения его содержимого. - Vitest и глобальные моки:
vi.mock()поднимается в начало файла автоматически, что ломает порядок импортов при написании интеграционных тестов с реальными Pinia-сторами. - Декомпозиция ради декомпозиции: чрезмерное дробление создаёт «prop drilling» через 3–4 уровня. Используйте
provide/injectили Pinia, когда данные нужны глубоко вложенным компонентам. - Метрики без базовой линии: улучшения производительности бессмысленны без замеров до и после. Всегда фиксируйте Lighthouse-отчёт или Web Vitals перед началом оптимизации.
What hurts your answer
- Выдумывать опыт или говорить слишком общими фразами
- Не объяснять свою личную роль в работе с Vue.js
- Не показывать результат, метрики или извлечённые уроки
What they're listening for
- Может подготовить честный пример использования Vue.js
- Показывает свою роль, решения и результат
- Умеет рефлексировать над trade-offs и уроками