本文记录了我所做一次系统性的首页性能优化:在保留 SSR 的前提下,把大面积的客户端组件拆解为“Server 外壳 + 小型 Client 子组件”,并优化媒体加载策略(首屏只解码必要帧、按需预热、避免重复请求)。
背景与问题
- 首页是 SSR,但性能仍然很差:LCP 超过 10s。
根因排查:
- 顶层布局 use client 让整棵树失去 SSR 优势,导致首屏要下载和 hydration 大量 JS。
- 首屏区块全部是客户端组件,进一步放大 JS 与 hydration。
- Swiper 实现会在首屏并发解码多图/视频,分流首帧带宽,拖慢 LCP。
- 部分区块用 CSS 背景图,首屏就加载,缺乏懒加载。
优化一:恢复 SSR 能力,缩小客户端范围
问题:公共布局 顶部 use client 导致整棵子树都在客户端渲染。
- 做法:移除 use client,让 (public) 布局回归 Server Component;将必须在客户端的行为(如 Navbar 的滚动/首页判断)保留为独立的“客户端岛”。
对比:
// 之前(反例):整个布局客户端化,丢失 SSR 优势
"use client";
import { Navbar } from "@/components/(public)/Navbar";
// ...
export default function PublicLayout({ children }: { children: React.ReactNode }) {
// useIsHomePage() 等客户端逻辑
return (
<div>
<Navbar />
<main>{children}</main>
</div>
);
}
// 现在(推荐):布局是 Server 组件,仅把 Navbar 保持为小 Client 岛
import { Navbar } from "@/components/(public)/Navbar";
// ...
export default function PublicLayout({ children }: { children: React.ReactNode }) {
return (
<div className="relative">
<Navbar /> {/* 小型客户端组件 */}
<main className="sm:min-w-[1005px]">{children}</main>
{/* Footer / FooterBar 等保持 SSR */}
</div>
);
}
- 效果:SSR 首屏直出回归,hydration 范围显著缩小,对 LCP 和 SEO 都是正向作用。
优化二:把“展示型区块”改为 Server 外壳 + 小 Client 子组件
问题:首页楼层组件大量使用 use client,导致首屏 JS 与 hydration 暴涨。
- 设计:Server 外壳负责纯展示;必要的交互(比如表格行点击、手风琴展开)抽成一个小 Client 子组件。
以 新闻中心 楼层为例:
// 之前:整个区块是 Client 组件(反例)
"use client";
import { Table } from "@heroui/table";
export const NewsCenter = () => (
<Table /* ... */>...</Table>
);
// 现在:Server 外壳 + Client 子组件
import { FloorContainer } from "./components/FloorContainer";
import { NewsTableClient } from "@/app/(public)/overview/components/NewsTableClient";
export const NewsCenter = () => {
return (
<FloorContainer /* SSR 渲染标题/说明等纯展示区域 */>
<NewsTableClient columns={columns} mobileColumns={mobileColumns} rows={rows} />
</FloorContainer>
);
}
// NewsTableClient.tsx(小型 Client 子组件)
"use client";
import { Table } from "@heroui/table";
import { useRouter } from "next/navigation";
export function NewsTableClient({ columns, mobileColumns, rows }) {
const router = useRouter();
const handleRowAction = (key) => {
const item = rows.find((r) => r.key === String(key));
if (!item?.link) return;
item.link.startsWith("http") ? window.open(item.link, "_blank") : router.push(item.link);
};
return (
<>
{/* PC / H5 表格... */}
<Table isHeaderSticky onRowAction={handleRowAction}>...</Table>
</>
);
}
文件中心 楼层也采取同样模式:Server 外壳 + DocumentAccordionClient 仅承载手风琴的“选中态”。
架构示意图:
- 收益:大部分首页结构与文案直出 HTML,小范围交互再 hydration,显著降低 First Load JS 与 hydration 时间。
优化三:将 CSS 背景图替换为 next/image(懒加载 + 首次 hover 才加载并复用)
问题:部分区块(如 故事库 楼层桌面端)使用 CSS 背景图,首屏就会并发加载所有卡片图片,造成带宽争抢。hover 切换图也会每次重新请求。
- 目标:默认图懒加载;hover 图首次 hover 再加载,并保持挂载避免重复请求。
onMouseEnter={(e) => {
setHoveredIndex(index);
setVisitedHover((prev) => {
const next = new Set(prev);
next.add(index);
return next;
});
// 预热 hover 图
try {
if (cardData.hoverImage) {
const img = new window.Image();
img.src = cardData.hoverImage;
}
} catch {}
e.currentTarget.style.left = "0%";
}}
Hover 加载状态机(简化版):
- 收益:首屏不再并发请求全部背景;hover 图只请求一次,后续复用,避免流量浪费。
效果与经验
- SSR 恢复 + 客户端岛屿:减少 hydration 范围,提高首屏可用性和 SEO 抓取质量。
- 媒体策略收敛:首屏只解码必要帧,减少网络竞争,切换体验更稳。
- 架构清晰,迭代更容易扩展:Server 外壳渲染所有文案与结构,Client 岛只承载必要交互。
小提示:
- 图像资源建议使用 AVIF/WEBP,控制尺寸与 sizes,首图可 priority。
- @fortawesome/* 如使用大量图标,建议改按需引入,避免把整库打进客户端包。
结语
这次优化的核心理念是“让 SSR 真正发挥作用 + 让客户端 JS 只做该做的事”。Server 外壳直出结构与文案,交互则拆解为小而精的“客户端岛屿”;媒体层面严格按需加载、按需预热、一次加载多次复用。这样既提升了 LCP,又避免了重复拉流与资源浪费,同时保持了现有样式和用户体验。
本文由 小但 创作
全文共:4146个字
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载,均为作者原创,转载前请务必署名
最后编辑时间为: Aug 13, 2025 at 03:42 pm