Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 9635fb972e ️(frontend) defer loading the Crisp script until idle
Load the Crisp JavaScript module only once the frontend is idle,
instead of during the initial page load.

Keeps the critical path lighter and prevents Crisp from competing
with the app's own bootstrap for network and CPU on slow devices.
2026-09-10 23:22:48 +02:00
3 changed files with 92 additions and 22 deletions
+1
View File
@@ -13,6 +13,7 @@ and this project adheres to
- 📈(frontend) include LiveKit SIDs in the connection analytics event - 📈(frontend) include LiveKit SIDs in the connection analytics event
- 🔇(backend) silence expected 401 warnings on /me - 🔇(backend) silence expected 401 warnings on /me
- 🔇(backend) silence noisy request summary info logs - 🔇(backend) silence noisy request summary info logs
- ⚡️(frontend) defer loading the Crisp script until idle
### Fixed ### Fixed
@@ -2,23 +2,23 @@ import { RiQuestionLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components' import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe' import { menuRecipe } from '@/primitives/menuRecipe'
import { Crisp } from 'crisp-sdk-web' import {
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport' useIsSupportEnabled,
openSupportChat,
} from '@/features/support/hooks/useSupport'
export const SupportMenuItem = () => { export const SupportMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const isSupportEnabled = useIsSupportEnabled() const isSupportEnabled = useIsSupportEnabled()
if (!isSupportEnabled || !Crisp) { if (!isSupportEnabled) {
return return
} }
return ( return (
<MenuItem <MenuItem
className={menuRecipe({ icon: true, variant: 'dark' }).item} className={menuRecipe({ icon: true, variant: 'dark' }).item}
onAction={() => { onAction={openSupportChat}
Crisp?.chat.open()
}}
> >
<RiQuestionLine size={20} /> <RiQuestionLine size={20} />
{t('support')} {t('support')}
@@ -1,20 +1,45 @@
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { Crisp } from 'crisp-sdk-web'
import { type ApiUser } from '@/features/auth/api/ApiUser' import { type ApiUser } from '@/features/auth/api/ApiUser'
import { useUser } from '@/features/auth/api/useUser' import { useUser } from '@/features/auth/api/useUser'
import { useConfig } from '@/api/useConfig' import { useConfig } from '@/api/useConfig'
type CrispSdk = (typeof import('crisp-sdk-web'))['Crisp']
let crisp: CrispSdk | undefined
let crispPromise: Promise<CrispSdk> | undefined
const loadCrisp = (): Promise<CrispSdk> => {
crispPromise ??= import('crisp-sdk-web')
.then((module) => {
crisp = module.Crisp
return module.Crisp
})
.catch((error) => {
crispPromise = undefined
throw error
})
return crispPromise
}
export const openSupportChat = () => {
if (!crisp?.isCrispInjected()) return
crisp.chat.open()
}
export const initializeSupportSession = (user: ApiUser) => { export const initializeSupportSession = (user: ApiUser) => {
if (!Crisp.isCrispInjected()) return if (!crisp?.isCrispInjected()) return
const { id, email } = user const { id, email } = user
Crisp.setTokenId(`meet-${id}`) crisp.setTokenId(`meet-${id}`)
if (email) Crisp.user.setEmail(email) if (email) crisp.user.setEmail(email)
} }
export const terminateSupportSession = () => { export const terminateSupportSession = () => {
if (!Crisp.isCrispInjected()) return if (!crisp?.isCrispInjected()) return
Crisp.setTokenId()
Crisp.session.reset() crisp.setTokenId()
crisp.session.reset()
} }
export type useSupportProps = { export type useSupportProps = {
@@ -22,26 +47,70 @@ export type useSupportProps = {
isDisabled?: boolean isDisabled?: boolean
} }
// Configure Crisp chat for real-time support across all pages. const IDLE_TIMEOUT_MS = 10_000
const scheduleWhenIdle = (callback: () => void): (() => void) => {
if (typeof window.requestIdleCallback === 'function') {
const handle = window.requestIdleCallback(callback, {
timeout: IDLE_TIMEOUT_MS,
})
return () => window.cancelIdleCallback(handle)
}
const handle = window.setTimeout(callback, 1)
return () => window.clearTimeout(handle)
}
export const useSupport = ({ id, isDisabled }: useSupportProps) => { export const useSupport = ({ id, isDisabled }: useSupportProps) => {
const { user } = useUser() const { user } = useUser()
const [isInjected, setIsInjected] = useState(
() => crisp?.isCrispInjected() ?? false
)
useEffect(() => { useEffect(() => {
if (!id || Crisp.isCrispInjected() || isDisabled) return if (!id || isDisabled) return
Crisp.configure(id)
Crisp.setHideOnMobile(true) if (crisp?.isCrispInjected()) {
setIsInjected(true)
return
}
let cancelled = false
const cancelIdle = scheduleWhenIdle(() => {
void loadCrisp()
.then((sdk) => {
if (cancelled) return
if (!sdk.isCrispInjected()) {
sdk.configure(id)
sdk.setHideOnMobile(true)
}
setIsInjected(true)
})
.catch((error) => {
if (!cancelled) {
console.error('Failed to initialize support chat', error)
}
})
})
return () => {
cancelled = true
cancelIdle()
}
}, [id, isDisabled]) }, [id, isDisabled])
useEffect(() => { useEffect(() => {
if (!user) return if (!user || !isInjected || isDisabled) return
initializeSupportSession(user) initializeSupportSession(user)
}, [user]) }, [user, isInjected, isDisabled])
return null return null
} }
// Some users may block Crisp chat widget with browser ad blockers or anti-tracking plugins // Some users block the chat widget, so check its availability safely.
// So we need to safely check if Crisp is available and not blocked
const isCrispAvailable = () => { const isCrispAvailable = () => {
try { try {
return !!window?.$crisp?.is return !!window?.$crisp?.is