上周发版后,Lighthouse 跑分直接给移动端首屏赏了个 35 分的红灯,LCP (Largest Contentful Paint) 指标直接飙到了 6.8 秒。点开诊断报告一看,首屏那个占了大半个屏幕的主视觉 Banner 图,居然在页面加载完毕后 5 秒才姗姗来迟。更蠢的是,因为构建打包时用了某种通用的懒加载指令,这张决定首屏生死的大图居然被硬生生套上了 loading="lazy",再加上一大堆阻塞渲染的 CSS 和字体文件在前面排队,用户不跳出才怪。

针对这种情况,首要任务就是把这张首屏大图从延迟加载的泥潭里捞出来。常规的 <img> 标签即使没有 lazyload,也会因为浏览器解析 HTML 并发现它的优先级不够高,而被推迟到网络请求队列的后端。我们需要明确地告诉浏览器:“这张图是关键渲染路径上的核心,赶紧给我抢跑。”

首先,我们在 HTML 模板的 <head> 里直接塞入 <link rel="preload"> 指令。同时,利用 HTML5 最新的 fetchpriority 属性,把图片的获取优先级直接拉满到最高阶级:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>核心业务详情页</title>
  
  <!-- 预加载 LCP 关键图片,并设置最高优先级 -->
  <link 
    rel="preload" 
    as="image" 
    href="https://cdn.example.com/assets/images/hero-banner-large.webp" 
    fetchpriority="high"
    type="image/webp"
  >
  
  <!-- 内联 Critical CSS,避免加载外部大样式表阻塞渲染 -->
  <style id="critical-css">
    body {
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background-color: #0f172a;
      color: #f8fafc;
    }
    .hero-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 80vh;
      background-position: center;
      background-size: cover;
    }
    .hero-title {
      font-size: 2.5rem;
      font-weight: 800;
      line-height: 1.2;
      margin-bottom: 1rem;
      background: linear-gradient(135deg, #3b82f6, #8b5cf6);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    .hero-banner {
      width: 100%;
      max-width: 800px;
      height: auto;
      aspect-ratio: 16 / 9;
      border-radius: 12px;
      box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
    }
  </style>

  <!-- 异步加载非关键 CSS -->
  <link 
    rel="stylesheet" 
    href="/assets/css/main.chunk.min.css" 
    media="print" 
    onload="this.media='all'; this.onload=null;"
  >
  <noscript>
    <link rel="stylesheet" href="/assets/css/main.chunk.min.css">
  </noscript>
</head>
<body>
  <div class="hero-container">
    <h1 class="hero-title">构建下一代金融级数据看板</h1>
    <img 
      class="hero-banner"
      src="https://cdn.example.com/assets/images/hero-banner-large.webp" 
      fetchpriority="high" 
      alt="Hero Banner"
      decoding="async"
    >
  </div>
</body>
</html>

光搞定图片还不够。很多前端习惯一把梭地在头部引用几百 KB 的全局 CSS 文件,导致浏览器在下载并解析完这些 CSS 之前,坚决不进行任何页面绘制(即所谓的渲染阻塞)。为了解决这个问题,我折腾了一套 Critical CSS(关键路径 CSS)提取方案。

在构建期,我们使用 PostCSS 配合 Puppeteer 模拟无头浏览器跑一遍页面,将处于首屏视口(Viewport)内的 HTML 元素所依赖 of CSS 样式强行提取出来,并直接注入到 HTML 模板的 <style> 标签中。而剩下那一大堆涉及弹窗、页脚、次要模块的非关键 CSS,则通过修改 media="print"onload 回调变成异步加载,等首屏渲染完毕后再静默下载。

做完这两步改造后重新跑 Lighthouse,移动端 LCP 直接从 6.8 秒缩减到了 1.9 秒,评分终于回到了绿色的 96 分。这种细节优化不仅是对技术指标的追求,更直接关系到转化率,毕竟谁也不想在一个白屏转圈圈的网站上多等三秒钟。

你们在优化首屏时,有没有遇到过因为第三方统计脚本抢占带宽导致 preload 失效的坑?欢迎在下面聊聊解决方案。