fix: core web vitals on mobile IN-1001#1854
Conversation
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR targets Core Web Vitals regressions (CLS/INP) on mobile by stabilizing SSR/hydration output, reserving layout during loading, batching scroll-driven reactive updates, and deferring third-party UI effects.
Changes:
- Fix SSR header rendering on collection details by making
currentCollectionreactive via computed state and updating the TanStack query cache after edits. - Reduce layout shifts in collection/project headers and the global menu by stabilizing logo/login sizing and improving skeleton/layout reservation.
- Improve INP by making scroll listeners passive and batching
scrollTopupdates withrequestAnimationFrame; defer Intercom anonymous boot to post-load idle; add GitHub origin preconnects.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| frontend/app/components/modules/collection/views/collection-details.vue | Uses computed collection state and updates TanStack cache on edit to avoid SSR empty header. |
| frontend/app/components/modules/collection/components/details/header.vue | Improves layout reservation with skeletons and image dimension hints to reduce CLS. |
| frontend/app/components/modules/project/components/shared/header.vue | Removes page-width-dependent logo sizing to prevent SSR/client mismatch reflow. |
| frontend/app/components/shared/layout/menu.vue | Wraps client-only login in fixed-width container to stabilize header layout pre-hydration. |
| frontend/app/components/shared/utils/scroll.ts | Adds passive scroll listener and rAF batching to reduce per-frame reactive work. |
| frontend/app/plugins/intercom.ts | Defers anonymous Intercom boot until after load + idle to avoid CLS window impact. |
| frontend/setup/head.ts | Adds preconnects for GitHub origins used by hero/logo assets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div | ||
| v-if="props.collection?.logoUrl" | ||
| class="shrink-0" | ||
| v-if="loading || props.collection?.logoUrl" | ||
| class="shrink-0 flex items-center justify-start" | ||
| :class="scrollTop > 50 ? 'h-8 md:h-10' : 'h-12 md:h-30'" | ||
| > |
There was a problem hiding this comment.
logoUrl is optional/nullable (see Collection type), but the logo container now renders during loading and disappears when loading completes for collections without a logo. That will shift the title left/right at the end of loading and can introduce CLS for logo-less collections. Consider keeping a stable-width placeholder even when logoUrl is null (e.g., render a fallback icon/placeholder box, or keep the container with visibility: hidden), so the layout doesn’t change between loading and loaded states.
Summary
currentCollectioncaptured the useQuery ref at setup time (still undefined) with a non-immediate watcher. Use a computed overcollection.valueand update the query cache from the edit flow so the header renders with real content on SSR (measured drop from CLS 0.28 → ~0.001).<img>now haswidth/height+fetchpriority="high", skeletons mirror the final layout (logo, title, description, meta row) so height is stable during client-side navigation.pageWidth-gated logo size swap (SSR / client mismatch caused header reflow post-hydration) and wrap<lfx-login>in a fixed-width (min-w-24) container so the search bar width doesn't change when the client-only login hydrates.requestAnimationFrame, so the many components reacting toscrollTopdon't do reactive work multiple times per frame.window.load+ idle so its widget iframe entrance animation doesn't land inside the CLS measurement window.avatars.githubusercontent.com/raw.githubusercontent.comwhere project and collection hero logos are served from.Changes
frontend/app/components/modules/collection/views/collection-details.vue— computedcurrentCollection, edit flow usesqueryClient.setQueryData.frontend/app/components/modules/collection/components/details/header.vue— skeleton matches final layout, hero<img>gets dimensions + priority hint.frontend/app/components/modules/project/components/shared/header.vue— logo size no longer depends onpageWidth.frontend/app/components/shared/layout/menu.vue— fixed-width wrapper around<client-only>login.frontend/app/components/shared/utils/scroll.ts— passive listener + rAF-batched updates.frontend/app/plugins/intercom.ts— defer anonymous boot viarequestIdleCallback.frontend/setup/head.ts— preconnect to GitHub avatar origins.