perf: convert Hero to server component for faster LCP
All checks were successful
Gitea CI / test-build (push) Successful in 11m9s
All checks were successful
Gitea CI / test-build (push) Successful in 11m9s
- Hero now renders server-side, eliminating JS dependency for LCP text - CMS messages fetched server-side instead of client useEffect - Removes Hero from client JS bundle (~5KB less) - LCP element (hero paragraph) now in initial HTML response - Eliminates 2,380ms 'element rendering delay' reported by PSI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,16 +1,21 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import Hero from '@/app/components/Hero';
|
import Hero from '@/app/components/Hero';
|
||||||
|
|
||||||
// Mock next-intl
|
// Mock next-intl/server
|
||||||
jest.mock('next-intl', () => ({
|
jest.mock('next-intl/server', () => ({
|
||||||
useLocale: () => 'en',
|
getTranslations: () => Promise.resolve((key: string) => {
|
||||||
useTranslations: () => (key: string) => {
|
|
||||||
const messages: Record<string, string> = {
|
const messages: Record<string, string> = {
|
||||||
description: 'Dennis is a student and passionate self-hoster.',
|
description: 'Dennis is a student and passionate self-hoster.',
|
||||||
ctaWork: 'View My Work'
|
ctaWork: 'View My Work',
|
||||||
|
ctaContact: 'Get in touch',
|
||||||
};
|
};
|
||||||
return messages[key] || key;
|
return messages[key] || key;
|
||||||
},
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock directus getMessages
|
||||||
|
jest.mock('@/lib/directus', () => ({
|
||||||
|
getMessages: () => Promise.resolve({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock next/image
|
// Mock next/image
|
||||||
@@ -36,8 +41,9 @@ jest.mock('next/image', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Hero', () => {
|
describe('Hero', () => {
|
||||||
it('renders the hero section correctly', () => {
|
it('renders the hero section correctly', async () => {
|
||||||
render(<Hero />);
|
const HeroResolved = await Hero({ locale: 'en' });
|
||||||
|
render(HeroResolved);
|
||||||
|
|
||||||
// Check for the main headlines (defaults in Hero.tsx)
|
// Check for the main headlines (defaults in Hero.tsx)
|
||||||
expect(screen.getByText('Building')).toBeInTheDocument();
|
expect(screen.getByText('Building')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function HomePage() {
|
|||||||
{/* Spacer to prevent navbar overlap */}
|
{/* Spacer to prevent navbar overlap */}
|
||||||
<div className="h-16 sm:h-24 md:h-32" aria-hidden="true"></div>
|
<div className="h-16 sm:h-24 md:h-32" aria-hidden="true"></div>
|
||||||
<main className="relative">
|
<main className="relative">
|
||||||
<Hero />
|
<Hero locale="en" />
|
||||||
|
|
||||||
{/* Wavy Separator 1 - Hero to About */}
|
{/* Wavy Separator 1 - Hero to About */}
|
||||||
<div className="relative h-12 sm:h-16 md:h-24 overflow-hidden">
|
<div className="relative h-12 sm:h-16 md:h-24 overflow-hidden">
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import Header from "../components/Header.server";
|
import Header from "../components/Header.server";
|
||||||
|
import Hero from "../components/Hero";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import {
|
import {
|
||||||
getHeroTranslations,
|
|
||||||
getAboutTranslations,
|
getAboutTranslations,
|
||||||
getProjectsTranslations,
|
getProjectsTranslations,
|
||||||
getContactTranslations,
|
getContactTranslations,
|
||||||
getFooterTranslations,
|
getFooterTranslations,
|
||||||
} from "@/lib/translations-loader";
|
} from "@/lib/translations-loader";
|
||||||
import {
|
import {
|
||||||
HeroClient,
|
|
||||||
AboutClient,
|
AboutClient,
|
||||||
ProjectsClient,
|
ProjectsClient,
|
||||||
ContactClient,
|
ContactClient,
|
||||||
@@ -20,9 +19,8 @@ interface HomePageServerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePageServer({ locale }: HomePageServerProps) {
|
export default async function HomePageServer({ locale }: HomePageServerProps) {
|
||||||
// Parallel laden aller Translations
|
// Parallel laden aller Translations (hero translations handled by Hero server component)
|
||||||
const [heroT, aboutT, projectsT, contactT, footerT] = await Promise.all([
|
const [aboutT, projectsT, contactT, footerT] = await Promise.all([
|
||||||
getHeroTranslations(locale),
|
|
||||||
getAboutTranslations(locale),
|
getAboutTranslations(locale),
|
||||||
getProjectsTranslations(locale),
|
getProjectsTranslations(locale),
|
||||||
getContactTranslations(locale),
|
getContactTranslations(locale),
|
||||||
@@ -57,7 +55,7 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
|||||||
{/* Spacer to prevent navbar overlap */}
|
{/* Spacer to prevent navbar overlap */}
|
||||||
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
||||||
<main className="relative">
|
<main className="relative">
|
||||||
<HeroClient locale={locale} translations={heroT} />
|
<Hero locale={locale} />
|
||||||
|
|
||||||
{/* Wavy Separator 1 - Hero to About */}
|
{/* Wavy Separator 1 - Hero to About */}
|
||||||
<div className="relative h-24 overflow-hidden">
|
<div className="relative h-24 overflow-hidden">
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
|
|
||||||
import { NextIntlClientProvider } from 'next-intl';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import Hero from './Hero';
|
|
||||||
import type {
|
import type {
|
||||||
HeroTranslations,
|
|
||||||
AboutTranslations,
|
AboutTranslations,
|
||||||
ProjectsTranslations,
|
ProjectsTranslations,
|
||||||
ContactTranslations,
|
ContactTranslations,
|
||||||
@@ -30,23 +28,6 @@ function getNormalizedLocale(locale: string): 'en' | 'de' {
|
|||||||
return locale.startsWith('de') ? 'de' : 'en';
|
return locale.startsWith('de') ? 'de' : 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroClient({ locale }: { locale: string; translations: HeroTranslations }) {
|
|
||||||
const normalLocale = getNormalizedLocale(locale);
|
|
||||||
const baseMessages = messageMap[normalLocale];
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
home: {
|
|
||||||
hero: baseMessages.home.hero
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
||||||
<Hero />
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AboutClient({ locale }: { locale: string; translations: AboutTranslations }) {
|
export function AboutClient({ locale }: { locale: string; translations: AboutTranslations }) {
|
||||||
const normalLocale = getNormalizedLocale(locale);
|
const normalLocale = getNormalizedLocale(locale);
|
||||||
const baseMessages = messageMap[normalLocale];
|
const baseMessages = messageMap[normalLocale];
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
"use client";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { getMessages } from "@/lib/directus";
|
||||||
|
|
||||||
const Hero = () => {
|
interface HeroProps {
|
||||||
const locale = useLocale();
|
locale: string;
|
||||||
const t = useTranslations("home.hero");
|
|
||||||
const [cmsMessages, setCmsMessages] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/messages?locale=${locale}`);
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
setCmsMessages(data.messages || {});
|
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
})();
|
|
||||||
}, [locale]);
|
|
||||||
|
|
||||||
// Helper to get CMS text or fallback
|
export default async function Hero({ locale }: HeroProps) {
|
||||||
|
const [t, cmsMessages] = await Promise.all([
|
||||||
|
getTranslations("home.hero"),
|
||||||
|
getMessages(locale).catch(() => ({} as Record<string, string>)),
|
||||||
|
]);
|
||||||
|
|
||||||
const getLabel = (key: string, fallback: string) => cmsMessages[key] || fallback;
|
const getLabel = (key: string, fallback: string) => cmsMessages[key] || fallback;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -86,6 +76,4 @@ const Hero = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Hero;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user