Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Способи оптимізації додатків». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Способи оптимізації додатків** охоплюють мережевий, завантажувальний і runtime рівні. Gzip/Brotli стиснення, CDN, HTTP/2, code splitting на рівні маршрутів, `loading="lazy"` для зображень, debounce для полів вводу, віртуалізація списків. ```js // Dashboard завантажується тільки при переході на маршрут const Dashboard = React.lazy(() => import('./Dashboard')); ``` **Ключове:** спочатку запусти Lighthouse. Мемоізація і code splitting без профілювання додають накладні витрати замість того щоб їх прибрати.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Способи оптимізації додатків** - набір технік, що скорочують час завантаження, знижують вартість рендерингу і прискорюють взаємодію на рівні мережі, ресурсів і runtime. ## Теорія ### TL;DR - Мережа: стискай через Gzip/Brotli, підключай CDN, вмикай HTTP/2, виставляй заголовки `Cache-Control` для статики - Завантаження: code splitting на рівні маршрутів, lazy loading зображень і компонентів, конвертуй зображення в WebP/AVIF - Runtime: debounce для полів вводу, Web Workers для важких обчислень, віртуалізація довгих списків - React: `useMemo`, `useCallback`, `React.memo` мають сенс тільки якщо профайлер підтвердив реальну проблему - Вимірюй першим: Lighthouse, Web Vitals (LCP, FID, CLS), вкладка Performance у DevTools ### Мережевий рівень Gzip скорочує розмір HTML/CSS/JS при передачі в середньому на 60-80%. Brotli стискає на 15-20% краще за Gzip для текстових ресурсів. Більшість серверів (Nginx, Apache) підтримують обидва формати. Вмикай для всіх текстових відповідей, не тільки для HTML. **CDN** розміщує статику (зображення, шрифти, скрипти) на серверах ближче до користувачів. Cloudflare і Amazon CloudFront - найпоширеніші варіанти. Користувач у Варшаві, що завантажує ресурси з франкфуртської edge-ноди замість US origin, отримує round-trip ~15мс замість ~120мс. На кожному ресурсі сторінки ця різниця накопичується. HTTP/2 змінює спосіб спілкування браузера і сервера. HTTP/1.1 ставить запити в чергу на одному з'єднанні. HTTP/2 мультиплексує їх одночасно: 20 паралельних запитів не блокують один одного. Також він стискає заголовки через HPACK. Перехід на HTTP/2 найбільше помітний на сторінках з багатьма дрібними ресурсами. ### Заголовки кешування Встанови `Cache-Control: max-age=31536000, immutable` для версіонованих статичних ресурсів (файли з хешем у назві, на кшталт `main.a3f9c2.js`). Браузер не зробить жодного мережевого запиту протягом року. Для HTML використовуй `Cache-Control: no-cache` разом з `ETag`: браузер перевіряє актуальність, але не завантажує файл знову якщо нічого не змінилось. ### Code splitting і lazy loading Webpack і Vite автоматично розбивають бандл при динамічних викликах `import()`. React-додаток з одним бандлом 800KB стає кількома чанками по 50-100KB, що завантажуються за потреби. Браузер отримує тільки код для поточного маршруту. `React.lazy` обгортає динамічний імпорт, а `Suspense` показує заглушку під час завантаження: ```jsx const Dashboard = React.lazy(() => import('./Dashboard')); function App() { return ( <Suspense fallback={<Spinner />}> <Dashboard /> </Suspense> ); } ``` Для зображень нативний атрибут `loading="lazy"` на тегу `<img>` відкладає завантаження до появи елемента у viewport. Без JavaScript. Додай `width` і `height`, щоб уникнути зміщення верстки (layout shift). ### Оптимізація зображень Переходь на WebP або AVIF. WebP в середньому на 25-35% менший за JPEG при еквівалентній якості. AVIF стискає краще, але підтримка кодувальників з'явилась пізніше. Використовуй `<picture>` для фолбеків: ```html <picture> <source srcset="photo.avif" type="image/avif"> <source srcset="photo.webp" type="image/webp"> <img src="photo.jpg" alt="..." loading="lazy" width="800" height="600"> </picture> ``` Якщо роздаєш зображення через CDN з автоматичним вибором формату (Cloudinary, Imgix), CDN сам підбирає формат за URL-параметрами і цей HTML не потрібен. ### Debounce і throttle Поле пошуку, що відправляє запит при кожному натисканні клавіші, може генерувати 10-20 мережевих запитів на секунду. Debounce затримує виконання до паузи у введенні: ```js function debounce(fn, delay) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } const handleSearch = debounce((query) => fetchResults(query), 300); ``` Throttle працює інакше: виконує функцію з фіксованим інтервалом незалежно від частоти викликів. Підходить для обробників scroll і resize, де потрібні рівномірні оновлення без пропусків. ### Web Workers JavaScript виконується в одному потоці. Важкі обчислення на головному потоці блокують рендеринг і заморожують інтерфейс. Web Workers запускаються в окремому потоці, спілкуються через `postMessage` і не мають доступу до DOM. ```js // main.js const worker = new Worker('worker.js'); worker.postMessage({ data: largeArray }); worker.onmessage = (e) => console.log(e.data.result); // worker.js self.onmessage = (e) => { const result = processData(e.data.data); // виконується поза головним потоком self.postMessage({ result }); }; ``` Типові кейси: парсинг CSV, обробка зображень, сортування великих масивів, шифрування. ### Віртуалізація для довгих списків Рендеринг 10 000 рядків `<tr>` створює 10 000 DOM-вузлів. Браузер малює і відстежує їх всіх, навіть якщо на екрані видно тільки 20. Віртуалізація рендерить лише видимі рядки. Бібліотеки `react-window` і `@tanstack/virtual` беруть математику на себе. Список, що рендерився 3 секунди з повним DOM, може впасти до 100мс з віртуалізацією. ### React: мемоізація `useMemo`, `useCallback` і `React.memo` не безкоштовні: вони займають пам'ять і додають порівняння при кожному рендері. Типова помилка - додавати їх скрізь превентивно. Спочатку відкрий React Profiler і підтверди, що компонент справді зайво перерендерюється. Тоді вже застосовуй мемоізацію точково. `React.memo` пропускає перерендер компонента, якщо пропси не змінились (поверхневе порівняння). `useMemo` кешує результат дорогого обчислення. `useCallback` зберігає посилання на функцію, щоб дочірні компоненти, які отримують її як проп, не перерендерювались зайвий раз. ### Вимірювання продуктивності Чотири інструменти покривають більшість випадків: - **Lighthouse** (вкладка Audits у Chrome DevTools): оцінює LCP, FID/INP, CLS і дає конкретні рекомендації - **Web Vitals**: LCP менше 2.5с, FID менше 100мс, CLS менше 0.1 - пороги "добре" за версією Google - **Вкладка Performance у DevTools**: записує трейс рендерингу, скриптів і малювання, допомагає знайти причину повільної взаємодії - **WebPageTest**: тестує з реальних локацій і пристроїв, включно з емуляцією 3G React Profiler (розширення браузера або API `<Profiler>`) показує які компоненти рендеряться, як часто і скільки часу це займає. ### Типові помилки **Мемоізувати все.** `useMemo` і `useCallback` скрізь додають накладні витрати без користі, якщо компонент і так дешевий у рендерингу. **Надто агресивний code splitting.** Забагато дрібних чанків означає забагато HTTP-запитів. Розбивка на рівні маршрутів - правильна гранулярність у більшості проектів. **CDN без заголовків кешування.** Файли за CDN з `Cache-Control: no-store` змушують CDN щоразу звертатись до origin. Географічний розподіл є, кешування немає. **Lazy loading для зображень вище згину (above-the-fold).** Головне зображення або логотип не повинні мати `loading="lazy"`. Це безпосередньо затримує LCP. **Пропустити вимірювання.** Надмірний splitting або зайві інвалідації кешу можуть погіршити ситуацію. Профілюй перед змінами. ### Follow-up питання **Q:** Яка різниця між debounce і throttle? **A:** Debounce чекає паузу у викликах і спрацьовує один раз після неї. Throttle спрацьовує з фіксованою частотою незалежно від кількості викликів. Для пошуку - debounce, для scroll - throttle. **Q:** Коли не варто використовувати `React.memo`? **A:** Коли компонент дешевий у рендерингу, або коли пропси змінюються при кожному рендері батька. Вартість порівняння перевищує вартість рендерингу в таких випадках. **Q:** Які HTTP-заголовки відповідають за кешування браузера? **A:** `Cache-Control` задає політику (max-age, no-cache, immutable). `ETag` - відбиток файлу, який браузер надсилає назад для перевірки актуальності. `Last-Modified` робить те ж через часову мітку. **Q:** Чому HTTP/2 швидший за HTTP/1.1? **A:** HTTP/1.1 обробляє один запит на з'єднання за раз. HTTP/2 мультиплексує багато запитів через одне TCP-з'єднання, усуваючи head-of-line blocking для завантаження ресурсів. **Q:** Що таке Core Web Vitals і чому Google враховує їх у ранжуванні? **A:** LCP, FID/INP і CLS вимірюють реальний досвід користувача: час завантаження головного контенту, відгук на введення, стабільність верстки. Google включив їх у сигнали ранжування з 2021 року. **Q:** Яка різниця між віртуалізацією і пагінацією для великих списків? **A:** Пагінація завантажує підмножину даних і вимагає навігації між сторінками. Віртуалізація завантажує всі дані, але тримає у DOM тільки видимі рядки. Віртуалізація дає плавніший скролінг, пагінація скорочує обсяг переданих даних. ## Приклади ### Code splitting з React.lazy ```jsx import React, { Suspense } from 'react'; // UserProfile збирається окремим чанком, завантажується тільки коли isLoggedIn стає true const UserProfile = React.lazy(() => import('./UserProfile')); function App({ isLoggedIn }) { return ( <div> {isLoggedIn && ( <Suspense fallback={<div>Завантаження профілю...</div>}> <UserProfile /> </Suspense> )} </div> ); } ``` Webpack або Vite видадуть `UserProfile` як окремий чанк. Браузер завантажить його тільки якщо `isLoggedIn` дорівнює `true`. Початковий бандл іде без нього, що скорочує розмір першого завантаження. ### Debounce для поля пошуку ```js function debounce(fn, delay) { let timerId; return function (...args) { clearTimeout(timerId); timerId = setTimeout(() => fn.apply(this, args), delay); }; } const searchInput = document.querySelector('#search'); const fetchSuggestions = debounce(async (query) => { const res = await fetch(`/api/suggest?q=${query}`); const data = await res.json(); renderSuggestions(data); }, 300); searchInput.addEventListener('input', (e) => { fetchSuggestions(e.target.value); }); ``` Без debounce введення слова "react hooks" (10 символів) відправляє 10 запитів. З debounce 300мс - один запит після паузи. У продукті з 50 тис. активних користувачів на день це скорочує навантаження на API на 80-90%. ### Віртуальний список з react-window ```jsx import { FixedSizeList } from 'react-window'; const rows = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Елемент ${i}` })); function Row({ index, style }) { return ( <div style={style}> {rows[index].name} </div> ); } function VirtualList() { return ( <FixedSizeList height={600} itemCount={rows.length} itemSize={40} width="100%" > {Row} </FixedSizeList> ); } ``` `react-window` рендерить тільки рядки, що вміщаються в контейнер 600px. При висоті рядка 40px це 15 елементів у DOM замість 10 000. Я бачив як цей підхід скорочував початковий рендер звітної таблиці з 4 секунд до 150мс.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.