Меню Закрыть

Как мы ускорили работу Notion в браузере с помощью WASM SQLite

Последнее изменение: 12.08.2024
Вы здесь:
Расчетное время чтения: 7 мин
Как мы ускорили работу Notion в браузере с помощью WASM SQLite

Три года назад мы успешно ускорили работу приложения Notion для Mac и Windows за счет использования базы данных SQLite для кэширования данных на клиенте. Мы также используем это кэширование SQLite в нашем родном мобильном приложении.

В этом году нам удалось добиться такого же улучшения для пользователей, которые обращаются к Notion через веб-браузер. В этой статье мы подробно рассмотрим, как мы использовали реализацию sqlite3 в WebAssembly (WASM) для повышения производительности Notion в браузере.

Использование SQLite увеличило время навигации по странице на 20 % во всех современных браузерах. При этом разница была еще более заметна для пользователей, которые подвержены особенно медленному времени отклика API из-за внешних факторов, таких как подключение к Интернету. Например, время навигации по странице ускорилось на 28 % для пользователей из Австралии, на 31 % для пользователей из Китая и на 33 % для пользователей из Индии.

Как мы ускорили работу Notion в браузере с помощью WASM SQLite
WASM SQLite сократил время перехода с одной страницы на другую на 20 процентов.

Давайте перейдем к тому, как настроить SQLite в браузере!

Основные технологии: OPFS и Web Workers

Для сохранения данных во время сеансов библиотека WASM SQLite использует Origin Private File System (OPFS), современный API браузера, позволяющий сайту читать из файлов на устройстве пользователя и записывать в них.

Библиотека WASM SQLite может использовать OPFS только для слоя постоянства в Web Workers. Web Worker можно рассматривать как код, который выполняется в отдельном потоке, отличном от основного потока в браузере, где выполняется большинство JavaScript. Notion поставляется в комплекте с Webpack, который, к счастью, предоставляет простой в использовании синтаксис для загрузки Web Worker. Мы настроили наш Web Worker на создание файла базы данных SQLite с помощью OPFS или загрузку существующего файла. Затем мы запустили наш существующий код кэширования на этом Web Worker. Мы использовали отличную библиотеку Comlink, чтобы легко управлять передачей сообщений между главным потоком и рабочим.

Наш подход, основанный на технологии SharedWorker

Наша окончательная архитектура была основана на новом решении, которое Рой Хашимото изложил в этом обсуждении на GitHub. Хашимото описал подход, при котором только одна вкладка одновременно обращается к SQLite, но при этом позволяет другим вкладкам выполнять запросы SQLite.

Как работает эта новая архитектура? В двух словах, каждая вкладка имеет свой собственный Web Worker, который может писать в SQLite. Однако только одной вкладке разрешено использовать свой Web Worker. SharedWorker отвечает за управление «active tab». Когда активная вкладка закрывается, SharedWorker знает, что нужно выбрать новую активную вкладку. Чтобы обнаружить закрытые вкладки, мы открываем бесконечно открытый Web Lock на каждой вкладке, и если этот Web Lock закрывается, то вкладка должна быть закрыта.

Как мы ускорили работу Notion в браузере с помощью WASM SQLite
Архитектура нашей реализации WASM SQLite на базе SharedWorker.

Для выполнения любого SQLite-запроса главный поток каждой вкладки отправляет запрос на SharedWorker, который перенаправляет его на выделенный Worker активной вкладки. Любое количество вкладок может делать одновременные SQLite-запросы сколько угодно раз, и они всегда будут перенаправлены на единственную активную вкладку.

Каждый Web Worker получает доступ к базе данных SQLite, используя реализацию OPFS SyncAccessHandle Pool VFS, которая работает во всех основных браузерах.

В следующих разделах мы объясним, почему нам нужно было построить его именно таким образом, и с какими препятствиями мы столкнулись, когда пробовали разные подходы.

Почему более простой подход не сработал

До создания описанной выше архитектуры мы пытались запустить WASM SQLite более простым способом – один выделенный Web Worker на вкладку, каждый Web Worker записывает данные в базу данных SQLite.

У нас было две альтернативные реализации WASM SQLite, из которых мы могли выбрать:

В итоге мы пришли к выводу, что ни один из них, если использовать его по прямому назначению, не удовлетворяет нашим потребностям.

Камень преткновения № 1: кросс-оригинальная изоляция

OPFS через sqlite3_vfs требует, чтобы ваш сайт был «кросс-оригинально изолирован». Добавление кросс-оригинальной изоляции на страницу включает в себя установку нескольких заголовков безопасности, которые ограничивают загрузку скриптов. Хорошее место, чтобы узнать больше об этом, – «COOP и COEP Explained».

