1
0
Fork 0
mirror of https://github.com/overleaf/overleaf.git synced 2025-04-22 22:46:23 +00:00

[ide-react] Improve initial loading behaviour ()

* Defer script loading
* Only mount IdePage once everything has connected

GitOrigin-RevId: 32f16214f26ac6a6d71a9dd332b3c35b8b82deae
This commit is contained in:
Alf Eaton 2023-11-27 11:26:06 +00:00 committed by Copybot
parent 24023dd267
commit 345f51bedb
7 changed files with 85 additions and 99 deletions
services/web
app/views
frontend/js
features/ide-react/components
pages
shared/hooks

View file

@ -72,7 +72,7 @@ html(
body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false) class=(showThinFooter ? 'thin-footer' : undefined))
if(settings.recaptcha && settings.recaptcha.siteKeyV3)
script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3)
script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3, defer=deferScripts)
if (typeof(suppressSkipToContent) == "undefined")
a(class="skip-to-content" href="#main-content") #{translate('skip_to_content')}
@ -84,17 +84,19 @@ html(
block foot-scripts
each file in entrypointScripts(entrypoint)
script(type="text/javascript", nonce=scriptNonce, src=file)
script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts)
if (settings.splitTest.devToolbar.enabled)
each file in entrypointScripts("devToolbar")
script(type="text/javascript", nonce=scriptNonce, src=file)
script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts)
script(type="text/javascript", nonce=scriptNonce).
//- Look for bundle
var cdnBlocked = typeof Frontend === 'undefined'
//- Prevent loops
var noCdnAlreadyInUrl = window.location.href.indexOf("nocdn=true") != -1
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
//- Set query param, server will not set CDN url
window.location.search += "&nocdn=true";
}
window.addEventListener('DOMContentLoaded', function() {
//- Look for bundle
var cdnBlocked = typeof Frontend === 'undefined'
//- Prevent loops
var noCdnAlreadyInUrl = window.location.href.indexOf("nocdn=true") != -1
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
//- Set query param, server will not set CDN url
window.location.search += "&nocdn=true";
}
})

View file

@ -4,6 +4,7 @@ block vars
- var suppressNavbar = true
- var suppressFooter = true
- var suppressSkipToContent = true
- var deferScripts = true
- metadata.robotsNoindexNofollow = true
block entrypointVar
@ -24,5 +25,5 @@ block append meta
block prepend foot-scripts
each file in (useOpenTelemetry ? entrypointScripts("tracing") : [])
script(type="text/javascript", nonce=scriptNonce, src=file)
script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')
script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts)
script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js', defer=deferScripts)

View file

