diff --git a/next.config.mjs b/next.config.mjs index 3374e47..905520f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,7 @@ const nextConfig = { experimental: { ppr: true, inlineCss: true, + reactCompiler: true, }, typescript: { ignoreBuildErrors: true, diff --git a/package.json b/package.json index bda8114..d64f87b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@vercel/postgres": "^0.10.0", "@vercel/speed-insights": "^1.0.12", "ai": "^3.4.16", + "babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd376ee..cb27fc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: version: 1.34.3 '@vercel/analytics': specifier: ^1.3.1 - version: 1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + version: 1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) '@vercel/blob': specifier: ^0.25.1 version: 0.25.1 @@ -65,10 +65,13 @@ importers: version: 0.10.0 '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3)) + version: 1.0.12(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3)) ai: specifier: ^3.4.16 version: 3.4.16(openai@4.68.0(zod@3.23.8))(react@19.0.0-rc-cd22717c-20241013)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) + babel-plugin-react-compiler: + specifier: 19.0.0-beta-df7b47d-20241124 + version: 19.0.0-beta-df7b47d-20241124 bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -86,7 +89,7 @@ importers: version: 3.9.2 geist: specifier: ^1.3.1 - version: 1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)) + version: 1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)) jose: specifier: ^5.9.4 version: 5.9.4 @@ -98,7 +101,7 @@ importers: version: 0.453.0(react@19.0.0-rc-cd22717c-20241013) next: specifier: 15.0.4-canary.22 - version: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + version: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) openai: specifier: ^4.68.0 version: 4.68.0(zod@3.23.8) @@ -1482,6 +1485,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124: + resolution: {integrity: sha512-93iSASR20HNsotcOTQ+KPL0zpgfRFVWL86AtXpmHp995HuMVnC9femd8Winr3GxkPEh8lEOyaw3nqY4q2HUm5w==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4192,11 +4198,11 @@ snapshots: dependencies: crypto-js: 4.2.0 - '@vercel/analytics@1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)': + '@vercel/analytics@1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) react: 19.0.0-rc-cd22717c-20241013 '@vercel/blob@0.25.1': @@ -4220,9 +4226,9 @@ snapshots: transitivePeerDependencies: - utf-8-validate - '@vercel/speed-insights@1.0.12(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))': + '@vercel/speed-insights@1.0.12(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))': optionalDependencies: - next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) react: 19.0.0-rc-cd22717c-20241013 svelte: 4.2.19 vue: 3.5.12(typescript@5.6.3) @@ -4440,6 +4446,10 @@ snapshots: axobject-query@4.1.0: {} + babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124: + dependencies: + '@babel/types': 7.25.8 + balanced-match@1.0.2: {} bcryptjs@2.4.3: {} @@ -5182,9 +5192,9 @@ snapshots: functions-have-names@1.2.3: {} - geist@1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)): + geist@1.3.1(next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013)): dependencies: - next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) + next: 15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013) get-intrinsic@1.2.4: dependencies: @@ -5573,7 +5583,7 @@ snapshots: natural-compare@1.4.0: {} - next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013): + next@15.0.4-canary.22(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-df7b47d-20241124)(react-dom@19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013))(react@19.0.0-rc-cd22717c-20241013): dependencies: '@next/env': 15.0.4-canary.22 '@swc/counter': 0.1.3 @@ -5594,6 +5604,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 15.0.4-canary.22 '@next/swc-win32-x64-msvc': 15.0.4-canary.22 '@opentelemetry/api': 1.9.0 + babel-plugin-react-compiler: 19.0.0-beta-df7b47d-20241124 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' diff --git a/src/components/ui/link.tsx b/src/components/ui/link.tsx index 059b93f..b358c42 100644 --- a/src/components/ui/link.tsx +++ b/src/components/ui/link.tsx @@ -2,7 +2,7 @@ import NextLink from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; type PrefetchImage = { srcset: string; @@ -33,18 +33,15 @@ async function prefetchImages(href: string) { } const seen = new Set(); +const imageCache = new Map(); export const Link: typeof NextLink = (({ children, ...props }) => { - const [images, setImages] = useState([]); - const [preloading, setPreloading] = useState<(() => void)[]>([]); const linkRef = useRef(null); const router = useRouter(); - let prefetchTimeout: NodeJS.Timeout | null = null; // Track the timeout ID + let prefetchTimeout: NodeJS.Timeout | null = null; useEffect(() => { - if (props.prefetch === false) { - return; - } + if (props.prefetch === false) return; const linkElement = linkRef.current; if (!linkElement) return; @@ -53,31 +50,32 @@ export const Link: typeof NextLink = (({ children, ...props }) => { (entries) => { const entry = entries[0]; if (entry.isIntersecting) { - // Set a timeout to trigger prefetch after 1 second prefetchTimeout = setTimeout(async () => { router.prefetch(String(props.href)); - await sleep(0); // We want the doc prefetches to happen first. - void prefetchImages(String(props.href)).then((images) => { - setImages(images); - }, console.error); - // Stop observing once images are prefetched + await sleep(0); + + if (!imageCache.has(String(props.href))) { + void prefetchImages(String(props.href)).then((images) => { + imageCache.set(String(props.href), images); + }, console.error); + } + observer.unobserve(entry.target); - }, 300); // 300ms delay + }, 300); } else if (prefetchTimeout) { - // If the element leaves the viewport before 1 second, cancel the prefetch clearTimeout(prefetchTimeout); prefetchTimeout = null; } }, - { rootMargin: "0px", threshold: 0.1 }, // Trigger when at least 10% is visible + { rootMargin: "0px", threshold: 0.1 }, ); observer.observe(linkElement); return () => { - observer.disconnect(); // Cleanup the observer when the component unmounts + observer.disconnect(); if (prefetchTimeout) { - clearTimeout(prefetchTimeout); // Clear any pending timeouts when component unmounts + clearTimeout(prefetchTimeout); } }; }, [props.href, props.prefetch]); @@ -88,19 +86,10 @@ export const Link: typeof NextLink = (({ children, ...props }) => { prefetch={false} onMouseEnter={() => { router.prefetch(String(props.href)); - if (preloading.length) return; - const p: (() => void)[] = []; + const images = imageCache.get(String(props.href)) || []; for (const image of images) { - const remove = prefetchImage(image); - if (remove) p.push(remove); - } - setPreloading(p); - }} - onMouseLeave={() => { - for (const remove of preloading) { - remove(); + prefetchImage(image); } - setPreloading([]); }} onMouseDown={(e) => { const url = new URL(String(props.href), window.location.href); @@ -135,13 +124,4 @@ function prefetchImage(image: PrefetchImage) { img.srcset = image.srcset; img.src = image.src; img.alt = image.alt; - let done = false; - img.onload = img.onerror = () => { - done = true; - }; - return () => { - if (done) return; - img.src = img.srcset = ""; - seen.delete(image.srcset); - }; }