Як инкрементальные оновлення впливають на швидкість завантаження. Досвід Яндекс.пошти

Яндекс.пошта — велике і складне веб-додаток. Для початкового завантаження їй необхідно більше 1 МБ статичних ресурсів (JS/CSS/Шаблонів). При цьому Яндекс.пошта оновлюється двічі на тиждень, а іноді й частіше.

Але при оновленнях від версії до версії, змінюється не так багато коду — особливо у випадку хотфиксов. Це показують і фризи. Щоб зменшити час завантаження пошти при виході нових версій, ми вже робимо наступне:

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

Ми подумали: «А що якщо зберігати десь стару версію файлів (наприклад, в localStorage), а при виході нової передавати тільки diff між нею і тієї, яка збережена у користувача?» У браузері ж залишиться просто накласти латку на клієнті. Про те, що з цього вийшло і яких висновків ми прийшли, читайте під катом.
КАТ
На ділі ця ідея не нова. Вже існують стандарти для HTTP — наприклад, RFC 3229 «Delta encoding in HTTP» і Google SDHC, — але з різних причин вони не одержали належного поширення в браузерах і на серверах.

Ми ж вирішили зробити свій аналог на JS. Щоб реалізувати цей метод оновлення, почали шукати реалізації diff на JS. На популярних хостингах коду знайшли бібліотеки: VCDiff, google-diff-patch-match, jsdiff, Pretty Diff і jsdifflib.

Останні дві бібліотеки (jsdifflib і Pretty Diff) нам відразу не підійшли, тому що не вміють накладати патч, а показують тільки зміни між рядками. А jsdiff генерує патч у форматі, схожому на google diff patch match, але накладає його в п'ять разів повільніше. В результаті у нас залишилося два кандидати.

Для остаточного вибору бібліотеки нам потрібно порівняти їх за двома ключовими для нас метрик. Перша — розмір генерованого патча. Ми нагенерировали патчів для різних ресурсів різних версій і порівняли google diff patch match і vcdiff з різним розміром блоку.

Vcdiff (розмір блоку 3) Vcdiff (розмір блоку 10) Vcdiff (розмір блоку 20) google diff patch match
13957 3586 3431 9297
865 367 309 910
4615 1854 1736 6740


Як видно з таблиці результатів, vcdiff з розміром блоку 20 байт має найменший розмір патча. Друга ключова метрика для нас — час накладання латки на клієнта.

Бібліотека IE 9 Opera 12 Firefox 19 Chrome
vcdiff (розмір блоку 10) 8 5 5 3
google diff patch match 1363 76 43 35


Тут теж vcdiff виграє з великим відривом. В IE 9 і нижче google-diff-patch-match накладає патч більше, ніж за секунду.

У нас визначився переможець — vcdiff. Цей алгоритм був запропонований у 2002 році (RFC3284). Він досить популярний і має безліч реалізацій на різних мовах, у тому числі на C++, Java та PHP.

Після того як ми визначилися з бібліотекою для диффа, потрібно визначитися з тим, де і як зберігати статику на клієнті. Яндекс.пошта — сучасне веб-додаток, у нас немає старих браузерів, тому майже всі підтримують localStorage. Він є зручним місцем для зберігання статики. Ніяких особистих даних користувача там немає, тому немає проблем з безпекою. Кожен файл зберігається в окремому ключі. В цей ключ вшито не тільки назва ресурсу, але і його версія. Це дозволяє уникнути проблем, коли версія змогла записатися в localStorage, а сам файл — немає (або навпаки), у варіанті, коли файл і його метадані зберігаються в різних ключах.

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

Формат файлу з патчами для проекту виглядає так:
[ { "k": "jane.css", "p": [patch], "s": 4554 }, { "k": "jane.js", "p": [patch], "s": 4423 }, ...]


Тобто це звичайний масив об'єктів. Кожен об'єкт — окремий ресурс. У кожного об'єкта є три властивості. «k» — назви ключа в localStorage для цього ресурсу. «p» — патч для ресурсу, який згенерував vcdiff. «s» — чексумма ресурсу актуальної версії, щоб потім можна було перевірити правильність накладання латки на клієнті. Чексумма обчислюється за алгоритмом Флетчера.

Чому саме алгоритм Флетчера, а не інші популярні алгоритми начебто CRC16/32 або md5? Тому що він швидкий, компактний і легкий в реалізації.

Процес оновлення

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