Установка этих заголовков была бы серьезной задачей. При кросс-оригинальной изоляции недостаточно установить эти два заголовка на странице. Все кросс-оригинальные ресурсы, загружаемые вашим приложением, должны устанавливать другой заголовок, а все кросс-оригинальные iframe должны добавлять дополнительный атрибут, позволяющий им работать в кросс-оригинальной изолированной среде. В компании Notion мы зависим от множества сторонних скриптов, обеспечивающих работу различных функций нашей веб-инфраструктуры, и для достижения полной кросс-оригинальной изоляции пришлось бы просить каждого из этих поставщиков установить новый заголовок и изменить принцип работы своих iframe – нереальная задача.

В ходе нашего тестирования мы смогли получить важные данные о производительности, отправив этот вариант подмножеству пользователей с помощью Origin Trials для SharedArrayBuffer, доступных в браузерах Chrome и Edge. Эти Origin Trials позволили нам временно обойти требование кросс-оригинальной изоляции.

Использование этого обходного пути означало, что мы могли включить эту функцию только в Chrome и Edge, но не в других распространенных браузерах, таких как Safari. Но трафика Notion из этих браузеров было более чем достаточно для сбора данных о производительности.

Как мы ускорили работу Notion в браузере с помощью WASM SQLite
Проба Origin, которая позволяет использовать SharedArrayBuffer – необходимое условие для WASM SQLite – для пользователей Chrome в вашем домене без необходимости включения кросс-оригинальной изоляции.

Камень преткновения № 2: проблемы коррупции

Когда мы включили OPFS через sqlite3_vfs для небольшого процента наших пользователей, мы начали наблюдать серьезную ошибку у некоторых из них. Эти пользователи могли видеть неправильные данные на странице – например, комментарий, приписанный не тому коллеге, или ссылку на новую страницу, превью которой было совершенно другим.

Очевидно, что мы не могли запустить эту функцию для 100 % трафика в таком состоянии. Просматривая файлы баз данных пользователей, пострадавших от этой ошибки, мы заметили закономерность: их базы данных SQLite были каким-то образом повреждены. При выборе строк в определенных таблицах возникала ошибка, а когда мы изучали сами строки, то обнаруживали проблемы с согласованностью данных, например несколько строк с одинаковым идентификатором, но разным содержимым.

Очевидно, это и было причиной некорректных данных. Но как база данных SQLite оказалась в таком состоянии? Мы предположили, что проблема была вызвана параллелизмом. Могло быть открыто несколько вкладок, и на каждой вкладке был выделен Web Worker, который имел активное соединение с базой данных SQLite. Приложение Notion часто записывает данные в кэш – оно делает это каждый раз, когда получает обновление с сервера, а значит, вкладки будут писать в один и тот же файл одновременно. Несмотря на то, что мы уже использовали транзакционный подход, который объединял запросы SQLite в пакет, мы сильно подозревали, что повреждение было вызвано плохой обработкой параллелизма от имени OPFS API. Несколько обсуждений на форуме SQLite, похоже, подтвердили, что у других были проблемы с тем, как OPFS управляет параллелизмом (можно сказать, что не очень).

Как мы ускорили работу Notion в браузере с помощью WASM SQLite
Архитектура WASM Sqlite в тот момент, когда мы наблюдали проблемы с коррупцией.

Поэтому мы начали регистрировать ошибки повреждения, а затем попробовали несколько пластырей, таких как добавление веб-замков и запись в SQLite только на фокусной вкладке. Эти меры снизили уровень повреждения, но не настолько, чтобы мы были уверены в том, что можем снова включить функцию в производственный трафик. Тем не менее, мы убедились, что проблемы параллелизма вносят значительный вклад в возникновение повреждений.

Настольное приложение Notion не страдало от этой проблемы. На этой платформе только один родительский процесс записывает данные в SQLite; вы можете открыть сколько угодно вкладок в приложении, и только один поток будет обращаться к файлу базы данных. В нашем мобильном нативном приложении одновременно может быть открыта только одна страница, но даже если бы в нем было несколько вкладок, его архитектура в этом отношении была бы аналогична настольному приложению.

Камень преткновения № 3: альтернатива могла работать только на одной вкладке одновременно

Мы также оценили вариант OPFS SyncAccessHandle Pool VFS. Этот вариант не требует SharedArrayBuffer, что означает, что его можно использовать в Safari, Firefox и других браузерах, которые не имеют Origin Trial для SharedArrayBuffer.

Недостатком этого варианта является то, что он может работать только в одной вкладке одновременно; любая попытка открыть базу данных SQLite в последующей вкладке просто приведет к ошибке.

С одной стороны, это означало, что у OPFS SyncAccessHandle Pool VFS не было проблем с параллелизмом, характерных для варианта OPFS via sqlite3_vfs. Мы подтвердили это, когда включили его для небольшого процента пользователей и не увидели никаких проблем с коррупцией. С другой стороны, мы не могли запустить этот вариант из коробки, поскольку хотели, чтобы кэширование было полезно для всех вкладок наших пользователей.

Разрешение

