Производительность
Что держит холодный старт быстрым и как его измерять. Контрольные цифры ниже сняты на прод-сборке (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_settings → rates → categories → wallets → trns), и доминирует скан всей таблицы trns. Слияние четырёх маленьких запросов дало бы мало; оконная выборка trns по дате отвергнута, потому что итогам кошельков нужна полная история (см. Технические решения).
Бандл: что и когда загружается
@powersync/webленивый.getPowerSyncDb()(app/services/powersync/db.ts) динамически импортирует библиотеку, схему и коннектор при первом использовании, убирая ~230 КБ из входного чанка - страница логина и демо-режим его вообще не парсят. Это охраняют две детали:uploadErrorHandler.tsсуществует, чтобы статический импорт плагина не дотягивался до connector.ts, аuploadReconcile.tsсравнивает строки операций вместо импорта enumUpdateType.- 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как обычного объекта.