@ -1,24 +1,16 @@
import { FC, useState } from 'react'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import withErrorBoundary from '@/infrastructure/error-boundary'
import IdePage from '@/features/ide-react/components/layout/ide-page'
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
import { Loading } from '@/features/ide-react/components/loading'
import getMeta from '@/utils/meta'
function IdeRoot() {
// Check that we haven't inadvertently loaded Angular
// TODO: Remove this before rolling out this component to any users
if (typeof window.angular !== 'undefined') {
throw new Error('Angular detected. This page must not load Angular.')
}
const loadingText = getMeta('ol-loadingText')
const IdeRoot: FC = () => {
const [loaded, setLoaded] = useState(false)
return (
<ReactContextRoot>
<Loading loadingText={loadingText}>
<IdePage />
</Loading>
{loaded ? <IdePage /> : <Loading setLoaded={setLoaded} />}
</ReactContextRoot>
)
}

View file

@ -1,69 +1,69 @@
import { FC, useEffect, useState } from 'react'
import LoadingBranded from '../../../shared/components/loading-branded'
import i18n from '../../../i18n'
import { useConnectionContext } from '../context/connection-context'
import LoadingBranded from '@/shared/components/loading-branded'
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import getMeta from '@/utils/meta'
import { useConnectionContext } from '../context/connection-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
type LoadStatus = 'initial' | 'rendered' | 'connected' | 'loaded'
type Part = 'initial' | 'render' | 'connection' | 'translations' | 'project'
const loadProgressPercentage: Record<LoadStatus, number> = {
initial: 20,
rendered: 40,
connected: 70,
loaded: 100,
}
const initialParts = new Set<Part>(['initial'])
const totalParts = new Set<Part>([
'initial',
'render',
'connection',
'translations',
'project',
])
export const Loading: FC<{
setLoaded: (value: boolean) => void
}> = ({ setLoaded }) => {
const [loadedParts, setLoadedParts] = useState(initialParts)
const progress = (loadedParts.size / totalParts.size) * 100
useEffect(() => {
setLoaded(progress === 100)
}, [progress, setLoaded])
// Pass in loading text from the server because i18n will not be ready initially
export const Loading: FC<{ loadingText: string }> = ({
loadingText,
children,
}) => {
const [loadStatus, setLoadStatus] = useState<LoadStatus>('initial')
const { connectionState, isConnected } = useConnectionContext()
const loadProgress = loadProgressPercentage[loadStatus]
const editorLoaded = loadStatus === 'loaded'
const i18n = useWaitForI18n()
const { projectJoined } = useIdeReactContext()
const [i18nLoaded, setI18nLoaded] = useState(false)
const [translationLoadError, setTranslationLoadError] = useState(false)
// Advance to 40% once this component is rendered
useEffect(() => {
// Force a reflow now so that the animation from 20% to 40% occurs
// eslint-disable-next-line no-void
void document.body.offsetHeight
setLoadStatus('rendered')
setLoadedParts(value => new Set(value).add('render'))
}, [])
useEffect(() => {
i18n
.then(() => setI18nLoaded(true))
.catch(() => {
setTranslationLoadError(true)
})
}, [])
useEffect(() => {
if (editorLoaded) {
return
}
if (isConnected) {
setLoadStatus(i18nLoaded ? 'loaded' : 'connected')
setLoadedParts(value => new Set(value).add('connection'))
}
}, [i18nLoaded, editorLoaded, setLoadStatus, isConnected])
}, [isConnected])
const translationLoadErrorMessage = translationLoadError
? getMeta('ol-translationLoadErrorMessage')
: ''
useEffect(() => {
if (i18n.isReady) {
setLoadedParts(value => new Set(value).add('translations'))
}
}, [i18n.isReady])
return editorLoaded ? (
<>{children}</>
) : (
useEffect(() => {
if (projectJoined) {
setLoadedParts(value => new Set(value).add('project'))
}
}, [projectJoined])
const error =
connectionState.error ||
(i18n.error ? getMeta('ol-translationLoadErrorMessage') : '')
// Use loading text from the server, because i18n will not be ready initially
const label = getMeta('ol-loadingText')
return (
<div className="loading-screen">
<LoadingBranded
loadProgress={loadProgress}
label={loadingText}
error={connectionState.error || translationLoadErrorMessage}
/>
<LoadingBranded loadProgress={progress} label={label} error={error} />
</div>
)
}

View file

@ -1,21 +0,0 @@
// Configure dynamically loaded assets (via webpack) to be downloaded from CDN
import '../utils/webpack-public-path'
// Set up error reporting, including Sentry
import '../infrastructure/error-reporter'
import ReactDOM from 'react-dom'
import IdeRoot from '../features/ide-react/components/ide-root'
const element = document.getElementById('ide-root')
if (element) {
// Remove loading screen provided by the server and replace it with the same
// screen rendered in React. Could use replaceChildren() instead but browser
// support is relatively recent (arrived in Safari in 2020)
element.textContent = ''
// This will not be valid in React 18, which has a new API. See
// https://github.com/reactwg/react-18/discussions/5
// https://react.dev/blog/2022/03/08/react-18-upgrade-guide#deprecations
ReactDOM.render(<IdeRoot />, element)
}

View file

@ -0,0 +1,6 @@
import '../utils/webpack-public-path' // configure dynamically loaded assets (via webpack) to be downloaded from CDN
import '../infrastructure/error-reporter' // set up error reporting, including Sentry
import ReactDOM from 'react-dom'
import IdeRoot from '@/features/ide-react/components/ide-root'
ReactDOM.render(<IdeRoot />, document.getElementById('ide-root'))

View file

@ -5,15 +5,21 @@ import { useTranslation } from 'react-i18next'
function useWaitForI18n() {
const { ready: isHookReady } = useTranslation()
const [isLocaleDataLoaded, setIsLocaleDataLoaded] = useState(false)
const [error, setError] = useState<Error>()
useEffect(() => {
i18n.then(() => {
setIsLocaleDataLoaded(true)
})
i18n
.then(() => {
setIsLocaleDataLoaded(true)
})
.catch(error => {
setError(error)
})
}, [])
return {
isReady: isHookReady && isLocaleDataLoaded,
error,
}
}