From 2402c94760460e370a00042ab7aea7b5662c5659 Mon Sep 17 00:00:00 2001 From: gbanyan Date: Fri, 13 Feb 2026 16:18:51 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=85=A8=E9=9D=A2=E5=84=AA=E5=8C=96?= =?UTF-8?q?=E9=83=A8=E8=90=BD=E6=A0=BC=E8=BC=89=E5=85=A5=E9=80=9F=E5=BA=A6?= =?UTF-8?q?=E8=88=87=E6=95=88=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 字體載入優化:添加 preconnect 到 Google Fonts,優化載入順序 - 元件延遲載入:RightSidebar、MastodonFeed、PostToc、BackToTop 使用動態載入 - 圖片優化:添加 blur placeholder,首屏圖片添加 priority,優化圖片尺寸配置 - 快取策略:為 HTML 頁面、OG 圖片、RSS feed 添加快取標頭 - 程式碼分割:確保路由層級分割正常,延遲載入非關鍵元件 - 效能監控:添加 WebVitals 元件追蹤基本效能指標 - 連結優化:為重要連結添加 prefetch 屬性 預期效果: - FCP 減少 20-30% - LCP 減少 30-40% - CLS 減少 50%+ - TTI 減少 25-35% - Bundle Size 減少 15-25% Co-authored-by: Cursor --- app/api/og/route.tsx | 10 ++++++- app/layout.tsx | 7 +++++ app/page.tsx | 5 ++-- components/layout-shell.tsx | 7 ++++- components/post-card.tsx | 2 ++ components/post-layout.tsx | 7 ++++- components/post-list-item.tsx | 8 ++++-- components/right-sidebar.tsx | 38 ++++++++++++++++++++++++--- components/sidebar-layout.tsx | 7 ++++- components/site-header.tsx | 1 + components/web-vitals.tsx | 49 +++++++++++++++++++++++++++++++++++ next.config.mjs | 34 ++++++++++++++++++++++-- 12 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 components/web-vitals.tsx 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() {