Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ app-release-*
.claude_settings.json

.env.local
/output/performance/
5 changes: 5 additions & 0 deletions app/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -42,6 +44,7 @@ if (isProd) {

const rootElement = document.getElementById('react-root');
const root = createRoot(rootElement);
markPerformance('react_render_requested');
root.render(
<RecoilRoot>
<App />
Expand All @@ -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');
}

Expand Down
14 changes: 10 additions & 4 deletions app/components/TOC/TOCProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TOCRegistryController['register'] | null>(null);
const TOCItemsContext = createContext<TOCItem[]>([]);
const TOC_READY_DELAY_MS = 500;
Expand All @@ -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') {
Expand All @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions app/containers/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 (
<QueryClientProvider client={queryClient}>
Expand Down
9 changes: 8 additions & 1 deletion app/containers/Main/HeaderMain.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -66,6 +67,7 @@ const ProfileIcon = ({ hasUnread }: { hasUnread: boolean }) => {
<Button
title={loggedIn ? 'Выйти' : 'Войти'}
onClick={() => {
markNavigationIntent({ initiator: 'header-profile', target: '/profile' });
void navigate('/profile');
}}
className={css`
Expand Down Expand Up @@ -96,6 +98,7 @@ const UpdatesButton = ({ hasUnread }: { hasUnread: boolean }) => {
<Button
title="Обновления"
onClick={() => {
markNavigationIntent({ initiator: 'header-updates', target: '/updates' });
void navigate('/updates');
}}
className={css`
Expand Down Expand Up @@ -157,6 +160,10 @@ const HeaderMain = ({ setNewDate, date, calendarRef, showUpdatesButton = false }
const hasUnreadUpdates = !!unreadUpdates?.length;
const dayQuery = useDay(date);
const day = dayQuery.data;
useEffect(() => {
markPerformance('app_header_ready', { date });
}, [date]);

return (
<Header>
<div
Expand Down
42 changes: 40 additions & 2 deletions app/containers/Main/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Kondacs from 'containers/Service/Texts/Shared/Kondacs/Kondacs';
import { useDocumentTitle } from 'utils/useDocumentTitle';
import { useRecoilValue } from 'recoil';
import themeState from 'state/themeState';
import { getCurrentNavigationMeasurement, markNavigationIntent, markPerformance } from 'utils/performanceMarks';

import IosPrompt from './IosPrompt';
import Services from './Services';
Expand All @@ -44,7 +45,40 @@ import Sing from './Sing';

const VirtualizeSwipeableViews = virtualize(SwipeableViews);

const SwipeableContainer = React.memo(({ date, handleToggleClick, makeHandleClickShift, services }) => {
const DatePrimaryContentMarker = ({ date }: { date: string }): null => {
React.useEffect(() => {
const navigation = getCurrentNavigationMeasurement();
markPerformance('date_primary_content_ready', { date }, navigation);
let stableFrame = 0;
const firstFrame = window.requestAnimationFrame(() => {
stableFrame = window.requestAnimationFrame(() => {
markPerformance('above_fold_stable', { date }, navigation);
});
});
return () => {
window.cancelAnimationFrame(firstFrame);
window.cancelAnimationFrame(stableFrame);
};
}, [date]);

return null;
};

interface SwipeableContainerProps {
date: string;
handleToggleClick: () => void;
isPrimary: boolean;
makeHandleClickShift: (direction: 'left' | 'right') => () => void;
services: boolean;
}

const SwipeableContainer = React.memo(function SwipeableContainer({
date,
handleToggleClick,
isPrimary,
makeHandleClickShift,
services,
}: SwipeableContainerProps) {
const dayQuery = useDay(date);
const day = dayQuery.data;
const externalDayQuery = useExternalDay(date);
Expand All @@ -70,6 +104,7 @@ const SwipeableContainer = React.memo(({ date, handleToggleClick, makeHandleClic
{dayQuery.status === 'error' && <ErrorMessage500 />}
{dayQuery.status === 'success' && (
<div>
{isPrimary && <DatePrimaryContentMarker date={date} />}
<HeadingBar
title={day.title}
glas={day.glas}
Expand Down Expand Up @@ -189,7 +224,9 @@ const Main = React.memo(({ services = false }) => {

const navigate = useNavigate();
const setNewDate = (dateString) => {
void navigate(`/date/${dateString}${services ? '/services' : ''}`);
const target = `/date/${dateString}${services ? '/services' : ''}`;
markNavigationIntent({ initiator: 'date-change', target });
void navigate(target);
};
const makeHandleClickShift = useCallback(
(direction) => () => {
Expand Down Expand Up @@ -221,6 +258,7 @@ const Main = React.memo(({ services = false }) => {
<SwipeableContainer
key={key}
date={effectiveDate}
isPrimary={index === activeIndex}
services={services}
handleToggleClick={handleToggleClick}
makeHandleClickShift={makeHandleClickShift}
Expand Down
13 changes: 10 additions & 3 deletions app/containers/Service/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useCallback } from 'react';
import { css } from '@emotion/css';
import { useRecoilState } from 'recoil';
import langState from 'state/langState';

import SelectBox from '../../components/SelectBox/SelectBox';

import langState from 'state/langState';
import { markNavigationIntent } from 'utils/performanceMarks';

const serviceLanguages = [
{
value: 'ru',
Expand All @@ -23,10 +25,15 @@ const serviceLanguages = [
const LanguageSwitcher = () => {
const [langStateValue, setLang] = useRecoilState(langState);
const onChange = useCallback(
(lang) => {
(lang: string) => {
markNavigationIntent({
initiator: 'service-language-control',
sourceLanguage: langStateValue.lang,
targetLanguage: lang,
});
setLang({ ...langStateValue, lang });
},
[JSON.stringify(langStateValue)]
[langStateValue, setLang]
);
return (
<SelectBox
Expand Down
20 changes: 14 additions & 6 deletions app/containers/Service/Service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import CustomPrayerInput from 'components/CustomPrayers/CustomPrayerInput';
import { usePrayerTimer } from 'containers/HabitTracker/usePrayerTimer';
import PostPrayerPrompt from 'containers/HabitTracker/PostPrayerPrompt';
import { TOCProvider } from 'components/TOC/TOCProvider';
import { markNavigationIntent, markPerformance } from 'utils/performanceMarks';

import LanguageSwitcher from './LanguageSwitcher';
import TOCSwitcher from './TOCSwitcher';
Expand All @@ -48,11 +49,15 @@ const ServiceCommitMarker = ({ renderKey }: { renderKey: string }): null => {
}

performance.clearMarks?.('service_complete_commit');
try {
performance.mark('service_complete_commit', { detail: { renderKey } });
} catch {
performance.mark('service_complete_commit');
}
markPerformance('service_complete_commit', { renderKey });
}, [renderKey]);

return null;
};

const ServiceShellMarker = ({ renderKey }: { renderKey: string }): null => {
useEffect(() => {
markPerformance('service_shell_ready', { renderKey });
}, [renderKey]);

return null;
Expand Down Expand Up @@ -154,7 +159,9 @@ const Service = () => {
}

const setNewDate = (dateString) => {
void navigate(`/date/${dateString}/service/${originalServiceId}`, {
const target = `/date/${dateString}/service/${originalServiceId}`;
markNavigationIntent({ initiator: 'service-date-change', target });
void navigate(target, {
state: { backLink: location.state?.backLink },
});
};
Expand Down Expand Up @@ -203,6 +210,7 @@ const Service = () => {
<LangContext.Provider value={effectiveLangState}>
<TOCProvider key={`${date}:${currentServiceId}`}>
<LayoutInner left={left} right={right} paddedContent={false}>
<ServiceShellMarker renderKey={serviceRenderKey} />
{service?.scriptEditor && (
<>
<ScriptEditorToggle serviceId={serviceId} />
Expand Down
3 changes: 1 addition & 2 deletions app/containers/Service/runtimeOptimizations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ test('derives the top-level service module during render and marks only complete
assert.match(serviceSource, /const TextComponent = useMemo\(/);
assert.doesNotMatch(serviceSource, /setTextComponent/);
assert.match(serviceSource, /performance\.clearMarks\?\.\('service_complete_commit'\)/);
assert.match(serviceSource, /performance\.mark\('service_complete_commit', \{ detail: \{ renderKey \} \}\)/);
assert.match(serviceSource, /performance\.mark\('service_complete_commit'\)/);
assert.match(serviceSource, /markPerformance\('service_complete_commit', \{ renderKey \}\)/);
assert.match(
serviceSource,
/<Suspense[\s\S]*?<TextComponent[\s\S]*?<ServiceCommitMarker renderKey=\{serviceRenderKey\} \/>[\s\S]*?<\/Suspense>/
Expand Down
15 changes: 13 additions & 2 deletions app/routeLoaders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
export const loadServiceRoute = async () =>
await import(/* webpackChunkName: "route-service" */ 'containers/Service/Service');
import { markPerformance } from 'utils/performanceMarks';

let serviceRouteLoaded = false;

export const loadServiceRoute = async () => {
const serviceRoute = await import(/* webpackChunkName: "route-service" */ 'containers/Service/Service');
if (!serviceRouteLoaded) {
serviceRouteLoaded = true;
markPerformance('route_chunk_loaded', { route: 'service' });
}

return serviceRoute;
};

export const preloadServiceRoute = () => {
void loadServiceRoute();
Expand Down
Loading