Підвищуємо продуктивність клієнтської частини веб-додатки

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







Пояснювати, що глобальні змінні - це зло, не треба - це і так зрозуміло. Але чому зло і наскільки воно велике, ми спробуємо розібратися. Візьмемо для прикладу простий код, який здійснює обхід масиву і виробляє з кожним елементом яке-небудь просте дію. Створимо масив з 100 000 елементів і запишемо в кожен з них випадкове число.

Тут у функції здійснюється обхід масиву, що знаходиться в глобальному контексті. Інтерпретатор, побачивши в умови arr.length, починає пошук змінної arr. Насамперед він шукає в локальній області видимості, тобто всередині функції. Однак всередині функції змінна arr не оголошена. Тоді інтерпретатор переходить в ланцюжку областей видимості на рівень вище (в нашому випадку, на щастя, - відразу в глобальну область, хоча могло бути і гірше) і здійснює пошук там. Тут він нарешті знаходить змінну arr і шукає у міститься в ній об'єкта властивість length. Пошук по кожній області видимості займає дорогоцінний час. Спробуємо переписати функцію так, щоб їй не доводилося на кожній ітерації циклу звертатися в інші області видимості.

Така, здавалося б, дрібна оптимізація дає в Chrome дивовижний приріст продуктивності - в 2,5 рази (35 мс замість 81). У Firefox приріст менш відчутний, але теж є: 54 мс замість 60, тобто на 10%. У Opera 12 час виконання скорочується ще менш значно: з 99 до 95 мс.

Що стосується роботи з масивом, то ми можемо розраховувати для тіло циклу, використавши замість звернення до кожного конкретного елементу масиву метод push:

Там, де це можливо (а це - практично скрізь), варто переносити дані в якомога більш локальну область видимості. Наприклад, використовувати шаблон негайно викликаються функцій. Тобто замість такого коду:

краще написати такий:

А ще краще - зробити так, щоб навіть для отримання об'єкта document функції не доводилося шукати в глобальному просторі імен:

Тут ми передали об'єкт document в анонімну функцію як аргумент, таким чином перенесли його в локальне для цієї функції простір імен. Цей же патерн прийнято використовувати в плагінах jQuery:

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

Підвищуємо продуктивність клієнтської частини веб-додатки
Час доступу до різних областей видимості в різних браузерах, мс

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

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

Застосувавши цю оптимізацію до попереднього прикладу, отримуємо:







У Chrome цей прийом в даному випадку дає приріст продуктивності в 10% (з 32 до 29 мс), в Opera 12 час виконання практично не змінюється (923 і 939), проте в Firefox і IE воно скорочується в два рази: c 1030 до 525 мс для Firefox і з 1705 до 812 для IE. Щоб розібратися в причинах такого ефекту, розберемо всі операції, які виробляє інтерпретатор на кожній ітерації циклу.

У першому випадку послідовність дій буде такою:

  1. Обчислити значення булева висловлювання i <10000000.
  2. Порівняти отримане значення з true.
  3. Інкрементіровать i.

У другому випадку - такий:

  1. Порівняти значення i з true.
  2. Декрементіровать i.

Відповідно до «правилом брехні» будь-ненульове числове значення i приводиться до true. До false, нагадаю, наводяться тільки 0, NaN, null, undefined і порожній рядок.

Менш витончений, але досить ефективний метод оптимізації циклів полягає в тому, щоб розгорнути тіло циклу, збільшивши кількість операцій в кожній ітерації, але знизити кількість самих ітерацій. Такий прийом називається «Пристрій Даффа» (Duff's service). Для нього доведеться пожертвувати красою і лаконічністю коду, але у випадках, коли продуктивність важливіше стислості, цей прийом доведеться до речі. Наприклад, ми можемо замінити такий код:

Це скоротить час виконання на 10% в Opera 12, а в Chrome і Firefox - на 20%. Ми зменшили кількість ітерацій в вісім разів, як наслідок - о восьмій же раз знизили число накладних перевірок, неминуче виконуваних при кожному повторенні циклу. Ну а для того, щоб забезпечити коректну роботу з кількістю повторень, що не кратним восьми, використовували конструкцію switch c «проваленням» в наступний case (зверни увагу, що в кінці кожного case відсутня директива break).

Ми можемо зробити те ж саме коротше, замінивши switch на цикл, практично без втрати продуктивності:

Даний прийом ефективний лише при великій кількості ітерацій.

Підвищуємо продуктивність клієнтської частини веб-додатки
Час роботи циклу в різних браузерах, мс

Коли одна і та ж функція викликається неодноразово, корисно буде використовувати прийом «мемоізаціі» або, кажучи простіше, кешування, що повертається. Особливо цей прийом корисний в тому випадку, якщо функція виконує якісь тривалі операції. Розглянемо його на прикладі обчислення факторіала числа. Класична рекурсивна функція обчислення факторіала виглядає так:

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

Кеш значень доступний через замикання, це виключає його ненавмисне зміна - доступ до нього має тільки функція factorial. Припустимо, нам потрібно послідовно обчислити факторіали 100, 101 і 102. При використанні класичної рекурсивної функції ми спочатку обчислимо факторіал числа 100 (100! = 100 * 99 * 98 ... * 1 або 100 * 99!), Потім - числа 101 (101! = 101 * 100 * 99 ... * 1 або 101 * 100!), а потім - факторіал числа 102 (102! = 102 * 101 * 100 ... * 1 або 102 * 101!). Таким чином, факторіал числа 101 обчислити двічі, а числа 100 - аж тричі. Мемоізація значень дозволяє цього уникнути. При використанні кешування результатів ми спочатку обчислюємо значення 100. а при обчисленні 101! повторні обчислення проводитися не будуть. Замість цього з кеша буде вилучено вже обчислене значення 100, помножене на 101 і повернуто. Таким чином ми уникаємо величезної кількості необов'язкових обчислень. Особливо яскраво ефект мемоізаціі помітний при виконанні повільних, ресурсномістких операцій (наприклад, роботи з DOM-деревом). Однак при використанні цього прийому потрібно бути впевненим в тому, що при кожному наступному виконанні функції з одними і тими ж аргументами, що повертається нею результат повинен бути тим же, що і в попередній раз. Тому кешування не підходить для обробки динамічно змінюються даних.

Однак і це ще не все. Настільки ж довго, як і операції отримання елемента DOM, виконуються операції перемальовування сторінки. Рендеринг сторінки відбувається кожного разу, коли ти зраджуєш форму елементів: змінюєш в блоках текст, додаєш нові блоки в layout сторінки, змінюєш стилі елементів. Щоб мінімізувати час, який браузер витрачає на перемальовування сторінки, необхідно об'єднувати кілька йдуть один за одним операцій, що змінюють вигляд документа, в одну всюди, де це можливо.

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

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

Вимірювання часу роботи скрипта

Найпростіший спосіб виміряти час роботи:

Підвищуємо продуктивність клієнтської частини веб-додатки
Замір часу виконання в консолі

Хороші книги про оптимізацію фронтенда

Покажи цю статтю друзям: