first contentful paint 是什么:怎么量、怎么把首屏那一下提前

first contentful paint 量的是页面上第一块内容被画出来的那一刻,Lighthouse 的及格线是 1.8 秒。这篇讲它到底数什么、哪几个 head 改动能把首屏那一下提前,以及什么时候该改看另一个指标。

改写与发布6 分钟读完1479 次阅读
first contentful paint 是什么:怎么量、怎么把首屏那一下提前

first contentful paint 指的是浏览器把页面上第一块内容画到屏幕上的那一刻——可能是一行字、一张图或一块画布,谁先落下算谁。它之前,屏幕是白的;它之后,访客才知道页面在动。Lighthouse 拿 1.8 秒当及格线来给它打分,它通常也是所有加载指标里人们最先感知到的那一个。

读这篇前

你需要能改到站点的 <head>,最好还能碰服务器或 CDN 的设置。不用构建工具。这篇假设你已经知道「head 里一张样式表为什么会拖住首屏」,如果不清楚,render blocking resources 讲了那套机制。这个指标往上接的是 largest contentful paint;至于真实站点现在都发出去多少东西,34 个首页的 first contentful paint 实测 是这篇的读数版。

first contentful paint 到底在量什么

它量的不是「页面能用了」,而是「页面上有任何东西被画出来了」。所以它既便宜又好被误读。浏览器拿到 HTML 后要先建渲染树,再画下至少一个文字或图片节点;谁挡住建树,谁就把 first contentful paint 往后推。多数站点上,那个挡路的是样式表——布局算不出来,浏览器就不敢画文字。

这个指标的起点是导航开始,不是首字节到达、也不是第一段脚本执行。这一点很关键:一台快服务器配一张阻塞样式表,first contentful paint 经常比一台慢服务器配内联临界 CSS 更差,因为服务器省下的时间全被藏在「等样式表」里了。下表是每个阶段各自管什么、first contentful paint 在等谁。

阶段何时结束是否挡住 FCP
DNS、TCP、TLS连接就绪是,之前什么都起不来
服务器响应HTML 开始到达是
阻塞样式表CSS 取回并解析完是,没有它不画
head 里的同步脚本脚本跑完是,解析会暂停
延迟脚本与图片更晚否
first contentful paint 就是「你把多少东西挡在了第一像素之前」的代价。

由此有两个推论。第一,余地最大的两个抓手是样式表和 head 里的同步脚本,其余要么躲不掉、要么本来就延迟了。第二,因为只要有任何内容出现 first contentful paint 就会触发,一个先画占位块的页面可以拿到很好的 FCP,却仍然迟迟不给读者想看的东西——那才是 largest contentful paint 负责量的。

还有一件常被忽略的事:同一个页面,你本机看到的数和读者手机上看到的数可能差出好几倍。限速移动端档位就是为了把这个差距显式地摆出来,所以前后对照时必须用同一档位,而不是拿本机的暖和网络去跟读者的冷启动比。

按这个顺序做

五步,每步都有一个能先跑一下的检查。第一步免费,它决定后面几步值不值得做。

  1. 先量,再动手。把页面丢进 PageSpeed Insights 或 Lighthouse,记下 FCP 值和它列出的阻塞 URL 清单。完成标志是你手上有一个数、一份清单,而不是一种感觉。
  2. 砍掉阻塞样式表的数量。head 里每一个 <link rel="stylesheet"> 都是一条单独的链。把总是同时加载的几张拼成一张,把跟首屏无关的挪出关键路径——打印样式、轮播的 CSS、某个组件的 CSS。完成标志是报告里的阻塞条数下降了。
  3. 给首屏内联临界 CSS。把给页头和大图用的规则抽出来,放进第一张样式表链接之前的一个 <style> 块,完整样式表放到后面再加载。完成标志是在限速网络下,首屏能从这段内联块渲染出来。
  4. 能延后的就延后。必须跑、但不必在第一帧之前跑的经典脚本,加上 defer;不关键的样式表用 media 切换或「preload 后替换」的方式异步加载。完成标志是 head 里不再有同步脚本。
  5. 复测,并把差值留下来。在同一个连接档位下,跟第一步的数比。完成标志是你能说清改了什么、变了多少,并且两次读数都存了档。

交付物:五行就能审完的一个 head

下表是我们对自己页面用的那套检查。每一行都对应上面阶段表里的某一格,所以一处没过就指向一个具体的修法,而不是一句笼统的「再快一点」。

检查项通过不过时怎么修
head 里的样式表一到两张过多且总是同时加载的,合并
是否内联临界 CSS是首屏在等一张 link
head 里的同步脚本没有有 <script src> 没加 defer
大图是否在 HTML 里是它靠脚本注入
字体是否同源是head 里有第三方字体样式表

如果你要的是原始条数、而不是一个分数,下面这段命令会返回任意 URL 的阻塞样式表和同步脚本数量。它只是一次普通抓取,不需要无头浏览器,所以它跟爬虫看到的一致,也跟浏览器看到的一致。

curl -sL "$URL" | python3 -c '
import sys, re
h = re.search(r"<head[^>]*>(.*?)</head>", sys.stdin.read(), re.S|re.I).group(1)
css = [s for s in re.findall(r"<link\b[^>]*>", h, re.I)
       if re.search(r"rel\s*=\s*[\"\x27]?stylesheet", s, re.I)
       and not re.search(r"media\s*=\s*[\"\x27]?print", s, re.I)]
js  = [s for s in re.findall(r"<script\b[^>]*>", h, re.I)
       if re.search(r"\bsrc\s*=", s, re.I)
       and not re.search(r"\b(defer|async)\b", s, re.I)
       and not re.search(r"type\s*=\s*[\"\x27]?module", s, re.I)]
print("blocking stylesheets:", len(css), "sync scripts:", len(js))'

做错了会怎样

三种翻车最常见,而且都不报错,所以能拖很久才被发现。

  1. 内联了太多 CSS。临界 CSS 是被塞进 HTML 里下发的,没法单独缓存。只内联首屏;内联块涨到大约 14 KB 以上,它省掉的那次请求反而不划算,尤其是二次访问本来就命中了缓存。
  2. 把渲染主内容的脚本也 defer 了。如果大图或商品网格是脚本渲染出来的,把它延后等于把绘制往后推,不是往前拉。要么把那块渲染搬回 HTML,要么承认这一页的 FCP 本质是个脚本指标。
  3. 替换样式表却没有兜底。异步加载的样式表会有一段页面没样式的时间。如果它在替换前闪了一下,你是拿「慢一点的绘制」换来了「肉眼可见的重排」,读者对后者更敏感。

常见问题

first contentful paint 多少算好?

Lighthouse 以 1.8 秒及以内为及格、3 秒以上为差,口径是限速过的移动端档位。把它们当需要越过的线,而不是目标:FCP 很快但 largest contentful paint 很慢的页面,读者依然觉得慢,因为他看到的是个未完的东西。

first contentful paint 会影响排名吗?

它本身不会。它不是三个 Core Web Vitals 之一,也没有任何 Google 文档给它一个排名权重。它的重要性在于——它是访客感知到的第一件事,而能改善它的那些动作(更少的阻塞资源、内联临界 CSS)通常也会改善真正官方的指标。

服务器很快,为什么 first contentful paint 还是很慢?

因为 FCP 等的是渲染树,不是首字节。一次很快的响应,后面跟一张很大的阻塞样式表,出来的 FCP 就是慢的。先看阻塞清单再考虑升级服务器:把一张样式表挪出关键路径,往往比换一台更快的源站划算。

内联临界 CSS 会不会破坏缓存?

会。内联块是 HTML 的一部分,所以用它的每个页面都要重发这些规则,浏览器也没法跨页缓存它们。把块保持小、只覆盖首屏,而完整样式表仍作为正常可缓存的独立文件,紧跟着加载。

边界说清楚:这篇讲的是「让第一块内容更早出现」,不是「让整页更快完成」。我们没有测过单次改动在你的站上会把 FCP 挪动多少,因为它取决于你的标记、CDN 和连接;在热缓存下,差别可能低于 100 毫秒,肉眼根本看不出来。而当读者真正等的是最大的那块元素、不是第一块时,FCP 就已经不是有用的那个数了。把这套动作和 QueryWin 怎么跟踪页面性能 放在一起做是合理的,但第一步的复测要先做。

本文属于 QueryWin 实操手册 · 第 2 阶

first contentful paint 是什么:怎么量、怎么把首屏那一下提前