2024年09月28日7 分钟

首屏加载优化全方案:从白屏到秒开

系统性梳理首屏加载优化的所有方案,涵盖网络、资源、渲染、感知四个维度

首屏加载的时间分解

首屏总耗时 = DNS + TCP + TTFB + HTML下载 + CSS下载 + JS下载 + JS执行 + 数据请求 + 渲染

各阶段典型耗时(未优化):
  DNS:     50ms
  TCP:     50ms
  TTFB:    200ms
  HTML:    50ms
  CSS:     100ms  ← 阻塞渲染
  JS:      500ms  ← 最大瓶颈
  JS执行:  300ms  ← 阻塞交互
  API:     400ms
  渲染:    100ms
  ─────────────
  总计:   1750ms

维度一:网络层优化

1. CDN 加速

静态资源(JS/CSS/图片)部署到 CDN:
  源服务器(美国)→ 用户: 200-400ms
  CDN 节点(本地)→ 用户: 5-20ms

配置:
  HTML 不缓存(或短缓存)→ 保证更新能生效
  JS/CSS(带 hash)→ 强缓存 1 年
  图片/字体 → 强缓存 1 年

2. HTTP/2 多路复用

HTTP/1.1: 一个连接一次只能传一个文件 → 队头阻塞
  [CSS] → [JS1] → [JS2] → [JS3] → [image]  串行

HTTP/2: 一个连接同时传多个文件 → 并行
  [CSS ──────]
  [JS1 ───]
  [JS2 ────]       并行
  [JS3 ──]
  [image ─────]

3. 资源预加载

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//cdn.example.com">

<!-- 预连接 -->
<link rel="preconnect" href="https://fonts.googleapis.com">

<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
<link rel="preload" href="/hero.webp" as="image">

<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/about-page.js">

维度二:资源体积优化

1. JS Bundle 瘦身

优化前:bundle.js = 2MB

① 代码分割:
   main.js    = 200KB(首屏核心)
   vendor.js  = 300KB(React + 第三方库)
   page-a.js  = 150KB(按需加载)

② Tree Shaking:移除未使用代码
   lodash 整包 70KB → 只用 debounce 2KB

③ 替换重型依赖:
   moment.js (70KB) → date-fns (3KB 按需)
   lodash (70KB) → lodash-es (按需 tree-shake)

④ 压缩:Brotli 压缩
   500KB → 120KB (gzipped) → 95KB (brotli)

优化后:首屏 JS = 200KB → Brotli = 50KB

2. CSS 优化

<!-- 内联关键 CSS -->
<style>
  /* 首屏渲染需要的最小 CSS */
  *, *::before, *::after { box-sizing: border-box; margin: 0; }
  body { font-family: system-ui, sans-serif; }
  .hero { min-height: 100vh; display: flex; align-items: center; }
</style>

<!-- 异步加载完整 CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>

3. 图片优化

// Next.js Image 自动优化
<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority              // 首屏图片:预加载
  placeholder="blur"    // 模糊占位图
  sizes="100vw"         // 响应式
/>

// 等价于自动生成:
// <img srcset="hero-640.webp 640w, hero-1200.webp 1200w, hero-1920.webp 1920w"
//      sizes="100vw" src="hero-1200.webp" loading="eager">

维度三:渲染策略优化

1. SSR / SSG 消除白屏

CSR 首屏:
  0ms     500ms    1000ms    1500ms    2000ms
  │────白屏────│──JS执行──│──API请求──│──渲染──│
                                            ↑ 首屏可见

SSR 首屏:
  0ms     300ms    800ms
  │──TTFB──│──渲染──│──Hydration──│
            ↑ 首屏可见(提前 1200ms)

SSG 首屏:
  0ms   100ms    600ms
  │CDN──│──渲染──│──Hydration──│
        ↑ 首屏可见(最快)

2. 流式 SSR (React 18)

// 不等所有数据到齐,先发 HTML 外壳
export default function Page() {
  return (
    <main>
      <Header />  {/* 立即返回 */}

      <Suspense fallback={<HeroSkeleton />}>
        <HeroSection />  {/* 数据准备好后流式推送 */}
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <RecentPosts />  {/* 独立加载,不阻塞其他部分 */}
      </Suspense>
    </main>
  )
}

// 用户体验:
// 0ms: 看到 Header + 两个骨架屏
// 200ms: HeroSection 数据到了 → 替换骨架屏
// 500ms: RecentPosts 数据到了 → 替换骨架屏
// 全程无白屏!

3. 渐进式 Hydration

// 非关键组件延迟水合
import dynamic from 'next/dynamic'

const Comments = dynamic(() => import('./Comments'), {
  ssr: false,  // 不在服务端渲染
  loading: () => <CommentsSkeleton />,
})

const Footer = dynamic(() => import('./Footer'), {
  ssr: true,
  loading: () => <FooterSkeleton />,
})

// 首屏只水合关键组件 → TTI 更快

维度四:感知性能优化

即使实际加载时间不变,也能让用户感觉更快

1. 骨架屏(Skeleton)

function ArticleSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-2/3" />
    </div>
  )
}

// 用户看到骨架屏 → 知道内容在加载 → 不会认为页面坏了
// 比白屏或 Loading 转圈好得多

2. 渐进式图片加载

// 先加载模糊缩略图,再替换为高清图
function ProgressiveImage({ src, placeholder }) {
  const [loaded, setLoaded] = useState(false)

  return (
    <div className="relative">
      {/* 模糊占位图(1KB 内联 base64)*/}
      <img
        src={placeholder}
        className={`w-full transition-opacity duration-300 ${loaded ? 'opacity-0' : 'opacity-100'}`}
        style={{ filter: 'blur(20px)' }}
      />
      {/* 高清图 */}
      <img
        src={src}
        onLoad={() => setLoaded(true)}
        className={`absolute inset-0 w-full transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`}
      />
    </div>
  )
}

3. 乐观更新(Optimistic UI)

// 不等服务器响应,立即更新 UI
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false)

  const handleLike = async () => {
    setLiked(true)  // 立即更新 UI(乐观)

    try {
      await fetch(`/api/posts/${postId}/like`, { method: 'POST' })
    } catch {
      setLiked(false)  // 失败了回滚
    }
  }

  return <button onClick={handleLike}>{liked ? '❤️' : '🤍'}</button>
}

首屏优化检查清单

网络层:
  □ CDN 部署
  □ HTTP/2 启用
  □ Brotli 压缩
  □ DNS 预解析
  □ 关键资源 preload

资源体积:
  □ 代码分割(路由级 + 组件级)
  □ Tree Shaking 验证
  □ 图片 WebP/AVIF 格式
  □ 字体 woff2 + display:swap
  □ 移除未使用依赖

渲染策略:
  □ SSR / SSG / ISR 选型
  □ 流式 SSR + Suspense
  □ 非关键组件延迟加载
  □ 关键 CSS 内联

感知优化:
  □ 骨架屏
  □ 渐进式图片
  □ 路由预加载
  □ Loading 状态