refactor: lazy load large modules (#33888)

This commit is contained in:
Stephen Zhou 2026-03-24 15:29:42 +08:00 committed by GitHub
parent 1674f8c2fb
commit 0c3d11f920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 104 additions and 106 deletions

View File

@ -8,12 +8,14 @@ import AppListContext from '@/context/app-list-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useImportDSL } from '@/hooks/use-import-dsl'
import { DSLImportMode } from '@/models/app'
import dynamic from '@/next/dynamic'
import { fetchAppDetail } from '@/service/explore'
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
import CreateAppModal from '../explore/create-app-modal'
import TryApp from '../explore/try-app'
import List from './list'
const DSLConfirmModal = dynamic(() => import('../app/create-from-dsl-modal/dsl-confirm-modal'), { ssr: false })
const CreateAppModal = dynamic(() => import('../explore/create-app-modal'), { ssr: false })
const TryApp = dynamic(() => import('../explore/try-app'), { ssr: false })
const Apps = () => {
const { t } = useTranslation()

View File

@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
import { parseAsStringLiteral, useQueryState } from 'nuqs'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import TabSliderNew from '@/app/components/base/tab-slider-new'
import TagFilter from '@/app/components/base/tag-management/filter'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
@ -205,12 +205,12 @@ const List: FC<Props> = ({
options={options}
/>
<div className="flex items-center gap-2">
<CheckboxWithLabel
className="mr-2"
label={t('showMyCreatedAppsOnly', { ns: 'app' })}
isChecked={isCreatedByMe}
onChange={handleCreatedByMeChange}
/>
<label className="mr-2 flex h-7 items-center space-x-2">
<Checkbox checked={isCreatedByMe} onCheck={handleCreatedByMeChange} />
<div className="text-sm font-normal text-text-secondary">
{t('showMyCreatedAppsOnly', { ns: 'app' })}
</div>
</label>
<TagFilter type="app" value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon

View File

@ -5,17 +5,12 @@ import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
import * as React from 'react'
import { useEffect } from 'react'
import { AMPLITUDE_API_KEY, IS_CLOUD_EDITION } from '@/config'
import { AMPLITUDE_API_KEY, isAmplitudeEnabled } from '@/config'
export type IAmplitudeProps = {
sessionReplaySampleRate?: number
}
// Check if Amplitude should be enabled
export const isAmplitudeEnabled = () => {
return IS_CLOUD_EDITION && !!AMPLITUDE_API_KEY
}
// Map URL pathname to English page name for consistent Amplitude tracking
const getEnglishPageName = (pathname: string): string => {
// Remove leading slash and get the first segment
@ -59,7 +54,7 @@ const AmplitudeProvider: FC<IAmplitudeProps> = ({
}) => {
useEffect(() => {
// Only enable in Saas edition with valid API key
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
// Initialize Amplitude

View File

@ -2,14 +2,24 @@ import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
import { render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AmplitudeProvider, { isAmplitudeEnabled } from '../AmplitudeProvider'
import AmplitudeProvider from '../AmplitudeProvider'
const mockConfig = vi.hoisted(() => ({
AMPLITUDE_API_KEY: 'test-api-key',
IS_CLOUD_EDITION: true,
}))
vi.mock('@/config', () => mockConfig)
vi.mock('@/config', () => ({
get AMPLITUDE_API_KEY() {
return mockConfig.AMPLITUDE_API_KEY
},
get IS_CLOUD_EDITION() {
return mockConfig.IS_CLOUD_EDITION
},
get isAmplitudeEnabled() {
return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY
},
}))
vi.mock('@amplitude/analytics-browser', () => ({
init: vi.fn(),
@ -27,22 +37,6 @@ describe('AmplitudeProvider', () => {
mockConfig.IS_CLOUD_EDITION = true
})
describe('isAmplitudeEnabled', () => {
it('returns true when cloud edition and api key present', () => {
expect(isAmplitudeEnabled()).toBe(true)
})
it('returns false when cloud edition but no api key', () => {
mockConfig.AMPLITUDE_API_KEY = ''
expect(isAmplitudeEnabled()).toBe(false)
})
it('returns false when not cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
expect(isAmplitudeEnabled()).toBe(false)
})
})
describe('Component', () => {
it('initializes amplitude when enabled', () => {
render(<AmplitudeProvider sessionReplaySampleRate={0.8} />)

View File

@ -1,32 +0,0 @@
import { describe, expect, it } from 'vitest'
import AmplitudeProvider, { isAmplitudeEnabled } from '../AmplitudeProvider'
import indexDefault, {
isAmplitudeEnabled as indexIsAmplitudeEnabled,
resetUser,
setUserId,
setUserProperties,
trackEvent,
} from '../index'
import {
resetUser as utilsResetUser,
setUserId as utilsSetUserId,
setUserProperties as utilsSetUserProperties,
trackEvent as utilsTrackEvent,
} from '../utils'
describe('Amplitude index exports', () => {
it('exports AmplitudeProvider as default', () => {
expect(indexDefault).toBe(AmplitudeProvider)
})
it('exports isAmplitudeEnabled', () => {
expect(indexIsAmplitudeEnabled).toBe(isAmplitudeEnabled)
})
it('exports utils', () => {
expect(resetUser).toBe(utilsResetUser)
expect(setUserId).toBe(utilsSetUserId)
expect(setUserProperties).toBe(utilsSetUserProperties)
expect(trackEvent).toBe(utilsTrackEvent)
})
})

View File

@ -20,8 +20,10 @@ const MockIdentify = vi.hoisted(() =>
},
)
vi.mock('../AmplitudeProvider', () => ({
isAmplitudeEnabled: () => mockState.enabled,
vi.mock('@/config', () => ({
get isAmplitudeEnabled() {
return mockState.enabled
},
}))
vi.mock('@amplitude/analytics-browser', () => ({

View File

@ -1,2 +1,2 @@
export { default, isAmplitudeEnabled } from './AmplitudeProvider'
export { default } from './lazy-amplitude-provider'
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'

View File

@ -0,0 +1,11 @@
'use client'
import type { FC } from 'react'
import type { IAmplitudeProps } from './AmplitudeProvider'
import dynamic from '@/next/dynamic'
const AmplitudeProvider = dynamic(() => import('./AmplitudeProvider'), { ssr: false })
const LazyAmplitudeProvider: FC<IAmplitudeProps> = props => <AmplitudeProvider {...props} />
export default LazyAmplitudeProvider

View File

@ -1,5 +1,5 @@
import * as amplitude from '@amplitude/analytics-browser'
import { isAmplitudeEnabled } from './AmplitudeProvider'
import { isAmplitudeEnabled } from '@/config'
/**
* Track custom event
@ -7,7 +7,7 @@ import { isAmplitudeEnabled } from './AmplitudeProvider'
* @param eventProperties Event properties (optional)
*/
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.track(eventName, eventProperties)
}
@ -17,7 +17,7 @@ export const trackEvent = (eventName: string, eventProperties?: Record<string, a
* @param userId User ID
*/
export const setUserId = (userId: string) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.setUserId(userId)
}
@ -27,7 +27,7 @@ export const setUserId = (userId: string) => {
* @param properties User properties
*/
export const setUserProperties = (properties: Record<string, any>) => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
const identifyEvent = new amplitude.Identify()
Object.entries(properties).forEach(([key, value]) => {
@ -40,7 +40,7 @@ export const setUserProperties = (properties: Record<string, any>) => {
* Reset user (e.g., when user logs out)
*/
export const resetUser = () => {
if (!isAmplitudeEnabled())
if (!isAmplitudeEnabled)
return
amplitude.reset()
}

View File

@ -0,0 +1,13 @@
'use client'
import { IS_DEV } from '@/config'
import dynamic from '@/next/dynamic'
const Agentation = dynamic(() => import('agentation').then(module => module.Agentation), { ssr: false })
export function AgentationLoader() {
if (!IS_DEV)
return null
return <Agentation />
}

View File

@ -69,6 +69,7 @@ vi.mock('@/context/i18n', () => ({
const { mockConfig, mockEnv } = vi.hoisted(() => ({
mockConfig: {
IS_CLOUD_EDITION: false,
AMPLITUDE_API_KEY: '',
ZENDESK_WIDGET_KEY: '',
SUPPORT_EMAIL_ADDRESS: '',
},
@ -80,6 +81,8 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
get AMPLITUDE_API_KEY() { return mockConfig.AMPLITUDE_API_KEY },
get isAmplitudeEnabled() { return mockConfig.IS_CLOUD_EDITION && !!mockConfig.AMPLITUDE_API_KEY },
get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS },
IS_DEV: false,

View File

@ -9,16 +9,18 @@ import { flatten } from 'es-toolkit/compat'
import { produce } from 'immer'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
import CreateAppModal from '@/app/components/app/create-app-modal'
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import dynamic from '@/next/dynamic'
import { useParams } from '@/next/navigation'
import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app'
import Nav from '../nav'
const CreateAppTemplateDialog = dynamic(() => import('@/app/components/app/create-app-dialog'), { ssr: false })
const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { ssr: false })
const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { ssr: false })
const AppNav = () => {
const { t } = useTranslation()
const { appId } = useParams()

View File

@ -0,0 +1,16 @@
'use client'
import { IS_DEV } from '@/config'
import { env } from '@/env'
import dynamic from '@/next/dynamic'
const SentryInitializer = dynamic(() => import('./sentry-initializer'), { ssr: false })
const LazySentryInitializer = () => {
if (IS_DEV || !env.NEXT_PUBLIC_SENTRY_DSN)
return null
return <SentryInitializer />
}
export default LazySentryInitializer

View File

@ -2,13 +2,10 @@
import * as Sentry from '@sentry/react'
import { useEffect } from 'react'
import { IS_DEV } from '@/config'
import { env } from '@/env'
const SentryInitializer = ({
children,
}: { children: React.ReactElement }) => {
const SentryInitializer = () => {
useEffect(() => {
const SENTRY_DSN = env.NEXT_PUBLIC_SENTRY_DSN
if (!IS_DEV && SENTRY_DSN) {
@ -24,7 +21,7 @@ const SentryInitializer = ({
})
}
}, [])
return children
return null
}
export default SentryInitializer

View File

@ -1,9 +1,7 @@
import type { Viewport } from '@/next'
import { Agentation } from 'agentation'
import { Provider as JotaiProvider } from 'jotai/react'
import { ThemeProvider } from 'next-themes'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { IS_DEV } from '@/config'
import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getDatasetMap } from '@/env'
@ -12,9 +10,10 @@ import { ToastProvider } from './components/base/toast'
import { ToastHost } from './components/base/ui/toast'
import { TooltipProvider } from './components/base/ui/tooltip'
import BrowserInitializer from './components/browser-initializer'
import { AgentationLoader } from './components/devtools/agentation-loader'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
import LazySentryInitializer from './components/lazy-sentry-initializer'
import { I18nServerProvider } from './components/provider/i18n-server'
import SentryInitializer from './components/sentry-initializer'
import RoutePrefixHandle from './routePrefixHandle'
import './styles/globals.css'
import './styles/markdown.scss'
@ -57,6 +56,7 @@ const LocaleLayout = async ({
className="h-full select-auto"
{...datasetMap}
>
<LazySentryInitializer />
<div className="isolate h-full">
<JotaiProvider>
<ThemeProvider
@ -68,26 +68,24 @@ const LocaleLayout = async ({
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastHost timeout={5000} limit={3} />
<ToastProvider>
<GlobalPublicStoreProvider>
<TooltipProvider delay={300} closeDelay={200}>
{children}
</TooltipProvider>
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastHost timeout={5000} limit={3} />
<ToastProvider>
<GlobalPublicStoreProvider>
<TooltipProvider delay={300} closeDelay={200}>
{children}
</TooltipProvider>
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</TanstackQueryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
{IS_DEV && <Agentation />}
<AgentationLoader />
</div>
</body>
</html>

View File

@ -42,6 +42,8 @@ export const AMPLITUDE_API_KEY = getStringConfig(
'',
)
export const isAmplitudeEnabled = IS_CLOUD_EDITION && !!AMPLITUDE_API_KEY
export const IS_DEV = process.env.NODE_ENV === 'development'
export const IS_PROD = process.env.NODE_ENV === 'production'

View File

@ -1501,11 +1501,6 @@
"count": 2
}
},
"app/components/base/amplitude/AmplitudeProvider.tsx": {
"react-refresh/only-export-components": {
"count": 1
}
},
"app/components/base/amplitude/utils.ts": {
"ts/no-explicit-any": {
"count": 2