fix(desktop): устранить белый экран на первом холодном заходе (PWA boot race)
Гонка на первом заходе в проде: ленивый чанк маршрута падал на import() из-за конкуренции за сеть с фоновым precache только что установленного SW, а перехватить ошибку было некому → пустой router-view до ручной перезагрузки. - FIX-1: router.onError ловит провал загрузки чанка и делает авто-reload на целевой путь (с защитой от reload-цикла через sessionStorage). - FIX-2: регистрацию Service Worker откладываем до window 'load', чтобы precache-шторм не конкурировал с первой отрисовкой и догрузкой чанков. - Диагностика: таймстемп-логи [BOOTRACE] по всей boot-цепочке (initApp, router beforeEach/afterEach/onError, App.onMounted/isLoaded, SW register, safety-timeout) для грепа порядка инициализации на проде. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,8 +105,24 @@ if (!shouldRegisterSW) {
|
||||
}
|
||||
};
|
||||
|
||||
// Регистрируем Service Worker (указываем явный путь для SSR режима)
|
||||
register(process.env.SERVICE_WORKER_FILE || '/service-worker.js', {
|
||||
// Диагностический таймстемп первого старта (грепается по слову BOOTRACE).
|
||||
const bootraceTs = (): string => {
|
||||
try {
|
||||
return `${Math.round(performance.now())}ms`;
|
||||
} catch {
|
||||
return '?';
|
||||
}
|
||||
};
|
||||
|
||||
// [FIX-2] Регистрацию SW откладываем до window 'load'. Раньше register()
|
||||
// вызывался на этапе eval модуля — и фоновой precache всего build-manifest
|
||||
// (до 5 МБ/файл) стартовал ОДНОВРЕМЕННО с первой отрисовкой и догрузкой ленивых
|
||||
// чанков маршрутов, конкурируя за сеть. На холодном заходе в проде это и
|
||||
// приводило к провалу import() чанка → пустой router-view. После 'load'
|
||||
// критический путь уже отрисован, precache идёт «вторым эшелоном».
|
||||
const doRegister = () => {
|
||||
console.log(`[BOOTRACE] ${bootraceTs()} SW: старт регистрации (после window load)`);
|
||||
register(process.env.SERVICE_WORKER_FILE || '/service-worker.js', {
|
||||
// The registrationOptions object will be passed as the second argument
|
||||
// to ServiceWorkerContainer.register()
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter
|
||||
@@ -238,6 +254,15 @@ if (!shouldRegisterSW) {
|
||||
console.log('Приложение будет работать без Service Worker');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// window.load уже мог пройти к моменту eval (SSR-гидратация быстрая) —
|
||||
// тогда регистрируем сразу, иначе ждём событие load.
|
||||
if (document.readyState === 'complete') {
|
||||
doRegister();
|
||||
} else {
|
||||
window.addEventListener('load', () => doRegister(), { once: true });
|
||||
}
|
||||
|
||||
// Присваиваем функции глобальному объекту window
|
||||
window.applyUpdate = applyUpdate;
|
||||
|
||||
@@ -28,17 +28,27 @@ const { info } = system;
|
||||
|
||||
const isLoaded = ref(false);
|
||||
|
||||
// [BOOTRACE] таймстемп первого холодного старта (грепается по слову BOOTRACE).
|
||||
const bootraceTs = (): string => {
|
||||
try {
|
||||
return `${Math.round(performance.now())}ms`;
|
||||
} catch {
|
||||
return '?';
|
||||
}
|
||||
};
|
||||
|
||||
// Диалог разрешения уведомлений
|
||||
const { showDialog } = useNotificationPermissionDialog();
|
||||
|
||||
onMounted(async () => {
|
||||
console.log(`[BOOTRACE] ${bootraceTs()} App.onMounted старт`);
|
||||
const SAFETY_REMOVE_LOADER_MS = 60_000;
|
||||
let safetyTimerId: ReturnType<typeof setTimeout> | undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
safetyTimerId = setTimeout(() => {
|
||||
if (!isLoaded.value) {
|
||||
console.warn(
|
||||
'Экран загрузки снят по таймауту: boot или инициализация зависли дольше ожидаемого.',
|
||||
`[BOOTRACE] ${bootraceTs()} SAFETY-TIMEOUT 60s: boot/инициализация зависли, снимаем лоадер принудительно`,
|
||||
);
|
||||
removeLoader();
|
||||
isLoaded.value = true;
|
||||
@@ -113,6 +123,7 @@ onMounted(async () => {
|
||||
|
||||
removeLoader();
|
||||
isLoaded.value = true;
|
||||
console.log(`[BOOTRACE] ${bootraceTs()} App.isLoaded=true (лоадер снят, router-view рендерится)`);
|
||||
clearSafetyTimer();
|
||||
|
||||
// Показываем диалог разрешения уведомлений после загрузки
|
||||
|
||||
@@ -15,6 +15,17 @@ function getHistoryMode() {
|
||||
return env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory;
|
||||
}
|
||||
|
||||
// Диагностический логгер первого холодного старта. Грепается по слову BOOTRACE.
|
||||
// performance.now() — мс от старта документа; позволяет восстановить порядок гонки
|
||||
// «загрузка столов ↔ регистрация маршрутов ↔ навигация гарда ↔ догрузка чанка».
|
||||
function bootraceTs(): string {
|
||||
try {
|
||||
return `${Math.round(performance.now())}ms`;
|
||||
} catch {
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
// Main route export
|
||||
export default route(function () {
|
||||
const Router = createRouter({
|
||||
@@ -25,5 +36,75 @@ export default route(function () {
|
||||
},
|
||||
});
|
||||
|
||||
// [FIX-1] Авто-восстановление при провале загрузки ленивого чанка маршрута.
|
||||
// На первом холодном заходе в проде import() компонента маршрута может упасть
|
||||
// (сетевая латентность + фоновой precache-шторм только что установленного SW).
|
||||
// Без этого обработчика Vue Router молча гасит навигацию → router-view пустой →
|
||||
// белый экран до ручной перезагрузки. Здесь превращаем провал в авто-reload на
|
||||
// целевой путь (тёплый кэш SW отдаёт чанки мгновенно — повтор проходит).
|
||||
if (typeof window !== 'undefined') {
|
||||
Router.onError((err: unknown, to) => {
|
||||
const message = (err as { message?: string })?.message ?? String(err);
|
||||
const isChunkError =
|
||||
/dynamically imported module|Importing a module|ChunkLoadError|Failed to fetch|error loading dynamically imported module/i.test(
|
||||
message,
|
||||
);
|
||||
|
||||
console.error(`[BOOTRACE] ${bootraceTs()} router.onError`, {
|
||||
isChunkError,
|
||||
target: to?.fullPath,
|
||||
message,
|
||||
});
|
||||
|
||||
if (isChunkError) {
|
||||
// Защита от бесконечного reload-цикла: помечаем попытку в sessionStorage,
|
||||
// чтобы при повторном провале на той же сессии не зациклиться.
|
||||
const guardKey = '__bootrace_chunk_reload__';
|
||||
let alreadyTried = false;
|
||||
try {
|
||||
alreadyTried = sessionStorage.getItem(guardKey) === to?.fullPath;
|
||||
if (!alreadyTried && to?.fullPath) {
|
||||
sessionStorage.setItem(guardKey, to.fullPath);
|
||||
}
|
||||
} catch {
|
||||
// sessionStorage недоступен (инкогнито с жёсткими настройками) — просто reload
|
||||
}
|
||||
|
||||
if (!alreadyTried) {
|
||||
console.warn(
|
||||
`[BOOTRACE] ${bootraceTs()} chunk-load failed → авто-reload на ${to?.fullPath ?? 'текущий'}`,
|
||||
);
|
||||
if (to?.fullPath) window.location.assign(to.fullPath);
|
||||
else window.location.reload();
|
||||
} else {
|
||||
console.error(
|
||||
`[BOOTRACE] ${bootraceTs()} chunk-load снова упал после reload — НЕ зацикливаемся`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// [BOOTRACE] Тайминг навигации — видно, когда гард стартует относительно
|
||||
// загрузки столов и успевают ли зарегистрироваться динамические маршруты.
|
||||
Router.beforeEach((to, from) => {
|
||||
const matched = to.matched.length;
|
||||
console.log(
|
||||
`[BOOTRACE] ${bootraceTs()} beforeEach ${from.fullPath ?? '∅'} → ${to.fullPath} (matched=${matched})`,
|
||||
);
|
||||
// Успешная навигация на сматченный путь — снимаем reload-guard.
|
||||
try {
|
||||
if (matched > 0) sessionStorage.removeItem('__bootrace_chunk_reload__');
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
});
|
||||
|
||||
Router.afterEach((to) => {
|
||||
console.log(
|
||||
`[BOOTRACE] ${bootraceTs()} afterEach установлен маршрут ${to.fullPath} (name=${String(to.name ?? '∅')})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return Router;
|
||||
});
|
||||
|
||||
@@ -13,12 +13,27 @@ import { LocalStorage } from 'quasar';
|
||||
// Проверка, работаем ли мы на сервере (SSR)
|
||||
const isServer = typeof window === 'undefined';
|
||||
|
||||
// [BOOTRACE] Диагностика порядка инициализации первого холодного старта.
|
||||
// Грепается по слову BOOTRACE. Логируем только на клиенте — гонка именно там.
|
||||
function bootrace(stage: string): void {
|
||||
if (isServer) return;
|
||||
let ts = '?';
|
||||
try {
|
||||
ts = `${Math.round(performance.now())}ms`;
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
console.log(`[BOOTRACE] ${ts} initApp: ${stage}`);
|
||||
}
|
||||
|
||||
export async function useInitAppProcess(router: Router) {
|
||||
bootrace('start');
|
||||
applyThemeFromStorage();
|
||||
const system = useSystemStore();
|
||||
|
||||
try {
|
||||
await system.loadSystemInfo();
|
||||
bootrace('loadSystemInfo OK');
|
||||
|
||||
// Сохраняем реферала из URL, если он есть
|
||||
if (!isServer) {
|
||||
@@ -31,6 +46,7 @@ export async function useInitAppProcess(router: Router) {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
bootrace('loadSystemInfo FAIL');
|
||||
console.warn('Failed to load initial system info, backend might be unavailable:', error);
|
||||
// Продолжаем инициализацию даже при недоступности бэкенда
|
||||
}
|
||||
@@ -49,15 +65,19 @@ export async function useInitAppProcess(router: Router) {
|
||||
|
||||
try {
|
||||
await desktops.loadDesktop();
|
||||
bootrace('loadDesktop OK');
|
||||
} catch (error) {
|
||||
bootrace('loadDesktop FAIL');
|
||||
console.warn('Failed to load desktop configuration:', error);
|
||||
// Продолжаем инициализацию даже при ошибках загрузки десктопа
|
||||
}
|
||||
|
||||
// Регистрируем маршруты рабочего стола до выбора активного рабочего стола
|
||||
desktops.registerWorkspaceMenus(router);
|
||||
bootrace(`registerWorkspaceMenus done (routes=${router.getRoutes().length})`);
|
||||
|
||||
await useInitWalletProcess().run();
|
||||
bootrace('initWallet done');
|
||||
|
||||
// Выбираем authorized-рабочий стол только если пайщик принят советом
|
||||
// (status='active'). На промежуточных статусах оставляем дефолтный
|
||||
@@ -70,7 +90,9 @@ export async function useInitAppProcess(router: Router) {
|
||||
useBranchOverlayProcess();
|
||||
|
||||
setupNavigationGuard(router);
|
||||
bootrace('navigationGuard installed');
|
||||
|
||||
await useInitExtensionsProcess(router);
|
||||
bootrace(`initExtensions done (routes=${router.getRoutes().length})`);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user