Как инкрементальные обновления влияют на скорость загрузки. Опыт Яндекс.Почты
Яндекс.Почта — большое и сложное веб-приложение. Для первоначальной загрузки ей необходимо более 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 выигрывает c большим отрывом. В IE 9 и ниже google-diff-patch-match накладывает патч больше, чем за секунду.
У нас определился победитель — vcdiff. Этот алгоритм был предложен в 2002 году (RFC3284). Он достаточно популярен и имеет множество реализаций на разных языках, в том числе на C++, Java и JS.
После того как мы определились с библиотекой для диффа, нужно определиться с тем, где и как хранить статику на клиенте. Яндекс.Почта — современное веб-приложение, у нас нет старых браузеров, поэтому почти все поддерживают 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 подключаем через динамическую вставку тега