From c1d30f69238b7e13450550e18ab436d574588670 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 26 May 2026 19:14:34 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20load=20PostHog?= =?UTF-8?q?=20dynamically?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit posthog-js was bundled in the main chunk. Loading it dynamically saves 200ko on the initial load and around 4s of loading time on slow 4G connections. PostHog, and analytics in general, should not block the initial rendering. The risk is that some feature-flagged features may not be instantaneously available to the user, but for most users on high-speed connections this will be imperceptible. Other places in the code rely on a direct import from posthog, I only verified the delay import of posthog for the home page, and did not for the /room route. I'm pretty sure there is room for improvements on other parts of the app. --- .../features/analytics/hooks/useAnalytics.ts | 47 +++++++++++++------ .../src/features/auth/api/useUser.tsx | 25 +--------- .../src/features/auth/utils/logout.ts | 13 +++++ .../src/features/auth/utils/logoutUrl.ts | 5 -- .../settings/components/SettingsDialog.tsx | 3 +- .../settings/components/tabs/AccountTab.tsx | 3 +- .../src/features/support/hooks/useSupport.tsx | 10 +++- src/frontend/src/layout/Header.tsx | 3 +- 8 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 src/frontend/src/features/auth/utils/logout.ts delete mode 100644 src/frontend/src/features/auth/utils/logoutUrl.ts diff --git a/src/frontend/src/features/analytics/hooks/useAnalytics.ts b/src/frontend/src/features/analytics/hooks/useAnalytics.ts index 7149c54d..36ac5e6e 100644 --- a/src/frontend/src/features/analytics/hooks/useAnalytics.ts +++ b/src/frontend/src/features/analytics/hooks/useAnalytics.ts @@ -1,17 +1,28 @@ import { useEffect } from 'react' import { useLocation } from 'wouter' -import posthog from 'posthog-js' -import { ApiUser } from '@/features/auth/api/ApiUser' +import { type PostHog } from 'posthog-js' +import { type ApiUser } from '@/features/auth/api/ApiUser' +import { useUser } from '@/features/auth/api/useUser' -export const startAnalyticsSession = (data: ApiUser) => { - if (posthog._isIdentified()) return - const { id, email } = data - posthog.identify(id, { email }) +let posthog: PostHog | null = null + +const getPosthog = async () => { + if (!posthog) posthog = (await import('posthog-js')).default + return posthog } -export const terminateAnalyticsSession = () => { - if (!posthog._isIdentified()) return - posthog.reset() +export const startAnalyticsSession = (data: ApiUser) => { + getPosthog().then((ph) => { + if (ph._isIdentified()) return + const { id, email } = data + ph.identify(id, { email }) + }) +} + +export const terminateAnalyticsSession = async () => { + const ph = await getPosthog() + if (!ph._isIdentified()) return + ph.reset() } export type useAnalyticsProps = { @@ -22,18 +33,26 @@ export type useAnalyticsProps = { export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => { const [location] = useLocation() + const { user } = useUser() + useEffect(() => { if (!id || !host || isDisabled) return - if (posthog.__loaded) return - posthog.init(id, { - api_host: host, - person_profiles: 'always', + getPosthog().then((ph) => { + if (ph.__loaded) return + ph.init(id, { api_host: host, person_profiles: 'always' }) }) }, [id, host, isDisabled]) + useEffect(() => { + if (!user) return + startAnalyticsSession(user) + }, [user]) + // From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context. useEffect(() => { - posthog.capture('$pageview') + getPosthog().then((ph) => { + ph.capture('$pageview') + }) }, [location]) return null diff --git a/src/frontend/src/features/auth/api/useUser.tsx b/src/frontend/src/features/auth/api/useUser.tsx index f435cc3d..6236fb0b 100644 --- a/src/frontend/src/features/auth/api/useUser.tsx +++ b/src/frontend/src/features/auth/api/useUser.tsx @@ -2,16 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { keys } from '@/api/queryKeys' import { fetchUser } from './fetchUser' import { type ApiUser } from './ApiUser' -import { useEffect, useMemo } from 'react' -import { - startAnalyticsSession, - terminateAnalyticsSession, -} from '@/features/analytics/hooks/useAnalytics' -import { - initializeSupportSession, - terminateSupportSession, -} from '@/features/support/hooks/useSupport' -import { logoutUrl } from '../utils/logoutUrl' +import { useMemo } from 'react' import { useConfig } from '@/api/useConfig' /** @@ -45,19 +36,6 @@ export const useUser = ( enabled: !isConfigLoading, }) - useEffect(() => { - if (query?.data) { - startAnalyticsSession(query.data) - initializeSupportSession(query.data) - } - }, [query.data]) - - const logout = () => { - terminateAnalyticsSession() - terminateSupportSession() - window.location.href = logoutUrl() - } - const isLoggedIn = query.status === 'success' ? query.data !== false : undefined const isLoggedOut = isLoggedIn === false @@ -67,6 +45,5 @@ export const useUser = ( user: isLoggedOut ? undefined : (query.data as ApiUser | undefined), isLoggedIn, isLoading: query.isLoading, - logout, } } diff --git a/src/frontend/src/features/auth/utils/logout.ts b/src/frontend/src/features/auth/utils/logout.ts new file mode 100644 index 00000000..50783019 --- /dev/null +++ b/src/frontend/src/features/auth/utils/logout.ts @@ -0,0 +1,13 @@ +import { apiUrl } from '@/api/apiUrl' +import { terminateSupportSession } from '@/features/support/hooks/useSupport' +import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics' + +const logoutUrl = () => { + return apiUrl('/logout') +} + +export const logout = async () => { + await terminateAnalyticsSession() + terminateSupportSession() + window.location.href = logoutUrl() +} diff --git a/src/frontend/src/features/auth/utils/logoutUrl.ts b/src/frontend/src/features/auth/utils/logoutUrl.ts deleted file mode 100644 index 6fa09fb7..00000000 --- a/src/frontend/src/features/auth/utils/logoutUrl.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { apiUrl } from '@/api/apiUrl' - -export const logoutUrl = () => { - return apiUrl('/logout') -} diff --git a/src/frontend/src/features/settings/components/SettingsDialog.tsx b/src/frontend/src/features/settings/components/SettingsDialog.tsx index 804bf84b..c329ba64 100644 --- a/src/frontend/src/features/settings/components/SettingsDialog.tsx +++ b/src/frontend/src/features/settings/components/SettingsDialog.tsx @@ -3,12 +3,13 @@ import { useLanguageLabels } from '@/i18n/useLanguageLabels' import { A, Badge, Dialog, type DialogProps, Field, H, P } from '@/primitives' import { useUser } from '@/features/auth/api/useUser' import { LoginButton } from '@/components/LoginButton' +import { logout } from '@/features/auth/utils/logout' export type SettingsDialogProps = Pick export const SettingsDialog = (props: SettingsDialogProps) => { const { t, i18n } = useTranslation('settings') - const { user, isLoggedIn, logout } = useUser() + const { user, isLoggedIn } = useUser() const { languagesList, currentLanguage } = useLanguageLabels() const userDisplay = user?.full_name && user?.email diff --git a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx index 1109c86d..1fcf491d 100644 --- a/src/frontend/src/features/settings/components/tabs/AccountTab.tsx +++ b/src/frontend/src/features/settings/components/tabs/AccountTab.tsx @@ -9,6 +9,7 @@ import { useState } from 'react' import { LoginButton } from '@/components/LoginButton' import { useRenameParticipant } from '@/features/rooms/api/renameParticipant' import { saveUsername } from '@/stores/userChoices' +import { logout } from '@/features/auth/utils/logout' export type AccountTabProps = Pick & Pick @@ -16,7 +17,7 @@ export type AccountTabProps = Pick & export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => { const { t } = useTranslation('settings') const room = useRoomContext() - const { user, isLoggedIn, logout } = useUser() + const { user, isLoggedIn } = useUser() const { renameParticipant } = useRenameParticipant() diff --git a/src/frontend/src/features/support/hooks/useSupport.tsx b/src/frontend/src/features/support/hooks/useSupport.tsx index 211d6767..a4b3679e 100644 --- a/src/frontend/src/features/support/hooks/useSupport.tsx +++ b/src/frontend/src/features/support/hooks/useSupport.tsx @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { Crisp } from 'crisp-sdk-web' -import { ApiUser } from '@/features/auth/api/ApiUser' +import { type ApiUser } from '@/features/auth/api/ApiUser' +import { useUser } from '@/features/auth/api/useUser' import { useConfig } from '@/api/useConfig' export const initializeSupportSession = (user: ApiUser) => { @@ -23,12 +24,19 @@ export type useSupportProps = { // Configure Crisp chat for real-time support across all pages. export const useSupport = ({ id, isDisabled }: useSupportProps) => { + const { user } = useUser() + useEffect(() => { if (!id || Crisp.isCrispInjected() || isDisabled) return Crisp.configure(id) Crisp.setHideOnMobile(true) }, [id, isDisabled]) + useEffect(() => { + if (!user) return + initializeSupportSession(user) + }, [user]) + return null } diff --git a/src/frontend/src/layout/Header.tsx b/src/frontend/src/layout/Header.tsx index 99fbe963..8df8bd6d 100644 --- a/src/frontend/src/layout/Header.tsx +++ b/src/frontend/src/layout/Header.tsx @@ -13,6 +13,7 @@ import { LoginButton } from '@/components/LoginButton' import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip' import { useLoginHint } from '@/hooks/useLoginHint' +import { logout } from '@/features/auth/utils/logout' const Logo = () => ( { const isAccessibility = useMatchesRoute('accessibility') const isTermsOfService = useMatchesRoute('termsOfService') const isRoom = useMatchesRoute('room') - const { user, isLoggedIn, logout } = useUser() + const { user, isLoggedIn } = useUser() const userLabel = user?.full_name || user?.email const loggedInTooltip = t('loggedInUserTooltip') const loggedInAriaLabel = userLabel