diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index b6076f3..c0ffd44 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -10,7 +10,7 @@ export async function GET(request: NextRequest) { const description = searchParams.get('description') || ''; const tags = searchParams.get('tags')?.split(',').slice(0, 3) || []; - return new ImageResponse( + const imageResponse = new ImageResponse( (
+ + {/* Preconnect to Google Fonts for faster font loading */} + + + @@ -117,6 +123,7 @@ export default function RootLayout({ {children} + ); diff --git a/app/page.tsx b/app/page.tsx index 6b6a649..520c497 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -50,14 +50,15 @@ export default function HomePage() { 所有文章 →
- {posts.map((post) => ( - + {posts.map((post, index) => ( + ))} diff --git a/components/layout-shell.tsx b/components/layout-shell.tsx index 4e7eb0c..803125b 100644 --- a/components/layout-shell.tsx +++ b/components/layout-shell.tsx @@ -1,6 +1,11 @@ import { SiteHeader } from './site-header'; import { SiteFooter } from './site-footer'; -import { BackToTop } from './back-to-top'; +import dynamic from 'next/dynamic'; + +// Lazy load BackToTop since it's not critical for initial render +const BackToTop = dynamic(() => import('./back-to-top').then(mod => ({ default: mod.BackToTop })), { + ssr: false, +}); export function LayoutShell({ children }: { children: React.ReactNode }) { return ( diff --git a/components/post-card.tsx b/components/post-card.tsx index 83febd0..f16e652 100644 --- a/components/post-card.tsx +++ b/components/post-card.tsx @@ -28,6 +28,8 @@ export function PostCard({ post, showTags = true }: PostCardProps) { height={360} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" loading="lazy" + placeholder="blur" + blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q==" className="mx-auto max-h-60 w-full object-contain transition-transform duration-300 ease-out group-hover:scale-105" /> diff --git a/components/post-layout.tsx b/components/post-layout.tsx index ebc5cb2..c134e7c 100644 --- a/components/post-layout.tsx +++ b/components/post-layout.tsx @@ -3,10 +3,15 @@ import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { FiList, FiX } from 'react-icons/fi'; -import { PostToc } from './post-toc'; +import dynamic from 'next/dynamic'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; +// Lazy load PostToc since it's not critical for initial render +const PostToc = dynamic(() => import('./post-toc').then(mod => ({ default: mod.PostToc })), { + ssr: false, +}); + function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } diff --git a/components/post-list-item.tsx b/components/post-list-item.tsx index 0006843..e7758e6 100644 --- a/components/post-list-item.tsx +++ b/components/post-list-item.tsx @@ -7,9 +7,10 @@ import { MetaItem } from './meta-item'; interface Props { post: Post; + priority?: boolean; } -export function PostListItem({ post }: Props) { +export function PostListItem({ post, priority = false }: Props) { const cover = post.feature_image && post.feature_image.startsWith('../assets') ? post.feature_image.replace('../assets', '/assets') @@ -29,7 +30,10 @@ export function PostListItem({ post }: Props) { width={320} height={240} sizes="(max-width: 640px) 96px, 160px" - loading="lazy" + loading={priority ? undefined : 'lazy'} + priority={priority} + placeholder="blur" + blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q==" className="h-full w-full object-cover transition-transform duration-300 ease-out group-hover:scale-105" /> diff --git a/components/right-sidebar.tsx b/components/right-sidebar.tsx index b3ee5ad..ddc6a0c 100644 --- a/components/right-sidebar.tsx +++ b/components/right-sidebar.tsx @@ -1,13 +1,43 @@ +'use client'; + import Link from 'next/link'; import Image from 'next/image'; +import { useEffect, useRef, useState } from 'react'; import { FaGithub, FaMastodon, FaLinkedin } from 'react-icons/fa'; import { FiTrendingUp, FiArrowRight } from 'react-icons/fi'; import { siteConfig } from '@/lib/config'; import { getAllTagsWithCount } from '@/lib/posts'; import { allPages } from 'contentlayer2/generated'; -import { MastodonFeed } from './mastodon-feed'; +import dynamic from 'next/dynamic'; + +// Lazy load MastodonFeed - only load when sidebar is visible +const MastodonFeed = dynamic(() => import('./mastodon-feed').then(mod => ({ default: mod.MastodonFeed })), { + ssr: false, +}); export function RightSidebar() { + const [shouldLoadFeed, setShouldLoadFeed] = useState(false); + const feedRef = useRef(null); + + useEffect(() => { + // Use Intersection Observer to lazy load MastodonFeed when sidebar is visible + if (!feedRef.current) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setShouldLoadFeed(true); + observer.disconnect(); + } + }, + { rootMargin: '100px' } // Start loading 100px before it's visible + ); + + observer.observe(feedRef.current); + + return () => observer.disconnect(); + }, []); + const tags = getAllTagsWithCount().slice(0, 5); const aboutPage = @@ -91,8 +121,10 @@ export function RightSidebar() { - {/* Mastodon Feed */} - + {/* Mastodon Feed - Lazy loaded when visible */} +
+ {shouldLoadFeed && } +
{tags.length > 0 && (
diff --git a/components/sidebar-layout.tsx b/components/sidebar-layout.tsx index 6221bf7..4e5ab86 100644 --- a/components/sidebar-layout.tsx +++ b/components/sidebar-layout.tsx @@ -1,4 +1,9 @@ -import { RightSidebar } from './right-sidebar'; +import dynamic from 'next/dynamic'; + +// Lazy load RightSidebar since it's only visible on lg+ screens +const RightSidebar = dynamic(() => import('./right-sidebar').then(mod => ({ default: mod.RightSidebar })), { + ssr: false, +}); export function SidebarLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/components/site-header.tsx b/components/site-header.tsx index 71cb1aa..fb72f59 100644 --- a/components/site-header.tsx +++ b/components/site-header.tsx @@ -78,6 +78,7 @@ export function SiteHeader() {