Прискорюємо Nuxt 3 застосунок: три дієві кроки для оптимізації перформансу
Привіт усім! Мене звати Михайло Кухарський, я Front-end Engineer в ІТ-компанії Futurra Group. Ми працюємо над розробкою та просуванням власних кросплатформних EdTech-продуктів. Зокрема, розвиваємо математичний сервіс MathMaster та нашу нову платформу в ніші лайфстайл-освіти.
Стек наших вебзастосунків здебільшого базується на Nuxt. Чому? Він надає багато корисних і зручних рішень, які пришвидшують розробку та покращують user experience, як то кажуть, out of the box. Переваги Nuxt можна перелічувати довго, але з цим уже чудово впорався мій колега у статті про міграцію наших проєктів на цей стек.
Утім, хай яким зручним та швидким є Nuxt (далі говоритиму саме про його третю версію), великою проблемою для розробників усе ще залишаються PageSpeed-показники, які безпосередньо передають те, як бачать ваш застосунок користувачі. Все через те, що у вільному доступі не так багато змістовної інформації, як оптимізувати Nuxt-застосунок найефективніше. Тож у якийсь момент розробник стикається з тим, що всі стандартні поради він уже спробував, документація себе вичерпала, а PageSpeed-показники досі не сягають бажаного рівня.
У цій статті я поділюся рішеннями, що стали мені в пригоді й допомогли значно покращити перформанс-показники наших застосунків.
Навіщо та як вимірювати перформанс-показники
Перформанс — це не лише про швидкість завантаження сайту / застосунку, а й про те, як швидко юзер отримує можливість взаємодіяти з ним та виконувати ключові дії, які несуть пряму цінність для вашого бізнесу.
Так, наприклад, дослідження від Pinterest показали, що покращення перформанс-показників їхнього застосунку дозволили на 40% знизити час очікування, на 15% покращити результати їхнього пошукового трафіку та на 15% збільшити конверсії у створення нових акаунтів.
Звісно, важливим фактором є регіон, на який ви таргетитесь. Там, де інтернет дешевий та швидкий, рідко можна відчути проблеми із завантаженням, навіть якщо застосунок максимально насичений контентом, різними HTTP-запитами тощо. Проте якщо ваша аудиторія мешкає, наприклад, у країнах Азії, Латинської Америки, Африки тощо, де якість інтернет-покриття гірша, питання покращення перформансу стає ключовим.
Загалом перформанс вебзастосунку визначається шістьма основними метриками:
- TTFB (Time to First Byte) — час, потрібний для встановлення зʼєднання з вебсервером та отриманням вмісту сайту.
- LCP (Largest Contentful Paint) — час на відображення найбільшого елементу (картинка, текст тощо) в зоні доступу користувача.
- CLS (Cumulative Layout Shift) — найцікавіша, як на мене, метрика. Вона визначає, скільки разів під час першого завантаження сайту його контент неочікувано (різко) змінювався або ж окремі елементи змінювали своє розташування тощо.
- FCP (First Contentful Paint) — час, що потрібен, аби користувач побачив найперший елемент на сторінці під час її завантаження.
- INP (Interaction to Next Paint) — загальна доступність сайту для користувача, наскільки легко сайт відгукується на його дії. Кінцевий рахунок цієї метрики — найдовший час, який був помічений під час дій юзера.
- FID (First Input Delay) — час, який потрібен користувачу, аби мати можливість зробити першу взаємодію із сайтом. Відмінність INP та FID полягає в тому, що INP вимірює загальну затримку між взаємодією користувача протягом усієї сесії, а FID — саме затримку до першої взаємодії.
Головним інструментом для вимірювання перформанс-показників вважають Google PageSpeed Insights, що докладно відображає статистичні дані та проблемні місця вашого застосунку.
Втім, це не єдиний і далеко не найкращий для цього інструмент, адже є низка шляхів, як його можна «обманути» (наприклад, існують цілі модулі або просто загальнодоступні скрипти, які відкладають процес гідрації на сайті, збільшуючи в такий спосіб показники першого завантаження). Проте вказати на проблемні точки він точно зможе, а також, якщо відвідуваність вашого сайту достатньо велика, покаже статистику для реальних користувачів, а не лише виміряні алгоритмами показники.
Куди кращою практикою вважається стежити за показниками реальних користувачів — Core Web Vitals, які ви також зможете переглянути на Google PageSpeed, за умови що відвідуваність вашого сайту достатньо висока. Core Web Vitals дивиться саме на LCP, CLS, FIP реальних користувачів..
Ще один хороший інструмент, який я знайшов для вимірювання таких даних, — DebugBear. Він дозволяє вам встановити, перформанс яких саме сторінок потрібно відстежувати, як часто та в яких регіонах варто вимірювати цю статистику, а також розділяє показники на mobile і desktop.
Як покращити перформанс
Поради щодо покращення TTFB, LCP, CLS, FCP, INP та FID наведені у висновку від Google PageSpeed. Втім, вони розкривають алгоритм дій не надто докладно, як і офіційна документація Nuxt 3. Тож розробнику зазвичай доводиться досліджувати документацію дотичних технологій, які використовує Nuxt «під капотом», а саме Vite та Nitro. Про усе, що я сам зміг знайти, розповім далі.
Universal Rendering
Та спершу поговоримо про велику перевагу Nuxt, яка за правильної реалізації дозволить вам досягти топових показників. Це Universal Rendering — комбінація Server-Side Rendering (SSR) та Static Site Generation (SSG). Яка між ними різниця та чому Universal Rendering — це плюс?
SSR — це тип завантаження, який передбачає, що уся DOM-структура вашого вебзастосунку починає завантажуватись, коли ви намагаєтеся на неї потрапити. Таким чином очікування відкриття сторінки може бути більшим, ніж зазвичай, але після відкриття користувач відразу одержує готовий застосунок та не ждатиме, поки картинки, текст чи функціонал завантажаться. До того ж користувач завжди отримуватиме актуальний вміст, якщо він завантажується динамічно, і йому не потрібно буде очікувати, поки він провантажиться.
SSG, своєю чергою, завантажує весь вміст ще до того, як користувач намагається потрапити в застосунок, що дасть майже миттєве завантаження всіх сторінок. Але цей варіант не підійде, коли вміст має бути підлаштований під кожного користувача окремо або кількість динамічного вмісту значна.
Universal Rendering, який пропонує нам Nuxt 3, поєднує в собі найкращі практики обох вищезгаданих типів: комбінує миттєве завантаження статичних сторінок і зберігає чудовий UX під час серверного завантаження динамічних даних на перших етапах користування застосунком.
А зараз пропоную перейти безпосередньо до оптимізації вашого проєкту, або ж закладання фундаменту хорошого перформансу на початкових його етапах.
Крок 1. Дослідження розміру бандлу
Якщо ви плануєте оптимізацію вже наявного застосунку, слід починати з розміру кінцевого бандлу. На щастя, Nuxt пропонує для цього ефективний інструмент — vite-bundle-visualizer. Завдяки консольній команді nuxt analyze у вас буде доступ до перегляду серверного Nitro-бандлу, а також до кінцевого клієнтського бандлу. Візуалізовані дані виглядатимуть приблизно так:
Така візуалізація допоможе дослідити місця в коді, які варто оптимізувати, або ж розбити на менші частини та компоненти, або ж починати пошук альтернатив, якщо велику частину бандлу займають сторонні модулі та бібліотеки. Найбільші блоки (особливо ті, в яких ще багато менших підблоків) — це ті частини, на які потрібно звертати увагу передусім.
На розмір кожного блоку може впливати низка причин:
- Імпорт цілих модулів усередину файлів, а не лише потрібних вам частин:
import library from "someLibrary"
можна замінити на:
import { element } from "someLibrary"
- Надто велика кодова база в компонентах чи сторінках, що означає, що варто розбити цю частину проєкту на менші блоки й компоненти.
Також великі компоненти можна завантажувати асинхронно.
Уявімо, що вам потрібно відображати попап-вікно за якоїсь певної умови. Це означає, що відкривати компонент під час першого завантаження не потрібно.
Наприклад:
<script setup lang="ts"> const SomeComponent = defineAsyncComponent(() => import("@/components/SomeComponent.vue")); const isComponentShown = ref<boolean>(false); const showComponent = () => { isComponentShown.value = true; }; </script> <template> <div> <span>Some content</span> <SomeComponent v-if="isComponentShown" /> </div> </template>
Більше про це ви можете прочитати тут.
Крок 2. Встановлення модулів та бібліотек
Наведені нижче модулі та бібліотеки допоможуть оптимізувати ваш проєкт.
NuxtImage
Наступний крок до кращих перформанс-показників застосунку — оптимізація статичного контенту (такого як картинки), адже вони водночас є і важливою, і проблемною точкою в будь-якому проєкті.
Розглянемо такий приклад. На головній сторінці є велика кількість зображень для постів, аватарок юзерів тощо. Їхня початкова якість зазвичай досить висока, але кінцеве відображення користувачу може зводитися до картинки розміром 48 пікселів. Отож завантажувати їх в оригінальних розширеннях та форматах, а потім просто підлаштовувати під розмір — неефективно. Значно краще одразу завантажувати картинки в потрібних нам розмірах.
Те саме стосується і форматів. Сучасні формати за типом .webp підійдуть не всюди, адже матимуть трохи гіршу якість. Проте коли якість не є першим пріоритетом, варто використовувати саме сучасні формати.
NuxtImage відкриває перед вами можливість усе вищезгадане (і навіть більше) робити максимально зручно і швидко:
- він замінює стандартні <img> та <picture> HTML-теги на <nuxt-img> та <nuxt-picture> відповідно;
- завдяки атрибуту format допомагає легко керувати форматами вашого контенту;
- через атрибути width, height дозволяє встановлювати розмір для відображуваного контенту;
- також завдяки атрибуту sizes ви можете встановити адаптивний підбір розмірів для контенту, одразу полегшуючи розробку, особливо за принципами mobile-first;
- якщо не весь ваш контент зберігається безпосередньо в бандлі проєкту, ви можете легко використовувати модуль і з іншими content-providers, адже список тих, що підтримують NuxtImg, дуже великий.
Це не всі можливості конфігурації модуля, а лише ті, що лежать на поверхні і явно використовувані найчастіше.
Почнемо з додавання NuxtImage до проєкту:
npx nuxi@latest module add image
Після цього у вашому nuxt.config.ts файлі має зʼявитися це:
export default defineNuxtConfig({ modules: [ "@nuxt/image", ] });
З важливих деталей — NuxtImg буде справно працювати лише з вмістом /public папки.
Її основна різниця від /assets полягає в тому, що Vite (або ж Webpack) не кешує та не мініфікує її вміст, а також її контент можна буде знайти на сервері в загальному доступі. Очевидно, що такий підхід (коли картинки та вміст не мініфікуються під час білду) підійде не завжди, особливо якщо ви пропонуєте велику кількість статичного контенту на сайті.
Важливо: якщо ви будете намагатись використати в NuxtImg шлях до контенту з папки /assets, працюватиме це коректно лише в локальному середовищі, під час білду ж усі ваші старання зникнуть.
Тому NuxtImg не є універсальним рішенням для всіх картинок, але особисто я раджу використовувати його тоді, коли SSR-завантаження, динамічність вмісту тощо мають суттєве значення.
Наприклад, у вас на головній сторінці є велика картинка, це по факту LCP вашого сайту, що відіграє одну з найважливіших ролей під час першого завантаження користувачем. Тому перенести це завантаження на SSR та покращити показник LCP буде більш пріоритетним, ніж мініфікувати цю картинку під час білду проєкту.
Як саме працює NuxtImage?
- Для Lazy-завантаження модуль використовує Observer API, що дозволяє завантажувати контент лише тоді, коли він наближається до зони бачення користувача, що особливо важливо для великих та насичених сторінок.
- Окрім конфігурації розробником, модуль автоматично базується на DPR (Device Pixel Ratio) під час завантаження контенту. Це дає змогу уникнути завантаження картинок у розмірах, які не підійдуть конкретному користувачеві (наприклад, на мобайлі не потрібно завантажувати контент тих же розмірів, що на десктопі).
- На етапі SSR модуль перевіряє, чи є контент оптимізованим та підготовленим для відображення юзеру, виставляє необхідні атрибути, такі як srcset, аби сторінка з’являлася швидше і нічого не шкодило SEO-оптимізації.
VSharp
VSharp — це модуль для Vite, тому встановлювати його, якщо ви досі використовуєте webpack, марно. VSharp дозволяє сильніше мініфікувати та компресувати картинки у вашому проєкті.
Він першочергово базується на модулі для Node.js — Sharp, який автоматично мініфікує картинки до більш user-friendly розмірів та форматів.
Якщо трохи заглибитись, VSharp використовує libvips — неймовірно швидку та перевірену часом бібліотеку, яка досі активно покращується, хоча була розроблена ще 1989 року. Цю технологію застосовують для оптимізації картинок не лише у Frontend-розробці, а й у багатьох інших напрямах.
Тож установимо VSharp і додамо в nuxt.config.ts:
npm install vite-plugin-vsharp --save-dev
import vsharp from "vite-plugin-vsharp"; export default defineNuxtConfig({ modules: [ "@nuxt/image", ], vite: { plugins: [vsharp()], }, });
Цей плагін також дозволяє додатково налаштовувати опції стискання картинок (детальніше про них можна прочитати тут). Проте, як на мене, стандартної конфігурації також цілком достатньо.
ViteSVGLoader
Цей плагін хоч і не геймчейнджер оптимізації, але також у ній допоможе. Ми будемо його використовувати для завантаження SVG як компонентів, підтримуючи SSR.
Цей модуль автоматично оптимізує SVG-контент завдяки SVGO (SVG Optimizer) — бібліотеці для Node.js, який, своєю чергою, перетворює SVG на абстрактне синтаксичне дерево і маніпулює контентом, видаляючи усе зайве: пусті місця, відступи, непотрібні атрибути тощо.
Таким чином розмір ваших SVG буде меншим, що допоможе як в оптимізації розміру бандлу, так і в покращенні першого завантаження сторінки.
Почнемо з інсталяції:
npm install vite-svg-loader --save-dev
import vsharp from "vite-plugin-vsharp"; import ViteSvgLoader from "vite-svg-loader"; export default defineNuxtConfig({ modules: [ "@nuxt/image", ], vite: { plugins: [vsharp(), ViteSvgLoader()], }, });
Далі замість використання стандартних <img> тегів для завантаження SVG-компонентів ми можемо робити ось так:
<script setup lang="ts"> import SpinnerLoader from "@/assets/img/icons/spinner_loader.svg?component"; </script> <template> <SpinnerLoader /> </template>
Такі компоненти підтримуватимуть SSR і будуть оптимізовані самим плагіном. Цей підхід не вдасться комбінувати з NuxtImg, тому слід розділяти:
- NuxtImg — для великих, тяжких і важливих картинок;
- ViteSVGLoader — для SVG.
Крок 3. Конфігурація Nuxt у nuxt.config.ts
Тут хочу розказати детальніше про конфігурацію нашого проєкту в nuxt.config.ts, що також відіграло значну роль у покращенні перформансу наших застосунків.
Документацію Nuxt варто ділити на три рівноцінно важливі частини: безпосередньо Nuxt, Vite та Nitro.
Методом спроб та помилок, а також глибинного ресерчу я дійшов висновку, що найбільше розкрити можливості цих інструментів ми можемо з таким nuxt.config:
import vsharp from "vite-plugin-vsharp"; import ViteSvgLoader from "vite-svg-loader"; export default defineNuxtConfig({ modules: [ "@nuxt/image", ], nitro: { minify: true, compressPublicAssets: { brotli: true, }, }, vite: { ssr: { noExternal: true, }, json: { stringify: true, }, build: { cssMinify: "lightningcss", ssrManifest: true, minify: "terser", }, plugins: [vsharp(), ViteSvgLoader()], }, });
NITRO:
minify: true — значення за замовчуванням false — слід увімкнути для додаткової мініфікації вашого бандлу.
compressPublicAssets: brotli — значення за замовчуванням { gzip: false, brotli: false } — виконує стискання елементів у папці /public, можна вибрати як gzip, так і brotli. Коли ввімкнено, використовується найкращий рівень стискання.
VITE:
ssr.noExternal: true — налаштування, яке запобігає винесенню залежностей за білд для SSR, тобто усі залежності в проєкті будуть також зібрані в білд, що підвищить час відповіді сервера та початкове завантаження.
Важливо: цю опцію потрібно вимикати для розробки, інакше локально ваш проєкт запускатись не буде.
json.minify: true — значення за замовчуванням false — увесь імпортований json у проєкті буде трансформуватись за допомогою export default JSON.parse("...«), що дуже покращить перформанс, особливо в проєктах з великими json-ами.
Проте це налаштування може викликати проблеми зі зчитуванням json-ів, якщо в таких файлах дуже глибокий рівень вкладеностей.
build.minify: «lightningcss» — дозволяє обрати різні інструменти для мініфікації JS та CSS-файлів, бо стандартне значення це «esbuild», що є не найкращою опцією ні для JS, ні для CSS. «lightningcss» забезпечує кращий рівень мініфікації, але потребує встановлення додаткового пакету:
npm i lightningcss
build.minify: «terser» — як я згадував раніше, стандартне значення «esbuild» не найкращим чином мініфікує ваші JS-файли, до яких все і буде зводитись під час білду. terser справиться з цим трохи краще.
build.ssrManifest: true — за замовчуванням — false. Під час білду генеруватиме додатковий SSR-маніфест, визначаючи вказівки для попереднього завантаження контенту.
Що в підсумку
Усе вищенаведене я досліджував, тестив та впроваджував, безпосередньо стикаючись з різноманітними робочими кейсами — коли доводилося розробляти щось з нуля або ж оптимізувати вже наявні проєкти.
Впевнений, що багато чого я не згадав. Також не відкидаю, що існують кращі варіанти покращити перформанс-показники. Проте ці поради — перевірені на моєму досвіді й точно робочі.
Щоб не бути голослівним, так виглядали наші PageSpeed-показники до змін, про які я розповів.
А ось і результат оптимізації:
З графіків вище можемо побачити, що усі показники до оптимізації були дуже низькими, і не лише за алгоритмами Google PageSpeed, а й за статистикою реальних юзерів від Core Web Vitals. Після оптимізації усі важливі показники опинилися в зеленій зоні, що явно покращило рівень задоволеності користувачів та підняло загальний UX у нашому застосунку.
Звичайно, якщо розбиратись детальніше, то ще є пункти, які можна покращити, а саме FCP та TTFB для mobile. Втім, це вже менш критично, адже головна мета досягнута: Core Web Vitals assessment — пройдено, Google PageSpeed показники — високі, користувачі — задоволені.
Далі ви можете побачити показники двох з найбільш відвідуваних, наповнених контентом та функціональних сторінок проєкту, який розроблено повністю з нуля з дотриманням цих порад:
Наведені показники доводять, що якщо використовувати якомога більш оптимізовані рішення під час розробки, то покращення Google PageSpeed показників ще довгий час не буде для вас завданням у беклозі, а питання задоволеності користувачів від швидкості завантаження вашого застосунку не виступатиме проблемним фактором.
Оптимізовані вебзастосунки мають більші шанси на задоволення потреб користувачів та подальшу їхню успішну взаємодію з вашим продуктом і, як результат, збільшення вашого прибутку. Тому я дуже раджу вам замислюватися над продуктивністю сайту / застосунку — як на початкових етапах, так і для оптимізації того, що вже давно працює.
Дуже сподіваюся, що рішення, про які я розповів, стануть у пригоді й вам. А про ваші кейси роботи з перформанс-показниками залюбки почитаю в коментарях!
8 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів