Open Finapp
Справочник

Производительность

Бюджет холодного старта, ленивые чанки, бандлинг иконок и встроенные метки инструментирования.

Что держит холодный старт быстрым и как его измерять. Контрольные цифры ниже сняты на прод-сборке (pnpm generate) с реальным аккаунтом в ~8 000 транзакций, на десктопном железе, с ассетами из service worker - воспринимайте их как относительные, а не абсолютные.

Бюджет холодного старта

В загрузке доминирует выполнение JS, а не сеть (всё закешировано в SW). Хранилища наполняются двумя путями:

ПутьДанные на экране
Наполнение из blob-кеша (cache:prime, чтение ~70 мс)~280 мс
Первая эмиссия watch для trns из SQLite~730 мс

Этот разрыв в ~450 мс - причина существования blob-кеша: его ценность в ранней отрисовке, а не в пропуске трансформации строк (трансформация 8k строк занимает ~3 мс - стоимость в скане SQLite + передаче из воркера, а не в JS).

Пять загрузочных подписок сериализованы на единственном worker-подключении PowerSync - их первые эмиссии приходят лесенкой (user_settingsratescategorieswalletstrns), и доминирует скан всей таблицы trns. Слияние четырёх маленьких запросов дало бы мало; оконная выборка trns по дате отвергнута, потому что итогам кошельков нужна полная история (см. Технические решения).

Бандл: что и когда загружается

  • @powersync/web ленивый. getPowerSyncDb() (app/services/powersync/db.ts) динамически импортирует библиотеку, схему и коннектор при первом использовании, убирая ~230 КБ из входного чанка - страница логина и демо-режим его вообще не парсят. Это охраняют две детали: uploadErrorHandler.ts существует, чтобы статический импорт плагина не дотягивался до connector.ts, а uploadReconcile.ts сравнивает строки операций вместо импорта enum UpdateType.
  • swiper ленивый. Форма транзакции (LazyTrnFormBottom / LazyTrnFormSidebar в дефолтном layout) монтируется после первого открытия; idle-префетч делает это первое открытие мгновенным.
  • echarts монтируется в idle. Графики спрятаны за useIdleMount() с плейсхолдером равной высоты (stat/chart/Wrap.vue), поэтому чанк echarts в 450 КБ никогда не конкурирует с первой отрисовкой.
  • Скелетон дашборда. StatPageSkeleton резервирует форму страницы, пока хранилища гидрируются, поэтому приходящие данные не сдвигают layout.
  • Иконки в бандле. Все статически используемые имена иконок поставляются в клиентском бандле (icon.clientBundle.icons в nuxt.config.ts, включая коллекцию hugeicons) - иконки, загружаемые с api.iconify.design в рантайме, появляются после первой отрисовки и сдвигают layout (CLS). Сканер иконок не видит имена внутри динамических тернарников, отсюда явный список. Только иконки пользовательских данных (иконки категорий/кошельков вне бандла) по-прежнему загружаются в рантайме и кешируются SW (CacheFirst).
  • Прекеш SW сужен. Из восьми эмитируемых WASM-вариантов wa-sqlite прекешируется только wa-sqlite-async.<hash>.wasm (тот, который воркер действительно загружает - cipher-сборкам нужен encryptionKey, который мы никогда не передаём), что экономит ~12 МБ на каждую установку service worker.

Инструментирование

Метки холодного старта включены всегда (они ничего не стоят). В консоли браузера после перезагрузки:

performance.getEntriesByType('measure')
  .filter(m => /^(ps:watch|cache:|trns:)/.test(m.name))
  .map(m => `${m.name}: ${Math.round(m.startTime)}ms +${Math.round(m.duration)}ms`)
МеткаЗначение
cache:primeЧтение blob-кеша + наполнение хранилищ (путь отрисовки холодного старта)
ps:watch:<table>Подписка → первая эмиссия SQLite для каждой загрузочной подписки
trns:first-transformСтоимость трансформации строка → элемент для первой эмиссии trns

Отвергнутые подходы

Задокументированы, чтобы их не пробовали заново без новых данных (детали в Технических решениях и todo.md):

  • OPFS VFS вместо IDBBatchAtomicVFS - холодный старт ограничен db.init + сериализацией запросов, а не страничным I/O.
  • Оконная подписка trns по дате - ломает итоги кошельков, которым нужна полная история.
  • Разделение vendor-чанков через manualChunks - фрагментация reka-ui ломает inject/createContext в продакшен-сборках.
  • Хранение items в Map - экономит единицы миллисекунд на запись при 8k строк, но ломает всех читателей items как обычного объекта.