diff --git a/.gitignore b/.gitignore index ce10c544a..be659a7e0 100755 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ app-release-* .claude_settings.json .env.local +/output/performance/ diff --git a/app/client.tsx b/app/client.tsx index c06ae0455..344761ab3 100644 --- a/app/client.tsx +++ b/app/client.tsx @@ -15,7 +15,9 @@ import './redirectToHome'; import { isCapacitor } from 'utils/deviceInfo'; import precache from 'precache.ts'; import { startPerformanceTelemetry } from 'utils/performanceTelemetry'; +import { markPerformance } from 'utils/performanceMarks'; +markPerformance('bundle_evaluated'); window.APP_LOADED = true; const isProd = process.env.NODE_ENV === 'production'; @@ -42,6 +44,7 @@ if (isProd) { const rootElement = document.getElementById('react-root'); const root = createRoot(rootElement); +markPerformance('react_render_requested'); root.render( @@ -52,9 +55,11 @@ if (isProd) { serviceWorker.register(); } if (isCapacitor()) { + markPerformance('background_precache_started', { mode: 'capacitor' }); precache(); } else { const precacheWorker = new Worker(); + markPerformance('background_precache_started', { mode: 'worker' }); precacheWorker.postMessage('precache'); } diff --git a/app/components/TOC/TOCProvider.tsx b/app/components/TOC/TOCProvider.tsx index af057d985..10d97b6a2 100644 --- a/app/components/TOC/TOCProvider.tsx +++ b/app/components/TOC/TOCProvider.tsx @@ -2,6 +2,8 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useSt import { getTOCSnapshotSignature, TOCHeadingDefinition, TOCItem, TOCRegistryController } from './tocRegistry'; +import { getCurrentNavigationMeasurement, markPerformance } from 'utils/performanceMarks'; + const TOCRegistrationContext = createContext(null); const TOCItemsContext = createContext([]); const TOC_READY_DELAY_MS = 500; @@ -25,7 +27,7 @@ export const TOCProvider = ({ children }: { children: React.ReactNode }): JSX.El } const register = controllerRef.current.register; - const lastMarkedSignatureRef = useRef(''); + const lastMarkedSnapshotRef = useRef<{ navigationKey: string; signature: string }>(); useEffect(() => { if (typeof window.performance?.clearMarks === 'function') { @@ -42,17 +44,21 @@ export const TOCProvider = ({ children }: { children: React.ReactNode }): JSX.El return undefined; } + const navigation = getCurrentNavigationMeasurement(); const signature = getTOCSnapshotSignature(items); - if (signature === lastMarkedSignatureRef.current) { + if ( + signature === lastMarkedSnapshotRef.current?.signature && + navigation.key === lastMarkedSnapshotRef.current.navigationKey + ) { return undefined; } const timeoutId = window.setTimeout(() => { if (typeof window.performance?.mark === 'function') { window.performance.clearMarks('service_toc_ready'); - window.performance.mark('service_toc_ready'); + markPerformance('service_toc_ready', { tocEntryCount: items.length }, navigation); } - lastMarkedSignatureRef.current = signature; + lastMarkedSnapshotRef.current = { navigationKey: navigation.key, signature }; }, TOC_READY_DELAY_MS); return () => window.clearTimeout(timeoutId); diff --git a/app/containers/App.tsx b/app/containers/App.tsx index 713bac4a9..f3f3838fc 100644 --- a/app/containers/App.tsx +++ b/app/containers/App.tsx @@ -18,6 +18,7 @@ import precache from 'precache'; import isDarkMode from 'utils/isDarkMode'; import RouteErrorBoundary from 'components/RouteErrorBoundary/RouteErrorBoundary'; import ScrollRestoration from 'components/ScrollRestoration/ScrollRestoration'; +import { markNavigationIntent, markPerformance } from 'utils/performanceMarks'; const queryClient = new QueryClient({ defaultOptions: { @@ -37,9 +38,47 @@ const App = () => { if (loader && reactRoot) { loader.style.display = 'none'; reactRoot.style.display = 'block'; + markPerformance('inline_loader_hidden'); } void Plugins.SplashScreen.hide(); }, []); + useEffect(() => { + const markLinkNavigationIntent = (event: MouseEvent) => { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { + return; + } + + const eventTarget = event.target; + if (!(eventTarget instanceof Element)) { + return; + } + + const link = eventTarget.closest('a[href]'); + if (!link || link.hasAttribute('download') || link.getAttribute('target') === '_blank') { + return; + } + + const destination = new URL((link as HTMLAnchorElement).href, window.location.href); + if (destination.origin !== window.location.origin) { + return; + } + + markNavigationIntent({ initiator: 'link-click', target: `${destination.pathname}${destination.hash}` }); + }; + const markHistoryNavigationIntent = () => { + markNavigationIntent({ + initiator: 'browser-history', + target: `${window.location.pathname}${window.location.hash}`, + }); + }; + + document.addEventListener('click', markLinkNavigationIntent, { capture: true, passive: true }); + window.addEventListener('popstate', markHistoryNavigationIntent); + return () => { + document.removeEventListener('click', markLinkNavigationIntent, { capture: true }); + window.removeEventListener('popstate', markHistoryNavigationIntent); + }; + }, []); const dark = isDarkMode(); return ( diff --git a/app/containers/Main/HeaderMain.tsx b/app/containers/Main/HeaderMain.tsx index bf8742389..292f321e3 100644 --- a/app/containers/Main/HeaderMain.tsx +++ b/app/containers/Main/HeaderMain.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { css } from '@emotion/css'; import { useTheme } from '@emotion/react'; import { useQuery } from 'convex/react'; @@ -14,6 +14,7 @@ import Share from 'components/Share/Share'; import useDay from 'hooks/useDay'; import SettingsButton from 'components/SettingsButton/SettingsButton'; import CalendarStreakWidget from 'containers/HabitTracker/CalendarStreakWidget'; +import { markNavigationIntent, markPerformance } from 'utils/performanceMarks'; import { api } from '../../../convex/_generated/api'; @@ -66,6 +67,7 @@ const ProfileIcon = ({ hasUnread }: { hasUnread: boolean }) => {