Поради щодо оптимізації продуктивності JavaScript: огляд

15

Від автора: сьогодні ми будемо говорити про те, як робиться в JavaScript оптимізація продуктивності. У цій статті багато потрібно обговорити, так як тема обширна. Це також всіма улюблена тема – JS фреймворк місяця.

Будемо дотримуватися мантри «інструменти, а не правила» і зведемо до мінімуму JS терміни. Ми не зможемо покрити все, що стосується JS продуктивності, у статті в 2000 слів, тому читайте також статті по посиланнях і вивчайте тематику самостійно.

Але перш ніж ми заглибимося в деталі, давайте глибше зрозуміємо проблему, відповівши на таке питання: що вважається продуктивним JS кодом, і як він вписується в ширший діапазон веб-продуктивності?

Налаштування оточення

По-перше, зробимо наступне: якщо ви тестуєте виключно на десктопі, ви отметаете більше 50% користувачів.

Поради щодо оптимізації продуктивності JavaScript: огляд

Тенденція буде тільки зростати, так як Android пристрою менше 100$ є кращим каналом ринків в мережі. Ера, коли головним пристрій доступу в інтернет був десктоп, закінчилася. Наступний мільярд користувачів, які відвідають ваш сайт, буде з мобільних пристроїв.

Тестування Chrome DevTools в режимі певного пристрою не замінює тестування на реальному пристрої. CPU уповільнення і уповільнення швидкості мережі допомагає, але це зовсім інше. Тестуйте на реальних пристроях.

І якщо ви тестуєте на реальних мобільних пристроях, ви, швидше за все, робите це на брендовому флагманському смартфоні за 600$. У ваших користувачів не такий пристрій. У ваших користувачів щось середнє — Moto G1 – пристрій з менш 1Гб ОЗУ і слабкими CPU і GPU.

Подивимося залежність при парсингу середнього пакета JS.

Поради щодо оптимізації продуктивності JavaScript: огляд

Ого. Цей скріншот покриває тільки час парсинга і компіляції JS (докладніше трохи пізніше), а не загальну продуктивність, однак кореляція сильна і може розглядатися як показник загальної продуктивності JS.

Процитую Bruce Lawson – «Це всесвітня павутина, а не багата західна мережа». Тому ваше цільове пристрій приблизно в 25 разів повільніше вашого MacBook і iPhone. Розберемо тему трохи докладніше. Все стає гірше. Давайте подивимося на нашу реальну мету.

Що таке продуктивний JS код?

Ми дізналися нашу цільову платформу. Тепер ми можемо відповісти на наступне запитання – що таке продуктивний JS код?

Не існує точного визначення продуктивного коду, проте у нас є користувацька модель продуктивності, яку ми можемо використовувати модель RAIL.

Поради щодо оптимізації продуктивності JavaScript: огляд

Відповідь

Якщо ваш додаток відповідає на користувальницьке взаємодія менш ніж за 100мс, користувач сприймає його як миттєве. Це стосується елементів, на які можна натискати, і не відноситься до прокручування та drag and drop.

Анімація

На моніторі 60Гц нам необхідно отримати постійні 60 кадрів в секунду для анімації і прокручування. Це близько 16мс за кадр. З цих 16мс у вас реально є 8-10мс. Решта включена внутрішніми операціями браузера та іншими відхиленнями.

Робота в режимі очікування

Якщо у вас є важкі та постійно працюють завдання, постарайтеся розбити їх на невеликі шматочки, щоб головна гілка могла реагувати на користувальницьке взаємодія. У вас не повинно бути завдань, які гальмують користувача більш ніж на 50мс.

Завантаження

Ваше завдання – завантаження сторінок менш ніж за 1000мс. Все що триває більше цього часу, дратує користувачів. На мобільних пристроях досить складно домогтися таких показників, так як сторінка повинна взаємодіяти з користувачем, а не просто рендеритись і крутитися. На практиці часу ще менше:

Поради щодо оптимізації продуктивності JavaScript: огляд

На практиці, прагніть укластися з взаємодією в 5 секунд. Саме такий час має Chrome Lighthouse audit.

Ми знаємо цифри, тепер подивимося на статистику:

53% відвідувачів покидають сайт, якщо мобільна версія завантажується більше 3 секунд

1 з 2 людей очікує, що сторінка завантажиться менш ніж за 2 секунди

77% мобільних сайтів завантажуються більше 10 секунд на 3G з’єднання

19 секунд – середній час завантаження мобільного сайту на 3G з’єднання

І ще трохи від Addy Osmani:

