perf : replace loading flash with background preloader, parallel sub-page fetch
All checks were successful
Deploy Production / Deploy to Production (push) Successful in 25s
All checks were successful
Deploy Production / Deploy to Production (push) Successful in 25s
- Add #preloader div with scrollable-page-background.png as CSS background, matching the Svelte bg-fixed image so the transition is imperceptible (image is cached by the time Svelte requests it) - Revert #ssr-content to visually-hidden (clip) for Google Wave 1 crawl - Prevent scrollbar flash by adding overflow:hidden to html,body inline before main.css loads - Parallel-fetch sub-page data (/blog/slug.json) alongside parent in initRouter so Blog/WhitePapers components receive prefetched data on mount — eliminates the list→article flash on direct navigation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56e4e55791
commit
7ba18dcd9d
6 changed files with 101 additions and 4 deletions
|
|
@ -7,7 +7,10 @@
|
|||
<?php snippet('seo/head') ?>
|
||||
|
||||
<style>
|
||||
html, body { overflow: hidden; }
|
||||
#app { min-height: 100vh; }
|
||||
|
||||
/* Hidden SSR content — in DOM for Google Wave 1 crawl, invisible to users */
|
||||
#ssr-content {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
|
|
@ -17,6 +20,21 @@
|
|||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Preloader — covers the blank #app while JS loads.
|
||||
Uses the same background image as the Svelte app so the transition
|
||||
is seamless (image is already cached when Svelte requests it). */
|
||||
#preloader {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: #000 url('/assets/img/scrollable-page-background.png') center / cover no-repeat;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
#preloader.preloader-hiding {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Favicon -->
|
||||
|
|
@ -46,3 +64,4 @@
|
|||
</head>
|
||||
<body data-template="<?= $page->intendedTemplate() ?>">
|
||||
<div id="app"></div>
|
||||
<div id="preloader" aria-hidden="true"></div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@
|
|||
<?php foreach ($page->body()->toBlocks() as $block): ?>
|
||||
<?php if ($block->type() === 'text'): ?>
|
||||
<?= $block->content()->text()->value() ?>
|
||||
<?php elseif ($block->type() === 'heading'): ?>
|
||||
<<?= $block->content()->level() ?>><?= $block->content()->text() ?></<?= $block->content()->level() ?>>
|
||||
<?php elseif ($block->type() === 'quote'): ?>
|
||||
<blockquote><p><?= $block->content()->text() ?></p></blockquote>
|
||||
<?php elseif ($block->type() === 'image'): ?>
|
||||
<?php $imgFile = $block->content()->image()->toFile() ?>
|
||||
<?php if ($imgFile): ?>
|
||||
<figure><img src="<?= $imgFile->url() ?>" alt="<?= $block->content()->alt() ?>" /></figure>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
<?php
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import { slides } from '@state/slides.svelte'
|
||||
import { slideTo } from '@router'
|
||||
import { slideTo, hidePreloader } from '@router'
|
||||
|
||||
import Header from '@components/layout/Header.svelte'
|
||||
import Cursor from '@components/layout/Cursor.svelte'
|
||||
|
|
@ -95,6 +95,20 @@
|
|||
}
|
||||
})
|
||||
|
||||
// Hide preloader for direct (non-sub-page) routes once the slide is ready.
|
||||
// Sub-page routes (blog/slug, livres-blancs/slug) handle it themselves.
|
||||
$effect(() => {
|
||||
if (!isReady) return
|
||||
const pathParts = window.location.pathname.replace(/^\/en/, '').split('/').filter(Boolean)
|
||||
const isSubPage = pathParts.length >= 2
|
||||
if (!isSubPage) hidePreloader()
|
||||
})
|
||||
|
||||
// Standalone pages (policy, privacy, etc.)
|
||||
$effect(() => {
|
||||
if (slides.standalone) hidePreloader()
|
||||
})
|
||||
|
||||
// Détecte quand les médias critiques de la slide active sont prêts,
|
||||
// puis déclenche le rendu progressif des autres slides.
|
||||
$effect(() => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@ import { site } from "@state/site.svelte";
|
|||
import { locale } from "@state/locale.svelte";
|
||||
|
||||
let siteInitialized = false;
|
||||
let prefetchedSubPageData = null;
|
||||
|
||||
export function consumePrefetchedSubPage() {
|
||||
const data = prefetchedSubPageData;
|
||||
prefetchedSubPageData = null;
|
||||
return data;
|
||||
}
|
||||
|
||||
export function hidePreloader() {
|
||||
const el = document.getElementById("preloader");
|
||||
if (!el) return;
|
||||
el.classList.add("preloader-hiding");
|
||||
setTimeout(() => el.remove(), 450);
|
||||
}
|
||||
|
||||
function normalizePath(path) {
|
||||
const stripped = path.replace(/^\/en(\/|$)/, "$1") || "/";
|
||||
|
|
@ -148,8 +162,23 @@ export async function initRouter() {
|
|||
|
||||
const initialPath = normalizePath(window.location.pathname);
|
||||
|
||||
// Detect sub-page and start parallel fetch before awaiting the parent
|
||||
const parentPath = initialPath.replace(/\/[^/]+$/, "");
|
||||
const isSubPage = !!parentPath && parentPath !== initialPath;
|
||||
|
||||
let subPageFetch = null;
|
||||
if (isSubPage) {
|
||||
subPageFetch = fetch(`${apiPrefix()}${initialPath}.json`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null);
|
||||
}
|
||||
|
||||
await loadSlide(initialPath);
|
||||
|
||||
if (subPageFetch) {
|
||||
prefetchedSubPageData = await subPageFetch;
|
||||
}
|
||||
|
||||
const idx = findSlideIndex(initialPath);
|
||||
if (idx !== -1) {
|
||||
slides.setActiveIndex(idx);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte'
|
||||
import { slides } from '@state/slides.svelte'
|
||||
import { locale } from '@state/locale.svelte'
|
||||
import { consumePrefetchedSubPage, hidePreloader } from '@router'
|
||||
|
||||
import Article from '@views/Article.svelte'
|
||||
import { t } from '@i18n'
|
||||
|
|
@ -83,10 +84,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
// On mount: check if URL is /blog/slug (direct navigation)
|
||||
$effect(() => {
|
||||
if (articleData) hidePreloader()
|
||||
})
|
||||
|
||||
// On mount: use prefetched article data if available, otherwise fetch
|
||||
onMount(() => {
|
||||
const slug = getSlugFromUrl()
|
||||
if (slug) openArticle(slug)
|
||||
if (slug) {
|
||||
const prefetched = consumePrefetchedSubPage()
|
||||
if (prefetched) {
|
||||
articleData = prefetched
|
||||
scrollToTop()
|
||||
} else {
|
||||
openArticle(slug)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte'
|
||||
import { slides } from '@state/slides.svelte'
|
||||
import { locale } from '@state/locale.svelte'
|
||||
import { consumePrefetchedSubPage, hidePreloader } from '@router'
|
||||
import WhitePaper from '@views/WhitePaper.svelte'
|
||||
import { t } from '@i18n'
|
||||
|
||||
|
|
@ -74,9 +75,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (itemData) hidePreloader()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const slug = getSlugFromUrl() ?? (data?.singleSlug ?? null)
|
||||
if (slug) openItem(slug)
|
||||
if (slug) {
|
||||
const prefetched = consumePrefetchedSubPage()
|
||||
if (prefetched) {
|
||||
itemData = prefetched
|
||||
sectionEl?.scrollTo(0, 0)
|
||||
} else {
|
||||
openItem(slug)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => window.removeEventListener('popstate', handlePopState)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue