fix(fe2): various performance fixes (#5278)
* fix(fe2): buggy merge policies causing cache read failures * improved CWV reporting * improved preview lcp * more eager load improvements * more eager load improvements * SSR friendly relative time * import fixes
This commit is contained in:
committed by
GitHub
parent
f4382c9664
commit
47b9e9c5bc
@@ -180,12 +180,14 @@ const hasDiscoverableWorkspacesOrJoinRequests = computed(
|
||||
() => discoverableWorkspacesAndJoinRequestsCount.value > 0
|
||||
)
|
||||
|
||||
onActiveWorkspaceResult(({ data }) => {
|
||||
if (data?.activeUser?.activeWorkspace) {
|
||||
$intercom.updateCompany({
|
||||
id: data.activeUser.activeWorkspace.id,
|
||||
name: data.activeUser.activeWorkspace.name
|
||||
})
|
||||
}
|
||||
})
|
||||
if (import.meta.client) {
|
||||
onActiveWorkspaceResult(({ data }) => {
|
||||
if (data?.activeUser?.activeWorkspace) {
|
||||
$intercom.updateCompany({
|
||||
id: data.activeUser.activeWorkspace.id,
|
||||
name: data.activeUser.activeWorkspace.name
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -89,9 +89,11 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
previewUrl: string
|
||||
panoramaOnHover?: boolean
|
||||
eagerLoad?: boolean
|
||||
}>(),
|
||||
{
|
||||
panoramaOnHover: true
|
||||
panoramaOnHover: true,
|
||||
eagerLoad: true
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,7 +111,10 @@ const {
|
||||
isLoadingPanorama,
|
||||
hasDoneFirstLoad,
|
||||
isPanoramaPlaceholder
|
||||
} = usePreviewImageBlob(basePreviewUrl, { enabled: isInViewport })
|
||||
} = usePreviewImageBlob(basePreviewUrl, {
|
||||
enabled: isInViewport,
|
||||
eagerLoad: props.eagerLoad
|
||||
})
|
||||
|
||||
const hovered = ref(false)
|
||||
const panorama = ref(null as Nullable<HTMLDivElement>)
|
||||
|
||||
@@ -171,6 +171,7 @@ const props = defineProps<{
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
const isAutomateModuleEnabled = useIsAutomateModuleEnabled()
|
||||
|
||||
const showActionsMenu = ref(false)
|
||||
|
||||
@@ -74,6 +74,7 @@ const props = defineProps<{
|
||||
const { screenshot } = useCommentScreenshotImage(
|
||||
computed(() => props.thread.screenshot)
|
||||
)
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const isLimited = computed(() => {
|
||||
return !props.thread.rawText
|
||||
|
||||
@@ -57,6 +57,7 @@ const props = defineProps<{
|
||||
const { backgroundImage } = useCommentScreenshotImage(
|
||||
computed(() => props.thread.screenshot)
|
||||
)
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const updatedAt = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -339,6 +339,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const importArea = ref(
|
||||
null as Nullable<{
|
||||
|
||||
@@ -146,6 +146,7 @@ const props = defineProps<{
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const { copy } = useClipboard()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const { getErrorMessage, convertUploadToFailedJob } = useFailedFileImportJobUtils()
|
||||
const {
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { formattedFullDate } from '~/utils/dateFormatter'
|
||||
/**
|
||||
* Separate component so that hydration mismatches only cause this component to re-render, not the entire model card.
|
||||
* Hydration mismatches can happen here when the server resolves the update as X minutes ago, but the client resolves it as X minutes and 1 second ago.
|
||||
*
|
||||
* @deprecated Formatted dates now use a SSR-friendly now() date, so this doesnt need to be used anymore
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
updatedAt: string
|
||||
}>()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const updatedAtFormatted = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -66,7 +66,6 @@ import { TrashIcon } from '@heroicons/vue/24/outline'
|
||||
import { projectEmbedTokensQuery } from '~~/lib/projects/graphql/queries'
|
||||
import type { EmbedTokenItem } from '~~/lib/projects/helpers/types'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { formattedFullDate } from '~/utils/dateFormatter'
|
||||
import { useDeleteEmbedToken } from '~~/lib/projects/composables/tokenManagement'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
@@ -107,6 +106,7 @@ graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
const deleteEmbedToken = useDeleteEmbedToken()
|
||||
|
||||
@@ -143,6 +143,7 @@ const props = defineProps<{
|
||||
|
||||
const router = useRouter()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const isModelUploading = ref(false)
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ const props = defineProps<{
|
||||
workspace: MaybeNullOrUndefined<SettingsSharedProjects_WorkspaceFragment>
|
||||
}>()
|
||||
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
const { activeUser } = useActiveUser()
|
||||
const canCreatePersonal = useCanCreatePersonalProject({
|
||||
activeUser: computed(() => activeUser.value)
|
||||
|
||||
@@ -133,6 +133,7 @@ const props = defineProps<{
|
||||
workspaceSlug: string
|
||||
}>()
|
||||
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
const { result } = useQuery(settingsWorkspacesMembersTableQuery, () => ({
|
||||
slug: props.workspaceSlug
|
||||
}))
|
||||
|
||||
@@ -129,6 +129,7 @@ const props = defineProps<{
|
||||
const search = ref('')
|
||||
const showActionsMenu = ref<Record<string, boolean>>({})
|
||||
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
const cancelInvite = useCancelWorkspaceInvite()
|
||||
const resendInvite = useResendWorkspaceInvite()
|
||||
const { result: searchResult, loading: searchResultLoading } = useQuery(
|
||||
|
||||
@@ -99,6 +99,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { deny } = useWorkspaceJoinRequest()
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
|
||||
const showApproveJoinRequestDialog = ref(false)
|
||||
const requestToApprove =
|
||||
|
||||
@@ -146,6 +146,7 @@ const { activeUser } = useActiveUser()
|
||||
const { result } = useQuery(settingsWorkspacesMembersTableQuery, () => ({
|
||||
slug: props.workspaceSlug
|
||||
}))
|
||||
const { formattedFullDate } = useDateFormatters()
|
||||
|
||||
const selectedAction = ref<Record<string, WorkspaceUserActionTypes>>({})
|
||||
const search = ref('')
|
||||
|
||||
@@ -78,6 +78,7 @@ const { getErrorMessage } = useFailedFileImportJobUtils()
|
||||
|
||||
const { copyReference } = useGenerateErrorReference()
|
||||
const navigateToProject = useNavigateToProject()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const open = computed({
|
||||
get: () => failedJobs.value.length > 0,
|
||||
|
||||
@@ -59,6 +59,7 @@ const {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -119,6 +119,7 @@ const open = (id: string) => {
|
||||
|
||||
const { calculateThreadResourceStatus } = useCommentContext()
|
||||
const itemStatus = computed(() => calculateThreadResourceStatus(props.thread))
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -24,6 +24,7 @@ const props = defineProps<{
|
||||
version: ViewerModelVersionCardItemFragment
|
||||
isNewest: boolean
|
||||
}>()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -141,6 +141,7 @@ const {
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const mp = useMixpanel()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const route = useRoute()
|
||||
const resourceIdString = computed(() => {
|
||||
|
||||
@@ -33,6 +33,7 @@ interface Version {
|
||||
const props = defineProps<{
|
||||
version: Version
|
||||
}>()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return {
|
||||
|
||||
@@ -121,6 +121,7 @@ const {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const showVersions = ref(!!props.initiallyExpanded)
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
@@ -142,6 +142,7 @@ const {
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const copyModelLink = useCopyModelLink()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const isLoaded = computed(() => props.isLoadedVersion)
|
||||
const isLatest = computed(() => props.isLatestVersion)
|
||||
|
||||
@@ -158,6 +158,7 @@ const updateView = useUpdateSavedView()
|
||||
const isLoading = useMutationLoading()
|
||||
const { copyLink, applyView } = useViewerSavedViewsUtils()
|
||||
const eventBus = useEventBus()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const showMenu = ref(false)
|
||||
const menuId = useId()
|
||||
|
||||
+51
-10
@@ -7,9 +7,14 @@ import dayjs from 'dayjs'
|
||||
* customRelativeTime('2023-07-16') - returns "Jul 16" or "Jul 16, 2023" if the year is different from the current year
|
||||
* customRelativeTime(new Date()) - returns "just now"
|
||||
*/
|
||||
const customRelativeTime = (date: ConfigType, capitalize?: boolean): string => {
|
||||
const customRelativeTime = (
|
||||
date: ConfigType,
|
||||
now: ConfigType,
|
||||
capitalize?: boolean
|
||||
): string => {
|
||||
const pastDate = dayjs(date)
|
||||
const now = dayjs()
|
||||
now = dayjs(now)
|
||||
|
||||
const diffInMinutes = now.diff(date, 'minute')
|
||||
const diffInHours = now.diff(date, 'hour')
|
||||
const diffInDays = now.diff(date, 'day')
|
||||
@@ -35,8 +40,8 @@ const customRelativeTime = (date: ConfigType, capitalize?: boolean): string => {
|
||||
* isTimeframe('2023-07-16') - returns false
|
||||
* isTimeframe(new Date()) - returns true or false depending on the current time
|
||||
*/
|
||||
const isTimeframe = (date: ConfigType) => {
|
||||
const unit = customRelativeTime(date)
|
||||
const isTimeframe = (date: ConfigType, now: ConfigType) => {
|
||||
const unit = customRelativeTime(date, now)
|
||||
return (
|
||||
unit.includes('second') ||
|
||||
unit.includes('minute') ||
|
||||
@@ -51,7 +56,7 @@ const isTimeframe = (date: ConfigType) => {
|
||||
* @example
|
||||
* formattedFullDate('2023-12-01') - returns "Dec 12, 2023"
|
||||
*/
|
||||
export const formattedFullDate = (date: ConfigType): string =>
|
||||
const formattedFullDate = (date: ConfigType): string =>
|
||||
dayjs(date).format('MMM D, YYYY, H:mm')
|
||||
|
||||
/**
|
||||
@@ -63,15 +68,51 @@ export const formattedFullDate = (date: ConfigType): string =>
|
||||
* formattedRelativeDate('2023-12-31') - returns "1 day ago"
|
||||
* formattedRelativeDate('2023-12-31', { prefix: true }) - returns "1 day ago"
|
||||
*/
|
||||
export const formattedRelativeDate = (
|
||||
const formattedRelativeDate = (
|
||||
date: ConfigType,
|
||||
now: ConfigType,
|
||||
options?: Partial<{ prefix: boolean; capitalize: boolean }>
|
||||
): string => {
|
||||
if (options?.prefix) {
|
||||
return isTimeframe(date)
|
||||
? customRelativeTime(date, options?.capitalize)
|
||||
: `on ${customRelativeTime(date)}`
|
||||
return isTimeframe(date, now)
|
||||
? customRelativeTime(date, now, options?.capitalize)
|
||||
: `on ${customRelativeTime(date, now)}`
|
||||
} else {
|
||||
return customRelativeTime(date, options?.capitalize)
|
||||
return customRelativeTime(date, now, options?.capitalize)
|
||||
}
|
||||
}
|
||||
|
||||
// Remembering and reusing same now() value in SSR and CSR to avoid hydration mismatches
|
||||
const useNowState = () => useState('now', () => new Date())
|
||||
|
||||
export const useDateFormatters = () => {
|
||||
const state = useNowState()
|
||||
const { $isAppHydrated } = useNuxtApp()
|
||||
|
||||
return {
|
||||
/**
|
||||
* Formats a given date input into a relative time string with optional prefix
|
||||
* @example
|
||||
* Assuming today is January 1st 2024
|
||||
* formattedRelativeDate('2023-12-01') - returns "Dec 12, 2023"
|
||||
* formattedRelativeDate('2023-12-01', { prefix: true }) - returns "on Dec 12, 2023"
|
||||
* formattedRelativeDate('2023-12-31') - returns "1 day ago"
|
||||
* formattedRelativeDate('2023-12-31', { prefix: true }) - returns "1 day ago"
|
||||
*/
|
||||
formattedRelativeDate: (
|
||||
date: ConfigType,
|
||||
options?: Partial<{ prefix: boolean; capitalize: boolean; now: ConfigType }>
|
||||
): string => {
|
||||
// during SSR and hydration use static now, afterwards use a fresh one each time
|
||||
// (unless if specific one fed in)
|
||||
const now = options?.now || ($isAppHydrated.value ? new Date() : state.value)
|
||||
return formattedRelativeDate(date, now, options)
|
||||
},
|
||||
/**
|
||||
* Formats a given date input into a full date string with our default format
|
||||
* @example
|
||||
* formattedFullDate('2023-12-01') - returns "Dec 12, 2023"
|
||||
*/
|
||||
formattedFullDate
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,9 @@ function createCache(): InMemoryCache {
|
||||
ServerConfiguration: {
|
||||
merge: true
|
||||
},
|
||||
WorkspaceSubscription: {
|
||||
merge: true
|
||||
},
|
||||
CommentThreadActivityMessage: {
|
||||
merge: true
|
||||
},
|
||||
@@ -354,7 +357,7 @@ function createCache(): InMemoryCache {
|
||||
Workspace: {
|
||||
fields: {
|
||||
invitedTeam: {
|
||||
merge: (_existing, incoming) => incoming
|
||||
merge: incomingOverwritesExistingMergeFunction
|
||||
},
|
||||
team: {
|
||||
keyArgs: ['limit', 'filter', ['roles', 'search', 'seatType']],
|
||||
|
||||
@@ -113,6 +113,7 @@ export function buildAbstractCollectionMergeFunction<T extends string>(
|
||||
}
|
||||
|
||||
return {
|
||||
...(existing || {}),
|
||||
...(incoming || {}),
|
||||
...(finalItems ? { items: finalItems } : {}),
|
||||
__typename: incoming?.__typename || existing?.__typename || typeName
|
||||
@@ -132,7 +133,8 @@ export const incomingOverwritesExistingMergeFunction: FieldMergeFunction = (
|
||||
incoming: unknown
|
||||
) => incoming
|
||||
|
||||
export const mergeAsObjectsFunction: FieldMergeFunction = (existing, incoming) => ({
|
||||
...existing,
|
||||
...incoming
|
||||
})
|
||||
export const mergeAsObjectsFunction: FieldMergeFunction = (
|
||||
existing,
|
||||
incoming,
|
||||
{ mergeObjects }
|
||||
) => mergeObjects(existing, incoming) // built in apollo logic is more foolproof
|
||||
|
||||
@@ -5,6 +5,20 @@ import { useSubscription } from '@vue/apollo-composable'
|
||||
import { useLock } from '~~/lib/common/composables/singleton'
|
||||
import PreviewPlaceholder from '~~/assets/images/preview_placeholder.png'
|
||||
import { isValidBase64Image } from '@speckle/shared/images/base64'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
/**
|
||||
* Eager loading previews ensures a better LCP score, but also hits the preview endpoint more often.
|
||||
* Since we don't know the viewport size in SSR, we can just set a limit of how many previews to eager
|
||||
* load after which they should use spinners.
|
||||
*
|
||||
* Theoretically even 1 eager load will fix the LCP issue, but it will look odd if all of the other ones
|
||||
* show up as spinners. So ideally just set enough for 1 page load.
|
||||
*
|
||||
* Assuming a large screen w/ the busiest preview page (project page w/ model grid), there would be
|
||||
* about 20 previews
|
||||
*/
|
||||
const PREVIEWS_EAGER_LOAD_COUNT = 20
|
||||
|
||||
const previewUrlProjectIdRegexp = /\/preview\/([\w\d]+)\//i
|
||||
const previewUrlCommitIdRegexp = /\/commits\/([\w\d]+)/i
|
||||
@@ -12,6 +26,14 @@ const previewUrlObjectIdRegexp = /\/commits\/([\w\d]+)/i
|
||||
|
||||
class AngleNotFoundError extends Error {}
|
||||
|
||||
const usePreviewsState = () =>
|
||||
useState('preview_images_load_state', () => ({
|
||||
/**
|
||||
* How many previews have already been eager loaded
|
||||
*/
|
||||
eagerLoadedKeys: new Set<string>()
|
||||
}))
|
||||
|
||||
/**
|
||||
* Get authenticated preview image URL and subscribes to preview image generation events so that the preview image URL
|
||||
* is updated whenever generation finishes
|
||||
@@ -23,18 +45,47 @@ export function usePreviewImageBlob(
|
||||
* Allows disabling the mechanism conditionally (e.g. if image not in viewport)
|
||||
*/
|
||||
enabled: MaybeRef<boolean>
|
||||
/**
|
||||
* Whether to avoid spinners and just embed the image immediately. Means we will likely
|
||||
* load a lot more than needed, but also get a way better LCP score (no spinners).
|
||||
*
|
||||
* If enabled, overrides `enabled` to be true.
|
||||
*/
|
||||
eagerLoad: boolean
|
||||
}>
|
||||
) {
|
||||
// Checking if we're allowed to eager load
|
||||
const { $isAppHydrated } = useNuxtApp()
|
||||
const state = usePreviewsState()
|
||||
const eagerLoad =
|
||||
options?.eagerLoad &&
|
||||
!$isAppHydrated.value &&
|
||||
state.value.eagerLoadedKeys.size < PREVIEWS_EAGER_LOAD_COUNT
|
||||
const eagerLoadKey = nanoid()
|
||||
|
||||
if (eagerLoad) {
|
||||
state.value.eagerLoadedKeys.add(eagerLoadKey)
|
||||
}
|
||||
|
||||
// Continue on with normal operation
|
||||
const { enabled = ref(true) } = options || {}
|
||||
const logger = useLogger()
|
||||
const lazyLoad = !eagerLoad
|
||||
|
||||
const url = ref(PreviewPlaceholder as Nullable<string>)
|
||||
const hasDoneFirstLoad = ref(false)
|
||||
const url = ref<Nullable<string>>(
|
||||
(eagerLoad ? unref(previewUrl) : PreviewPlaceholder) || null
|
||||
)
|
||||
const hasDoneFirstLoad = ref(eagerLoad)
|
||||
const panoramaUrl = ref(null as Nullable<string>)
|
||||
const isLoadingPanorama = ref(false)
|
||||
const shouldLoadPanorama = ref(false)
|
||||
const basePanoramaUrl = computed(() => unref(previewUrl) + '/all')
|
||||
const isEnabled = computed(() => (import.meta.server ? true : unref(enabled)))
|
||||
const isEnabled = computed(() => {
|
||||
if (import.meta.server) return true // always true on server
|
||||
if (eagerLoad) return true // always true if eagerLoad
|
||||
|
||||
return unref(enabled)
|
||||
})
|
||||
const cacheBust = ref(0)
|
||||
const isPanoramaPlaceholder = ref(false)
|
||||
|
||||
@@ -137,7 +188,7 @@ export function usePreviewImageBlob(
|
||||
const blobUrl = blobUrlConfig.toString()
|
||||
|
||||
// Load img in browser first, before we set the url
|
||||
if (import.meta.client) {
|
||||
if (import.meta.client && lazyLoad) {
|
||||
const img = new Image()
|
||||
img.src = blobUrl
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -200,10 +251,12 @@ export function usePreviewImageBlob(
|
||||
}
|
||||
}
|
||||
|
||||
const regeneratePreviews = (basePreviewUrl?: string) => {
|
||||
const regeneratePreviews = async (basePreviewUrl?: string) => {
|
||||
cacheBust.value++
|
||||
processBasePreviewUrl(basePreviewUrl || unref(previewUrl))
|
||||
if (shouldLoadPanorama.value) processPanoramaPreviewUrl()
|
||||
await Promise.all([
|
||||
processBasePreviewUrl(basePreviewUrl || unref(previewUrl)),
|
||||
...(shouldLoadPanorama.value ? [processPanoramaPreviewUrl()] : [])
|
||||
])
|
||||
}
|
||||
|
||||
watch(shouldLoadPanorama, (newVal) => {
|
||||
@@ -213,7 +266,7 @@ export function usePreviewImageBlob(
|
||||
watch(
|
||||
() => unref(previewUrl),
|
||||
(newVal) => {
|
||||
regeneratePreviews(newVal || undefined)
|
||||
void regeneratePreviews(newVal || undefined)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -223,7 +276,7 @@ export function usePreviewImageBlob(
|
||||
(newVal) => {
|
||||
if (!newVal) return
|
||||
|
||||
regeneratePreviews()
|
||||
void regeneratePreviews()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"@tiptap/vue-3": "2.10.3",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"@vue/apollo-composable": "^4.2.2",
|
||||
"@vue/apollo-ssr": "4.0.0",
|
||||
"@vue/apollo-ssr": "4.2.2",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"apollo-upload-client": "^18.0.1",
|
||||
"dayjs": "^1.11.7",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Global ref for checking if app has hydrated
|
||||
*/
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const isAppHydrated = ref(false)
|
||||
|
||||
if (import.meta.client) {
|
||||
nuxtApp.hook('app:suspense:resolve', () => {
|
||||
isAppHydrated.value = true
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
isAppHydrated
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -126,18 +126,27 @@ async function initRumServer(app: PluginNuxtApp) {
|
||||
app.hook('app:rendered', (context) => {
|
||||
context.ssrContext!.head.push({
|
||||
script: [
|
||||
{
|
||||
innerHTML: `
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const last = entries[entries.length - 1];
|
||||
// last.startTime is your current LCP candidate time
|
||||
console.log('LCP candidate:', last.startTime, last);
|
||||
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
||||
`,
|
||||
type: 'module'
|
||||
},
|
||||
{
|
||||
innerHTML: `
|
||||
import {
|
||||
onCLS,
|
||||
onFID,
|
||||
onLCP,
|
||||
onINP,
|
||||
onTTFB
|
||||
} from 'https://cdn.jsdelivr.net/npm/web-vitals@3/dist/web-vitals.attribution.js?module';
|
||||
} from 'https://unpkg.com/web-vitals@5/dist/web-vitals.attribution.js?module';
|
||||
|
||||
onCLS(console.log);
|
||||
onFID(console.log);
|
||||
onLCP(console.log);
|
||||
onINP(console.log);
|
||||
onTTFB(console.log);
|
||||
|
||||
@@ -15981,7 +15981,7 @@ __metadata:
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.12.0"
|
||||
"@typescript-eslint/parser": "npm:^7.12.0"
|
||||
"@vue/apollo-composable": "npm:^4.2.2"
|
||||
"@vue/apollo-ssr": "npm:4.0.0"
|
||||
"@vue/apollo-ssr": "npm:4.2.2"
|
||||
"@vueuse/core": "npm:^10.9.0"
|
||||
apollo-upload-client: "npm:^18.0.1"
|
||||
autoprefixer: "npm:^10.4.14"
|
||||
@@ -20646,12 +20646,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/apollo-ssr@npm:4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@vue/apollo-ssr@npm:4.0.0"
|
||||
"@vue/apollo-ssr@npm:4.2.2":
|
||||
version: 4.2.2
|
||||
resolution: "@vue/apollo-ssr@npm:4.2.2"
|
||||
dependencies:
|
||||
serialize-javascript: "npm:^6.0.1"
|
||||
checksum: 10/e9a82bc27ad5ea464f0b9fb30edc7bae03b399bd3c7c8192652a00df5a0f77bf306a911ccf00e060aa72b7bd395eb5cc5abb35c60132f64387ff5ed5526e47e2
|
||||
checksum: 10/49a8d019b590f90d954db3fcf3e7501deb978737a1ef9bcca4f05e7509242948c007d1e58f9137a5d89eae643f084cae2fe465a23a7bc3caddd3fa20b8d35c6b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user