Додатки стають інтерактивними за 8 секунд на десктопі (по дроту) і за 16 на мобільних пристроях (Moto G4 на 3G+)

У середньому розробники відвантажують 410Кб стисненого JS на сторінки

Розчаровані? Добре. Приймемось за роботу і виправимо становище.

Контекст – все

Ви могли помітити, що найбільший недолік – час завантаження сайту. Зокрема, завантаження JS, парсинг, компіляція і виконання. Якось поліпшити це не можна, але можна завантажувати менше JS і робити це розумніше.

Але як щодо фактичної роботи, яку виконує ваш код крім простого завантаження сайту? Тут є де поліпшити продуктивність, так адже?

Перш ніж зануритися в оптимізацію коду, подумайте, що ви створюєте. Ви створюєте фреймворк або VDOM бібліотеку? Ваш код повинен виконувати тисячі операцій в секунду? Ви пишіть бібліотеку з упором на час обробки користувальницького введення та/або анімації? Якщо ні, то ви можете витратити час і енергію на щось більш значуще.

Суть не в тому, що писати продуктивний код марно. Просто зазвичай це мало впливає на загальну схему речей, особливо коли мова йде про микрооптимизации. Тому перш ніж ви почнете сперечатися .map vs .forEach vs for на Stack Overflow, порівнюючи результати з JSperf.com переконайтеся, що ви бачите на цілий ліс, а не дерева. 50к ops/s звучить в 50 разів краще ніж 1Кб ops/s, але на ділі різниці ніякої.

Парсинг, компіляція і виконання

Основна проблема повільного JS коду полягає не у виконанні самого коду, а у всіх кроках, які необхідні виконати перед виконанням самого коду.

Ми говоримо про рівнях абстракції. CPU у вашому комп’ютері запускає машинні коди. Велика частина запускаються на вашому ПК кодів скомпільовані в бінарний формат (я сказав саме код, а не програми з-за Electron додатків). Якщо не враховувати всі абстракції ОС, код запускається прямо на залозі без попередніх підготовок.

JS не компілюється заздалегідь. Він надходить (за досить повільного з’єднання) у формі читабельного коду в браузер, який є ОС для вашого JS коду.

Цей код першим ділом парс – тобто читається і переводиться у структуру, індексований комп’ютером, яку можна використовувати для компіляції. Далі код компілюється в байткод і на далі в машинний код, перш ніж він може бути виконаний у браузері/на пристрої.

Також варто сказати, що JS однопотоковый мову і запускається на головній гілці браузера. Тобто за один проміжок часу можна запустити тільки один процес. Якщо таймлайн продуктивності в DevTools насичений жовтими піками, що займають CPU на 100%, то ви отримаєте довгу/дерганую анімацію, дерганую прокрутку і т. д.

Поради щодо оптимізації продуктивності JavaScript: огляд

Перш ніж JS код почне працювати, необхідно виконати багато роботи. Парсинг і компіляція займають до 50% всього часу виконання JS в движку Chrome V8.

Поради щодо оптимізації продуктивності JavaScript: огляд

З цього розділу потрібно запам’ятати:

Час парсинга JS збільшується по мірі зростання розміру пакета, хоча і не лінійно. Чим менше JS, тим краще.

Кожен використовуваний JS фреймворк (React, Vue, Angular, Preact…) – ще один рівень абстракції (якщо він не заздалегідь складати, як Svelte). Це не тільки збільшить розмір пакета, але і сповільнить код, так як не спілкуєтеся з браузером безпосередньо.

Ви можете не використовувати JS-фреймворки для анімації все що не попадя, а також можете прочитати, що викликає перемальовування і макетування. Використовуйте бібліотеки тільки тоді, коли зовсім немає способу реалізувати анімацію через звичайні CSS переходи і CSS анімацію.

Незважаючи на використання CSS переходів, складових властивостей і requestAnimationFrame(), все це запускається в JS на головному потоці. Все це, в основному, забиває ваш DOM инлайновыми стилями кожен 16мс, так як по-іншому вони не вміють. Щоб анімація була плавною, весь JS повинен виконуватися менш 8мс за кадр.

CSS анімація і переходи виконуються не в головному потоці, а нА GPU при правильній реалізації без макетування і рефлоу.

Враховуючи що майже вся анімація запускається або під час завантаження, або під час інтерфейсу взаємодії, це дасть вашому додатку таке потрібне місце для маневру.

Web Animations API – пропонований набір функцій, з допомогою якого можна робити продуктивну JS анімацію з головного потоку. Але поки що зосередимося на CSS переходах і техніках типу FLIP.