Якщо ж є збережена стара версія localStorage, надсилаємо запит за файлом з патчем. Далі оновлюємо всі ресурси і перевіряємо чексумму. Якщо все чексумми збігаються, оновлюємо вміст ключів в localStorage.

На цьому процес оновлення завершено. Далі всі необхідні ресурси ми беремо з localStorage. Код JS-модулів і скомпільованих шаблонів ми виконуємо через new Function(), а CSS підключаємо через вставку тега
<style>
.

Що отримали

Фактично ми економимо 80-90% трафіку. Розмір завантажуваної статитки у байтах:
Реліз З патчем Без патча
7.7.20 — 7.7.21 397 174 549
7.7.21 — 7.7.22 383 53 995
7.7.22 — 7.8 18 077 611 378
7.8 — 7.8.50 2 817 137 820
7.8.50 — 7.8.8000 14 868 443 159


Що ж ми отримали в реальності?

В реальності швидкість завантаження зросла зовсім небагато. Чому так?

У нас відразу з'явилася проблема з перевіркою чексумми. Вважати його для всього файлу виявилося дуже дорого. Навіть у сучасних браузерах на потужних комп'ютерах на один файл йде приблизно 20мс, а в Opera 12 і IE9 — більше 100мс. Для завантаження Пошти потрібно мінімум шість файлів. З урахуванням того, що JavaScript в браузері — однопотоковий, то отримуємо послідовний розрахунок хеш для кожного файлу, тобто в кращому випадку це буде мінімум 120мс, а в реальності — ще більше.

Щоб уникнути таких великих втрат у швидкості завантаження, ми вирішили для експерименту вважати суму тільки для частини файлу — початку, середини, кінця. Це, природно, вирішило проблему затримки, але принесло іншу — будь-яка помилка в процесі генерування або накладення патча могла призвести до непередбачуваних наслідків, які буде складно знайти в процесі тестування.

Проблема посилюється ще й тим, що, скажімо, валідність JS ми можемо перевірити просто виконавши його. Якщо отриманий JS не запускається, то патч не пройшов — треба все прати і перезавантажувати. А ось з CSS проблема залишається відкритою. Невалидный CSS просто здійсниться, не викличе ніяких помилок, але користувач побачить не то (у нас навіть були такі випадки). Так, можна дописати CSS валідатор після його виконання, але це знову ж таки вплине на швидкість завантаження — за аналогією з хешсуммой.

Ще є проблема із занадто великими латками. Ми обмежили розмір патча 30% від вихідного файлу. Якщо патч виходить занадто великим, то браузер завантажує весь файл цілком. І в підсумку з цією схемою ми отримали блокуючий HTTP-запит. Розповісти заздалегідь, з яких версій можна оновлюватися, а за яких — ні, ми не можемо, тому що інформація про кешу в браузері. Відповідно, наш завантажувач розуміє, що є кеш і йде за патчами. В цей момент нічого більше не вантажиться. Якщо в латках сказано «вантаж все повністю», то завантажувач починає звичайний процес завантаження, як ніби немає кешей, але час вже втрачено.

По суті патч VS. завантаження файлу — це змагання між швидкістю браузера і швидкістю підключення до інтернету. Якщо дивитися на світові тенденції в цьому розмірі, то комп'ютери не стають швидше. Навпаки, потужні стаціонарні комп'ютери замінюються планшетами і ноутбуками, які не володіють тією ж продуктивністю. У той же час інтернет стає швидше, причому зазвичай безкоштовно. Оновлюються тарифи провайдерів, які будують мережі нового покоління.

У підсумку виходить, що швидше повністю паралельно скачати шість файлів, ніж послідовно для кожного взяти версію з localStorage, завантажити для кожного патч, накласти, перевірити чексумму і запустити.

Підсумок

Ми зрозуміли, що ні на які 146% завантаження пошти таким способом не прискорити. А зайвий головний біль як розробникам, так і тестувальникам отримати легко — з'являється ще один спосіб завантаження сторінки, який треба перевіряти для кожного релізу. У той же час фризи і грамотне розподіл коду на модулі дає зрозумілий і абсолютно безпечний, з точки зору тестування та підтримки, спосіб прискорення.

До речі, HTTP/2 хотіли включити механізм, схожий на RFC3229, але в останніх версіях специфікації його прибрали. У підсумку експеримент з инкрементальным оновленням ми визнали невдалим і вирішили від неї відмовитися. Тепер чекаємо вбудованої підтримки такого механізму в браузери.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.