典型的加载流程
我们可以将典型的加载流程分为两个阶段:
- 从浏览器到服务器(TTFB)
- 从服务器到屏幕(FP/LCP)
接下来分别展开这两个阶段。
从浏览器到服务器
这一阶段主要涵盖浏览器加载网页,并拿到HTML文件的整个流程。
在一次典型的加载流程中,浏览器打网页首先需要通过DNS查找域名对应的IP地址,与服务器建立连接,再通过TLS/SSL握手建立安全连接后才能发起正式请求,这一步我们很难进行优化。
客户端发起请求后,一段时间后才能收到服务端的响应。这期间所花费的时间包括网络数据传输的时延和服务端处理的时延。
网络传输的往返时间可以简单地通过ping估算,一般来讲从上海办公室到杭州阿里云的往返时间为30ms左右。
对于CSR项目,服务端处理时间可以忽略不计,大部分静态服务返回HTML文件仅需要不到1ms。相比之下SSR项目通常会有较长的处理时间,处理时间与页面复杂度和所依赖的接口响应速度有关,一般从10ms~100ms不等。
我们将开始加载到客户端接收服务器返回第一个字节的时间称为TTFB(Time To First Byte)。
忽略不计其余中间环节,所以我们可以简单用如下公示代表TTFB:
TTFB = 链接时间+往返时间+处理时间
由于链接时间难以优化,我们主要关注往返时间和处理时间。往返时间可以通过CDN解决,用户可以通过连接离他最近的CDN服务器来减少往返时间,而处理时间取决于我们选择CSR还是SSR,单纯从这阶段来看,不考虑SSG等优化方案,CSR的TTFB指标一般是优于SSR的。
从服务器到屏幕
这一阶段主要涵盖浏览器接收并解析HTML文件,下载关键资源,并渲染到屏幕的过程。注意实际情况中,以下流程并不完全串行执行。例如解析HTML、加载关键资源实际上是同步进行的。
我们先关注图例的左半部分,接收HTML、解析HTML、加载关键资源、渲染。
关键资源
首先复习一下什么是关键资源。
关键资源是会阻塞浏览器渲染的流程,例如HTML和CSS都会阻塞浏览器渲染。浏览器没有HTML就没法构建DOM。浏览器也需要样式才能正确渲染网页。可以想像一下,如果浏览器一开始展示黑色背景的文字,CSS加载后文字背景变为绿色,用户体验是非常差的。
script标签则会阻塞DOM解析,因为script标签执行后DOM可能发生变化。
所以最坏情况下,HTML加载完成后,浏览器还需要加载JavaScript和CSS才能开始渲染。
这也是为什么我们会把<link>
标签放在<head>
内,而把<script>
标签放在</body>
前。也因此,我们还会给<script>
标签加async
或者defer
的属性来避免脚本阻塞DOM解析。
其他资源如何影响加载性能
关于这个主题我们已经有许多共识,减少资源文件的体积意味着能够缩短资源下载时间,因此我们应该尽可能减少资源文件的体积,常用的方案包括。
- minify JS
- 缓存所有静态资源
- 压缩图片
- 使用TreeShake等技术减少包体积(永远不要直接引入lodash,使用
lodash-esm
或者import xx from 'lodash/xxx
') - 采用分包
- 异步加载组件或者库,例如hightlight.js完全可以等到页面将展示代码时再加载
建议开发者在上线后检查网页所有资源包的总体积,通过bundle可视化等工具查看资源文件是否有可优化的地方。
SSR vs CSR
对于SSR项目,加载关键资源后,无需等待脚本执行即可渲染出页面;对于CSR项目,还需要经过执行脚本、加载数据等环节才能渲染出画面。
这是不是意味着SSR的FP指标一定由于CSR呢?并不一定,因为SSR的TTFB时间显著长于CSR。具体选择哪种技术还是要看项目实际需求。
优化方案
SSR/CSR也并非水火不容,业界探索出了多种能够充分利用双方特点的技术。
例如:
- SSG(Server Side Generate 静态生成),是指在编译时生成所有网页,运行时服务端不必再次渲染,因此TTFB可以降到与CSR一致的水平,也能利用SSR FP快的优势。
- ISR(Incremental Static Regeneration 渐进式静态再生成),如果网站所属的页面特别多,以至于难以在编译时全量生成,可以先生成一部分页面,其余页面在用户第一次访问时生成。用户再次访问时利用stale-while-revalidate的策略先返回用户老页面,同时重新渲染新页面以供下次访问。TTFB也可以降到几乎与CSR一致。
- Prender,是指在编译时利用SSR渲染好一部分页面,通常是骨架图或不变更的页面框架或加载指示器。利用预渲染,CSR项目也能够在关键资源加载完成后立即渲染一部分或全部画面,有效减少用户焦虑。
以上方案又都有所不足,例如SSG的方案不适用于内容会改变的页面,适用于博客、新闻等场景,而ISR存在页面更新不及时的问题。
从FCP到LCP
由于FCP很容易优化,例如利用预渲染在最开始加骨架屏或加载指示器,不能客观代表用户真实体验,Google提出了LCP(Largest Contentful Paint)。
LCP是指,在用户第一次交互前,网页面积最大的内容渲染的时间点。尽管测量方式比FCP要复杂,但是这个指标更趋向于用户的真实体验。更多详细信息可以参考LCP。
排除大面积图片的情况,对于SSR项目LCP通常也是FCP。而对于CSR项目,使用LCP也意味着使用预渲染技术时,CSR不会有领先优势(这并不代表预渲染没有意义,也不代表SSR一定更好)。
总结
如果我们努力优化了TTFB,又通过预渲染、减少资源体积等方案优化LCP,用户能在比较短的时间内打开网页,那么也许他不会离开,继续使用我们的服务。用户体验的加载部分至此结束,但关于用户体验的部分才真正开始。
其他用于优化的技术
Server Push
也许你会注意到,第一阶段到第二阶段分隔比较明显的原因是,客户端需要下载HTML才能知道后续需要下载什么资源。
于是Google提出了Server Push技术,简单来说就是在访问HTML时,服务端可以预先将其他静态资源与HTML一同返回给客户端,这样客户端实际上无需再次发起请求加载静态资源,理论上能够节约大量时间。
遗憾的是这一技术很难也很少被人使用,浏览器也难以实现,Google在2020年的11月发起了从标准中移除Server Push的提案。
Service Worker
Service Worker能够大幅改善后续访问的性能。用户只要访问一次网站,我们就能够在浏览器里注册一个Servce Worker接管后续的网络请求。例如我们可以将首页缓存下来,用户下次访问时无需通过网络,完全消除第一阶段,缓存静态资源也能够加速第二阶段的展示。
Core Web Vitals
Google定义了Web Vitals作为衡量Web用户体验的量化指数,而LCP、FID、CLS三个指数被认为是最重要的Core Web Vitals(核心指标)的参考。
这三个指数的标准如下:
- Largest Contentful Paint (LCP) :最大内容绘制,测量_加载_性能。为了提供良好的用户体验,LCP 应在页面首次开始加载后的2.5 秒内发生。
- First Input Delay (FID) :首次输入延迟,测量_交互性_。为了提供良好的用户体验,页面的 FID 应为100 毫秒或更短。
- Cumulative Layout Shift (CLS) :累积布局偏移,测量_视觉稳定性_。为了提供良好的用户体验,页面的 CLS 应保持在 0.1. 或更少。
为了确保您能够在大部分用户的访问期间达成建议目标值,对于上述每项指标,一个良好的测量阈值为页面加载的第 75 个百分位数,且该阈值同时适用于移动和桌面设备。
如果一个页面满足上述全部三项指标建议目标值的第 75 个百分位数,那么评估核心 Web 指标合规性的工具应评判该页面为通过。
参考
默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。请务必精简您的 CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞。 在 渲染树构建中,我们看到关键渲染路径要求我们同时具有 DOM 和 CSSOM 才能构建渲染树。这会给性能造成严重影响: HTML 和 CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM,我们就没有可渲染的内容,但 CSS 的必要性可能就不太明显。如果我们在 CSS 不阻塞渲染的情况下尝试渲染一个普通网页会怎样? 默认情况下,CSS 被视为阻塞渲染的资源。 我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。 上例展示了纽约时报网站使用和不使用 CSS 的显示效果,它证明了为何要在 CSS 准备就绪之前阻塞渲染,---没有 CSS 的网页实际上无法使用。右侧的情况通常称为"内容样式短暂失效"(FOUC)。浏览器将阻塞渲染,直至 DOM 和 CSSOM 全都准备就绪。 CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。 不过,如果我们有一些 CSS 样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,又该如何?如果这些资源不阻塞渲染,该有多好。 我们可以通过 CSS"媒体类型"和"媒体查询"来解决这类用例: 媒体查询 由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,上面的第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况,也就是说,它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用---或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后,最后一个样式表声明提供由浏览器执行的"媒体查询": 符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。 通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。 声明您的样式表资产时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。 让我们考虑下面这些实例: 第一个声明阻塞渲染,适用于所有情况。 第二个声明同样阻塞渲染: "all"是默认类型,如果您不指定任何类型,则隐式设置为"all"。因此,第一个声明和第二个声明实际上是等效的。 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.
https://developers.google.com