Кэширование кода для разработчиков WebAssembly
Среди разработчиков есть поговорка, что самый быстрый код — это код, который не выполняется. Аналогично, самый быстрый компиляционный код — это код, который не нужно компилировать. Кэширование кода WebAssembly — это новая оптимизация в Chrome и V8, которая старается избежать компиляции кода, кэшируя скомпилированный нативный код. Мы писали о том, как Chrome и V8 кэшируют JavaScript-код в прошлом, а также о лучших практиках использования этой оптимизации. В этом посте мы опишем, как работает система кэширования кода WebAssembly в Chrome и как разработчики могут использовать её для ускорения загрузки приложений с большими модулями WebAssembly.
Резюме о компиляции WebAssembly
WebAssembly — это способ запуска не-JavaScript кода в интернете. Веб-приложение может использовать WebAssembly, загружая ресурс .wasm
, который содержит частично скомпилированный код на языке программирования, таком как C, C++ или Rust (и в будущем будут поддержаны другие). Задача компилятора WebAssembly заключается в декодировании ресурса .wasm
, проверке его корректности, а затем компиляции в нативный машинный код, который может быть выполнен на машине пользователя.
V8 имеет два компилятора для WebAssembly: Liftoff и TurboFan. Liftoff — это базовый компилятор, который компилирует модули как можно быстрее, чтобы выполнение могло начаться как можно быстрее. TurboFan — это оптимизирующий компилятор V8 как для JavaScript, так и для WebAssembly. Он работает в фоновом режиме, чтобы генерировать высококачественный нативный код, обеспечивая веб-приложению оптимальную производительность в долгосрочной перспективе. Для больших модулей WebAssembly TurboFan может занимать значительное время — от 30 секунд до минуты или более — чтобы полностью завершить компиляцию модуля в нативный код.
Именно здесь вступает в игру кэширование кода. После того как TurboFan завершил компиляцию большого модуля WebAssembly, Chrome может сохранить код в кэше, чтобы при следующей загрузке модуля можно было пропустить компиляции Liftoff и TurboFan, что приводит к более быстрому запуску и снижению энергопотребления — компиляция кода очень ресурсоемка для процессора.
Кэширование кода WebAssembly использует ту же инфраструктуру в Chrome, что и кэширование JavaScript-кода. Мы используем тот же тип хранилища и ту же технику двойного ключевого кэширования, которая сохраняет код, скомпилированный различными источниками, раздельно в соответствии с изоляцией сайтов, важной функцией безопасности Chrome.
Алгоритм кэширования кода WebAssembly
На текущий момент кэширование WebAssembly реализовано только для потоковых API вызовов: compileStreaming
и instantiateStreaming
. Эти вызовы работают с HTTP-запросами .wasm
-ресурсов, что упрощает использование механизмов загрузки и кэширования ресурсов Chrome, а также предоставляет удобный URL-адрес ресурса для использования в качестве ключа для идентификации модуля WebAssembly. Алгоритм кэширования работает следующим образом:
- Когда ресурс
.wasm
запрашивается впервые (то есть при холодном запуске), Chrome загружает его из сети и отправляет поток V8 для компиляции. Chrome также сохраняет ресурс.wasm
в кэше ресурсов браузера, который хранится в файловой системе устройства пользователя. Этот кэш ресурсов позволяет Chrome загружать ресурс быстрее при следующем использовании. - Когда TurboFan завершает полную компиляцию модуля и ресурс
.wasm
достаточно большой (в настоящее время 128 кБ), Chrome записывает скомпилированный код в кэш кода WebAssembly. Этот кэш кода физически отделен от кэша ресурсов на шаге 1. - Когда ресурс
.wasm
запрашивается во второй раз (то есть при горячем запуске), Chrome загружает ресурс.wasm
из кэша ресурсов и одновременно опрашивает кэш кода. Если найдено совпадение, скомпилированные байты модуля отправляются в процесс рендеринга и передаются V8, который десериализует код вместо компиляции модуля. Десериализация быстрее и менее ресурсоемка для процессора, чем компиляция. - Возможно, закэшированный код больше не является актуальным. Это может произойти из-за изменений ресурса
.wasm
или обновлений V8, которые происходят, как минимум, каждые 6 недель в связи с быстрой цикличной разработкой Chrome. В этом случае кэшированный нативный код удаляется из кэша, и процесс компиляции выполняется, как указано в шаге 1.
Исходя из этого описания, мы можем дать некоторые рекомендации по улучшению использования кэша кода WebAssembly на вашем сайте.
Совет 1: используйте потоковый API WebAssembly
Так как кэширование кода работает только с потоковым API, компилируйте или создавайте свой модуль WebAssembly с помощью compileStreaming
или instantiateStreaming
, как показано в этом примере JavaScript:
(async () => {
const fetchPromise = fetch('fibonacci.wasm');
const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
const result = instance.exports.fibonacci(42);
console.log(result);
})();
Эта статья подробно описывает преимущества использования потокового API WebAssembly. Emscripten пытается использовать этот API по умолчанию, когда генерирует код загрузчика для вашего приложения. Учтите, что потоковая передача требует, чтобы у ресурса .wasm
был корректный MIME-тип, поэтому сервер должен отправлять заголовок Content-Type: application/wasm
в своем ответе.
Совет 2: делайте кэширование дружественным
Так как кэширование кода зависит от URL ресурса и от того, актуален ли ресурс .wasm
, разработчики должны стараться поддерживать их стабильными. Если ресурс .wasm
извлекается из другого URL-адреса, он считается другим, и V8 должен заново скомпилировать модуль. Аналогично, если ресурс .wasm
больше не является допустимым в кэше ресурсов, Chrome должен удалить весь кешированный код.
Сохраняйте стабильность кода
Каждый раз, когда вы выпускаете новый модуль WebAssembly, он должен быть полностью перекомпилирован. Выпускайте новые версии вашего кода только при необходимости для предоставления новых функций или исправления ошибок. Когда ваш код не изменился, дайте знать Chrome. Когда браузер отправляет HTTP-запрос на URL-адрес ресурса, например модуля WebAssembly, он включает дату и время последнего извлечения этого URL. Если сервер знает, что файл не изменился, он может ответить сообщением 304 Not Modified
, что сообщает Chrome и V8 о том, что кэшированный ресурс и, следовательно, кэшированный код все еще актуальны. Если же возвращается ответ 200 OK
, обновляется кэшированный ресурс .wasm
, а кэш кода аннулируется, возвращая WebAssembly к холодному запуску. Следуйте лучшим практикам веб-ресурсов, используя ответ для информирования браузера о том, является ли ресурс .wasm
кэшируемым, как долго он будет оставаться актуальным или когда он был изменен последний раз.
Не изменяйте URL вашего кода
Кэшированный скомпилированный код ассоциируется с URL ресурса .wasm
, что позволяет легко находить его без необходимости сканирования самого ресурса. Это означает, что изменение URL ресурса (включая любые параметры запроса!) создает новую запись в нашем кэше ресурсов, что также требует полного перекомпилирования и создание новой записи в кэше кода.
Делайте больше (но не слишком много!)
Основная эвристика кэширования кода WebAssembly — это размер ресурса .wasm
. Если ресурс .wasm
меньше определенного порогового размера, мы не кэшируем байты скомпилированного модуля. Причина в том, что V8 может быстро компилировать небольшие модули, возможно, быстрее, чем загрузка скомпилированного кода из кэша. На данный момент порог составляет 128 кБ или больше для ресурсов .wasm
.
Но больше — это лучше только до определенного предела. Поскольку кэш занимает место на машине пользователя, Chrome внимательно следит за тем, чтобы не занять слишком много места. Сейчас кэш кода на настольных компьютерах обычно содержит несколько сотен мегабайт данных. Так как в кэше Chrome также ограничивается самый большой запись определенной долей от общей емкости кэша, есть дополнительный лимит около 150 МБ для скомпилированного кода WebAssembly (половина общего размера кэша). Важно учитывать, что скомпилированные модули часто в 5–7 раз больше, чем соответствующий ресурс .wasm
на типичной настольной машине.
Эта эвристика размера, как и остальное поведение кэширования, может измениться, если мы обнаружим, что это наиболее эффективно для пользователей и разработчиков.
Используйте сервисный воркер
Кэширование кода WebAssembly включено для воркеров и сервисных воркеров, поэтому их можно использовать для загрузки, компиляции и кэширования новой версии кода, чтобы он был доступен при следующем запуске вашего приложения. Каждый веб-сайт должен выполнить хотя бы одну полную компиляцию модуля WebAssembly — используйте воркеры, чтобы скрыть это от ваших пользователей.
Трассировка
Как разработчик вы можете проверить, что ваш скомпилированный модуль кэшируется в Chrome. События кэширования кода WebAssembly не отображаются по умолчанию в средствах разработки Chrome, поэтому лучший способ узнать, кэшируются ли ваши модули, — использовать функцию chrome://tracing
на более низком уровне.
chrome://tracing
записывает инструментированные трассировки Chrome за определенный период времени. Трассировка фиксирует поведение всего браузера, включая другие вкладки, окна и расширения, поэтому она работает лучше всего при выполнении в чистом пользовательском профиле, с выключенными расширениями и без открытых других вкладок браузера:
# Запустите новую сессию браузера Chrome с чистым профилем пользователя и отключенными расширениями
google-chrome --user-data-dir="$(mktemp -d)" --disable-extensions
Перейдите на chrome://tracing
и нажмите «Record», чтобы начать сеанс трассировки. В появившемся диалоговом окне нажмите «Edit Categories» и выберите категорию devtools.timeline
справа под «Disabled by Default Categories» (вы можете снять выбор с любых других предварительно выбранных категорий, чтобы уменьшить количество собираемых данных). Затем нажмите кнопку «Record» в диалоговом окне, чтобы начать трассировку.
В другой вкладке загрузите или перезагрузите ваше приложение. Дайте ему работать достаточно долго, 10 секунд или больше, чтобы убедиться, что компиляция TurboFan завершена. Когда завершите, нажмите «Stop», чтобы завершить трассировку. Появится временная шкала событий. В правом верхнем углу окна трассировки есть текстовое поле, справа от «View Options». Введите v8.wasm
, чтобы отфильтровать события, не относящиеся к WebAssembly. Вы должны увидеть одно или несколько из следующих событий:
v8.wasm.streamFromResponseCallback
— Ресурс, переданный в instantiateStreaming, получил ответ.v8.wasm.compiledModule
— TurboFan завершил компиляцию ресурса.wasm
.v8.wasm.cachedModule
— Chrome записал скомпилированный модуль в кэш кода.v8.wasm.moduleCacheHit
— Chrome нашел код в своем кэше при загрузке ресурса.wasm
.v8.wasm.moduleCacheInvalid
— V8 не смог десериализовать кэшированный код, так как он устарел.
При холодном запуске мы ожидаем увидеть события v8.wasm.streamFromResponseCallback
и v8.wasm.compiledModule
. Это указывает на то, что модуль WebAssembly был получен, и компиляция прошла успешно. Если ни одно из событий не наблюдается, проверьте, правильно ли работают ваши вызовы API потоковой передачи WebAssembly.
После холодного запуска, если порог размера был превышен, мы также ожидаем увидеть событие v8.wasm.cachedModule
, что означает, что скомпилированный код был отправлен в кэш. Возможно, мы получим это событие, но запись не будет успешной по какой-то причине. В настоящее время невозможно наблюдать это, но метаданные событий могут показать размер кода. Очень большие модули могут не уместиться в кэше.
Когда кэширование работает правильно, горячий запуск создает два события: v8.wasm.streamFromResponseCallback
и v8.wasm.moduleCacheHit
. Метаданные этих событий позволяют увидеть размер скомпилированного кода.
Подробнее о использовании chrome://tracing
смотрите нашу статью о кэшировании (байт)кода JavaScript для разработчиков.
Заключение
Для большинства разработчиков кэширование кода должно «просто работать». Оно работает лучше, как и любой кэш, когда все стабильно. Эвристики кэширования Chrome могут изменяться между версиями, но кэширование кода имеет особенности, которые можно использовать, и ограничения, которых можно избежать. Тщательный анализ с использованием chrome://tracing
может помочь вам настроить и оптимизировать использование кэша кода WebAssembly в вашем веб-приложении.