Тот факт, что ни один из вариантов не может быть использован из коробки, побудил нас создать описанную выше архитектуру SharedWorker, совместимую с любым из этих вариантов SQLite. При использовании варианта OPFS через sqlite3_vfs мы избегаем проблем с повреждениями, так как за раз записывается только одна вкладка. При использовании варианта OPFS SyncAccessHandle Pool VFS все вкладки могут иметь кэширование благодаря SharedWorker.

После того как мы убедились, что архитектура работает в обоих вариантах, что прирост производительности заметен в наших метриках и что нет проблем с коррупцией, пришло время сделать окончательный выбор, какой вариант поставлять. Мы выбрали OPFS SyncAccessHandle Pool VFS, потому что он не требовал кросс-оригинальной изоляции, что не позволило бы нам развернуть его на любом браузере, кроме Chrome и Edge.

Смягчение регрессий

Когда мы только начали отправлять это улучшение пользователям, мы заметили несколько регрессий, которые нам пришлось исправить, в том числе замедление времени загрузки.

Загрузка страницы происходила медленнее

Наше первое наблюдение заключалось в том, что, хотя переход с одной страницы Notion на другую происходил быстрее, начальная загрузка страницы была медленнее. После некоторого профилирования мы поняли, что загрузка страницы обычно не связана с получением данных – код загрузки нашего приложения выполняет другие операции (парсинг JS, настройка приложения и т. д.) в ожидании завершения вызовов API и, таким образом, не выигрывает от кэширования SQLite так же сильно, как навигация.

Почему это происходило медленнее? Потому что пользователям приходилось загружать и обрабатывать библиотеку WASM SQLite, которая блокировала процесс загрузки страницы, не позволяя другим операциям загрузки страницы происходить одновременно. Поскольку размер этой библиотеки составляет несколько сотен килобайт, дополнительное время было заметно в наших показателях.

Чтобы исправить это, мы немного изменили способ загрузки библиотеки – мы загружали WASM SQLite полностью асинхронно и следили за тем, чтобы он не блокировал загрузку страницы. Это означало, что начальные данные страницы редко загружались из SQLite. Это было нормально, поскольку мы объективно определили, что ускорение от загрузки начальной страницы из SQLite не перевешивает замедление от загрузки библиотеки.

После внесения этого изменения метрика загрузки начальной страницы стала одинаковой для тестовой и контрольной групп эксперимента.

Медленным устройствам кэширование не помогло

Еще один феномен, который мы заметили в наших показателях: в то время как медианное время перехода с одной страницы Notion на другую было быстрее, время 95-го процентиля было медленнее. Некоторые устройства, например мобильные телефоны, чьи браузеры были направлены на Notion, не получали никакой пользы от кэширования, а на самом деле даже ухудшали свои показатели.

Мы нашли ответ на эту загадку в предыдущем исследовании, проведенном нашей мобильной командой. Когда они реализовали это кэширование в нашем родном мобильном приложении, некоторые устройства, например старые телефоны на базе Android, считывали данные с диска крайне медленно. Поэтому мы не могли предположить, что загрузка данных из дискового кэша будет быстрее, чем загрузка тех же данных из API.

В результате этого мобильного исследования в загрузке нашей страницы уже была заложена логика, с помощью которой мы «гоняли» два асинхронных запроса (SQLite и API) друг за другом. Мы просто повторно реализовали эту логику в коде для навигационных кликов. Это позволило выровнять 95-й процентиль времени навигации между двумя экспериментальными группами.

Заключение

Обеспечение повышения производительности SQLite в Notion в браузере было сопряжено с определенными трудностями. Мы столкнулись с рядом неизвестных моментов, в частности с новыми технологиями, и извлекли несколько уроков на этом пути:

  • OPFS не поставляется с изящной обработкой параллелизма из коробки. Разработчики должны знать об этом и проектировать с учетом этого.
  • Web Workers и SharedWorkers (а также их двоюродный брат, не упомянутый в этом посте, Service Workers) имеют разные возможности, и при необходимости может быть полезно их комбинировать.
  • По состоянию на весну 2024 года полностью реализовать кросс-оригинальную изоляцию в сложном веб-приложении будет непросто, особенно если вы используете сторонние скрипты.

Благодаря использованию SQLite для браузеров, кэширующих данные для наших пользователей, мы увидели вышеупомянутое 20-процентное улучшение времени навигации и не заметили ухудшения каких-либо других показателей. Важно отметить, что мы не наблюдали никаких проблем, связанных с повреждением SQLite. Успех и стабильность нашего финального подхода мы ставим в заслугу команде, создавшей официальную реализацию SQLite в WASM, а также Рою Хашимото и экспериментальным подходам, которые они предоставили общественности.

Хотите внести свой вклад в такую работу в Notion? Ознакомьтесь с нашими вакансиями здесь →

Была ли эта статья полезной?
Нет 0
Просмотров: 65

Читать далее

Предыдущий: Создание и масштабирование озера данных Notion