Розмір пакета – все

Сьогодні все зав’язано на пакети. Пройшли часи Bower і нескінченних тегів script перед закриваючим body.

Зараз все що ви знайдете в NPM, можна встановити через npm install, додаючи все в один пакет через Webpack в один величезний JS файл 1Мб, забиваючи браузер користувачів і їх тарифний план.

Постарайтеся відвантажувати менше JS. Можливо, вам не потрібна вся бібліотека Lodash для проекту. Вам дійсно потрібно використовувати JS фреймворк? Якщо так, чи врахували ви що-небудь крім React. Наприклад, Preact або HyperHTML, які менше React більш ніж в 20 разів. Потрібна TweenMax для всіх анімацій прокручування до шапки сторінки? У зручності npm і ізольованих компонентів у фреймворках є недоліки – перший відповідь розробників на проблему привів до збільшення JS коду. Якщо у вас є молоток, все схоже на цвяхи.

Коли почистіть свій JS, спробуйте відвантажувати його більш розумним способом. Отгружайте що потрібно і коли потрібно.

У Webpack 3 є чудові функції – розділення коду і динамічні імпорти. Не заганяйте всі JS модулі в один app.js. Код автоматично можна розбити за допомогою import() і завантажувати асинхронно.

Не потрібно використовувати фреймворки, компоненти та клієнтський роутинг. Скажімо, у вас є складний шматок коду, що відповідає за .mega-widget, який може бути на будь-якій кількості сторінок. Можна просто написати наступне в головному JS файл:

if (document.querySelector(‘.mega-widget’)) {
import(‘./mega-widget’);
}

Webpack також вимагає свого часу для виконання роботи, яке він вставляє у всі згенеровані файли .js. Якщо ви використовуєте плагін commonChunks, з допомогою коду нижче ви можете витягти runtime в окремий код:

new webpack.optimize.CommonsChunkPlugin({
name: ‘runtime’,
}),

Цей код буде виштовхувати runtime з усіх інших шматків коду в файл (у нас це runtime.js). Не забудьте завантажити його перед головним JS пакетом. Наприклад:

Також можна розповісти про транспиллинг коду і полифилы. Якщо ви пишіть сучасний код (ES6+) JavaScript, ви, швидше за все, використовуєте Babel для транспиллинга коду в ES5. Транспиллинг збільшує розмір файлу не тільки з-за більшої кількості символів, але і з-за складності. Часто на відміну від ES6+ коду, тут є регресії продуктивності.

Враховуючи все це, ви, швидше за все, використовуєте пакет babel-polyfill і whatwg-fetch для патчінга відсутніх функцій в старих браузерах. Якщо ви пишете код через async/await, ви також транспиллируете його за допомогою генераторів, необхідних для підключення regenerator-runtime…

Суть в тому, що ви додаєте майже 100Кб до JS пакету, що не тільки величезний розмір, але і сильно позначається на парсингу та виконанні для поддеркжи старих браузерів.

Але не варто карати людей, що використовують сучасні браузери. Я використовую підхід, який Philip Walton розібрав у своїй статті – створення двох окремих пакетів і завантаження за умовою. В Babel це можна легко зробити через babel-preset-env. Наприклад, у вас один пакет для підтримки IE11 та іншої без полифилов для останніх версій сучасних браузерів.

Брудний, але ефективний спосіб – вставити код нижче в інлайн скрипт:

(function() {
try {
new Function(‘async () => {}’)();
} catch (error) {
// create script tag pointing to legacy-bundle.js;
return;
}
// create script tag pointing to modern-bundle.js;;
})();

Якщо браузер не знає функцію async, ми думаємо, що це старий браузер і підсовуємо пакет з полифилами. В іншому випадку користувач отримує сучасний варіант.

Висновок

Ми б хотіли, щоб ви з цієї статті з’ясували, що JS – дороге задоволення, і його потрібно використовувати обережно.

Протестуйте продуктивність сайту на найслабкіших пристроях з реальною швидкістю мережі. Ваш сайт повинен швидко завантажуватися, а також максимально взаємодіяти з користувачем. Тобто треба відвантажувати менше JS, відвантажувати його швидше з допомогою будь-яких інструментів. Код завжди має бути минифицирован, розділений на маленькі, керовані пакети, які завантажуються асинхронно при будь-якій можливості. Також перевірте активність HTTP/2 для більш швидкої паралельної передачі і gzip/Brotli стиснення для зниження передаються JS файлів.