From 141ceab7e6f606a5f5c5798f8255ddc2da2bedfc Mon Sep 17 00:00:00 2001 From: Adam French Date: Mon, 9 Mar 2026 16:41:55 +0000 Subject: [PATCH] Reduce performance lost on large screens --- nginx/vue/src/components/text/Headline.vue | 25 ++++++- nginx/vue/src/components/util/AutoScroll.vue | 19 ++++- nginx/vue/src/views/home/Intro2.vue | 77 ++++++++++++++------ 3 files changed, 93 insertions(+), 28 deletions(-) diff --git a/nginx/vue/src/components/text/Headline.vue b/nginx/vue/src/components/text/Headline.vue index 21427ea..1325378 100644 --- a/nginx/vue/src/components/text/Headline.vue +++ b/nginx/vue/src/components/text/Headline.vue @@ -5,21 +5,31 @@ const container = useTemplateRef("container"); const item1 = useTemplateRef("item1"); let offset = 0; +let cachedWidth = 0; let rafId; const speed = 0.5; // pixels per frame -function animate() { +function measureWidth() { const ctnr = container.value; const it1 = item1.value; + if (ctnr && it1) { + cachedWidth = Math.max(ctnr.offsetWidth, it1.scrollWidth); + } +} - const width = Math.max(ctnr.offsetWidth, it1.scrollWidth); +function animate() { + const ctnr = container.value; + if (!ctnr || cachedWidth === 0) { + rafId = requestAnimationFrame(animate); + return; + } offset -= speed; - if (offset <= -width) { - offset += width; + if (offset <= -cachedWidth) { + offset += cachedWidth; } ctnr.style.transform = `translateX(${offset}px)`; @@ -27,12 +37,19 @@ function animate() { rafId = requestAnimationFrame(animate); } +let resizeObserver; + onMounted(() => { + measureWidth(); rafId = requestAnimationFrame(animate); + + resizeObserver = new ResizeObserver(measureWidth); + resizeObserver.observe(container.value); }); onUnmounted(() => { cancelAnimationFrame(rafId); + resizeObserver?.disconnect(); }); diff --git a/nginx/vue/src/components/util/AutoScroll.vue b/nginx/vue/src/components/util/AutoScroll.vue index 0df118d..471fa3c 100644 --- a/nginx/vue/src/components/util/AutoScroll.vue +++ b/nginx/vue/src/components/util/AutoScroll.vue @@ -16,6 +16,12 @@ let pos = 0; let direction = 1; // 1 = down, -1 = up let timeoutId; let timeoutId2; +let cachedScrollHeight = 0; + +function measureScrollHeight() { + const el = container.value; + if (el) cachedScrollHeight = el.scrollHeight; +} function handleHover() { cancelAnimationFrame(timeoutId); @@ -28,6 +34,10 @@ function handleHover() { function tick() { const el = container.value; + if (!el || cachedScrollHeight === 0) { + timeoutId = requestAnimationFrame(tick); + return; + } const reachedBottom = pos <= 0; const reachedTop = pos >= 1; @@ -46,16 +56,23 @@ function tick() { pos += direction * SPEED; - el.scrollTop = pos * el.scrollHeight; + el.scrollTop = pos * cachedScrollHeight; timeoutId = requestAnimationFrame(tick); } +let resizeObserver; + onMounted(() => { + measureScrollHeight(); timeoutId = requestAnimationFrame(tick); + + resizeObserver = new ResizeObserver(measureScrollHeight); + resizeObserver.observe(container.value); }); onBeforeUnmount(() => { cancelAnimationFrame(timeoutId); + resizeObserver?.disconnect(); }); diff --git a/nginx/vue/src/views/home/Intro2.vue b/nginx/vue/src/views/home/Intro2.vue index 735ad51..1332451 100644 --- a/nginx/vue/src/views/home/Intro2.vue +++ b/nginx/vue/src/views/home/Intro2.vue @@ -23,49 +23,83 @@ const phrases = [ "I like anime, all kinds of music and sci fic", ]; +// Non-reactive animation state to avoid triggering Vue re-renders every frame +const animState = phrases.map((text, i) => ({ + x: i * 20, + y: i * 20, + dx: rand(0, 30) / 100, + dy: 0.5, + content: text, + cachedW: 0, + cachedH: 0, +})); + +// Reactive items only for initial render const items = ref( - phrases.map((text, i) => ({ - x: i * 20, - y: i * 20, - dx: rand(0, 30) / 100, - dy: 0.5, - content: text, + animState.map((s) => ({ + x: s.x, + y: s.y, + dx: s.dx, + dy: s.dy, + content: s.content, })), ); let rafId = 0; +let cachedCW = 0; +let cachedCH = 0; + +function measureSizes() { + const c = container.value; + if (c) { + cachedCW = c.clientWidth; + cachedCH = c.clientHeight; + } + itemEls.value.forEach((el, i) => { + if (el && animState[i]) { + animState[i].cachedW = el.offsetWidth; + animState[i].cachedH = el.offsetHeight; + } + }); +} function animate() { - const c = container.value; - if (!c) return; + if (!cachedCW || !cachedCH) { + rafId = requestAnimationFrame(animate); + return; + } - const cw = c.clientWidth; - const ch = c.clientHeight; - - items.value.forEach((item, i) => { + for (let i = 0; i < animState.length; i++) { + const s = animState[i]; const el = itemEls.value[i]; - if (!el) return; + if (!el) continue; - const ew = el.offsetWidth; - const eh = el.offsetHeight; + s.x += s.dx; + s.y += s.dy; - item.x += item.dx; - item.y += item.dy; + if (s.x < 0 || s.x > cachedCW - s.cachedW) s.dx *= -1; + if (s.y < 0 || s.y > cachedCH - s.cachedH) s.dy *= -1; - if (item.x < 0 || item.x > cw - ew) item.dx *= -1; - if (item.y < 0 || item.y > ch - eh) item.dy *= -1; - }); + el.style.transform = `translate(${s.x}px, ${s.y}px)`; + } rafId = requestAnimationFrame(animate); } +let resizeObserver: ResizeObserver; + onMounted(async () => { await nextTick(); + measureSizes(); rafId = requestAnimationFrame(animate); + + resizeObserver = new ResizeObserver(measureSizes); + resizeObserver.observe(container.value!); }); onUnmounted(() => { cancelAnimationFrame(rafId); + resizeObserver?.disconnect(); }); @@ -79,9 +113,6 @@ onUnmounted(() => { :key="i" ref="itemEls" class="absolute w-fit h-fit" - :style="{ - transform: `translate(${item.x}px, ${item.y}px)`, - }" >

{{ item.content }}