非新手教程,这里主要列举一些性能优化的关键点。
对于用户来说,首屏加载页面主要体现为 Login 登录页或者各个角色首页等。而构建快速加载网站的第一步是及时从服务器收到页面 HTML 的响应。当你在浏览器的地址栏中输入 URL 时,浏览器会向服务器发送一个 GET 请求以检索它,确保 HTML 快速到达且延迟最少是关键的性能目标。
TTFB#
HTML 的初始请求会经历多个步骤,每个步骤都需要花费一些时间,而减少每个步骤所花费的时间可以缩短首次字节时间 (TTFB)。
虽然 TTFB 不是前端关注页面加载速度时应该关注的唯一指标,但较高的 TTFB 确实会让达到指定 “良好” 阈值变得困难,例如最大内容绘制 (LCP) 和首次内容绘制 (FCP)。由此可以得出一个简单结论是:单单靠前端优化无法将性能做到极致,还需要跟后端进行配合。
大多数网站应努力将 TTFB 控制在 0.8 秒或更短。
Server-Timing
响应头可用于来测量可能导致高延迟的接口。
// Two metrics with descriptions and values
Server-Timing: db;desc="Database";dur=121.3, ssr;desc="Server-side Rendering";dur=212.2
任何带有 Server-Timing
响应标头的页面都可以通过 Navigation Timing API 中的 serverTiming
属性来获取:
// Get the serverTiming entry for the first navigation request:
performance.getEntries("navigation")[0].serverTiming.forEach(entry => {
// Log the server timing data:
console.log(entry.name, entry.description, entry.duration);
});
关于如何优化 TTFB 的方法,可以参考这篇文章:
Optimize Time to First Byte | Articles | web.dev
静态资源响应压缩#
基于静态文件(如 HTML、JavaScript、CSS 和 SVG 图像)响应需要进行压缩,减小其在网络上的传输成本,以便更快地下载它们。当前使用最广泛的压缩算法是 gzip
和 Brotli
,其中 Brotli
比 gzip
提高了约 15% 到 20%。
大多数 CDN 服务提供商通常会自动设置压缩,但如果能够自己配置或调整压缩设置,则尽可能使用 Brotli
。 Brotli
比 gzip
提供了相当明显的改进,并且所有主流浏览器都支持 Brotli
。
尽可能使用 Brotli
,但如果网站本身在旧版浏览器上有大量用户使用,请确保使用 gzip
作为兼容处理,因为任何压缩都比不压缩要好。
CDN#
内容分发网络(CDNs)通过利用一个分布式的服务器网络来向用户分发资源,从而提升网站性能。由于 CDNs 能够减轻服务器的负担,它们能够降低服务器的成本,并且非常适合应对流量突增的情况。
CDN 旨在降低延迟,其通过把资源分发至地理位置离用户更近的服务器来实现这一目标,也正因如此,CDN 的核心优势在于它能够提升加载性能,特别是,在引入 CDN 后,资源的首字节响应时间(Time to First Byte,TTFB)可以得到显著改善,而这对 LCP 指标的提升起到了至关重要的作用。
更多关于使用 CDN 提高网站加载速度可以参考该文章:
Content delivery networks (CDNs) | Articles | web.dev
blocking="render"
- 实验性功能#
作为实验性功能,现在可以将 blocking=render
作为属性及其值添加到 <script>
、<style>
或样式表的 <link>
标签中,从而明确地设定为渲染阻塞。主要用途是为了防止未加样式内容的闪现或者防止用户与一个未完全加载的页面进行交互,这种情况一般是由脚本插入的脚本 / 样式表或客户端的 A/B 测试等引起的。
浏览器兼容情况:
"blocking" | Can I use... Support tables for HTML5, CSS3, etc
目前所有的浏览器都内置了渲染阻塞机制:页面导航后,浏览器在 <head>
中的所有样式表及同步脚本加载和处理完毕之前,不会向屏幕渲染任何像素。这样做可以预防未加样式内容的闪现现象(FOUC),同时确保诸如框架代码等关键脚本得到执行,使得首次渲染周期后,页面功能可正常使用。
https://github.com/whatwg/html/pull/7474
CSS#
在最基础的层面上,CSS 压缩是一个能够有效增强网站性能的优化方法,它能够改善网站的首次内容绘制(First Contentful Paint, FCP),在某些情况下,甚至能改进最大内容绘制(Largest Contentful Paint, LCP)。打包工具(如 Webpack、Vite 等)能在你的生产环境构建中自动执行这些优化。
在渲染页面内容前,浏览器必须下载并解析所有的 CSS 样式表,这一解析过程还包括了那些当前页面上未被用到的样式,这部分样式实际上是不必要的。如果你使用的打包工具会把所有 CSS 合并到一个文件中,这可能导致用户下载的 CSS 超出了渲染当前页面所必需的量。
Chrome DevTools 中的覆盖率工具可用于检测当前页面未使用的 CSS(或 JavaScript)。
JavaScript#
JavaScript 负责大部分网页的交互功能,但这背后是有成本的。
加载过多的 JavaScript 代码会使网页在加载过程中响应缓慢,甚至可能引起交互响应缓慢的问题。
async
和 defer
属性使外部脚本能在不阻塞 HTML 解析器的同时加载,而带 module
类型的脚本(包括内联脚本)会自动延迟加载。不过,理解 async
和 defer
之间的一些关键区别非常重要。
Sourced from https://html.spec.whatwg.org/multipage/scripting.html
使用 async
属性加载的脚本下载完毕后会立即解析和执行,而通过 defer
属性加载的脚本则要等到 HTML 文档解析完毕时才会执行 —— 这与浏览器的 DOMContentLoaded 事件同步发生。同时,async
属性的脚本可能不按顺序执行,而 defer
属性的脚本会依照在页面中出现的顺序依次执行。
此外,JavaScript 的压缩比对其他资源的压缩(例如 CSS)进行得更彻底,在 JavaScript 压缩的过程中,不只是移除了空格、制表符和注释等非代码内容,源代码中的变量和函数名也会被缩短。这个过程有时也被称作 “丑化”(uglification)。
Preconnect
预连接#
通过使用 preconnect
,可以预测到浏览器即将需要连接到一个特定的跨域服务器,并且浏览器应当立刻开启该连接,理想情况下是在 HTML 解析器或预加载扫描器开始工作之前。
preconnect
常用于 Google 字体服务。Google 字体建议预先连接到 https://fonts.googleapis.com 域名,它用于提供 @font-face 声明,同时也建议连接到 https://fonts.gstatic.com 域名,该域名用于提供字体文件。
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
dns-prefetch
dns 预取#
虽然提前打开跨域服务器的连接可以显著加快页面最初的加载时间,但让网页同时建立多个跨域连接可能既不合理也不可行。
如果担心可能在过度使用 preconnect
,一个更节省资源的办法是采用 dns-prefetch
。正如其名称所示,dns-prefetch
并不是建立服务器连接,而是只进行域名的 DNS 解析。
在将域名解析为相对应的 IP 地址的过程中,虽然设备和网络级别的 DNS 缓存能够加速这一过程,但仍然会消耗一些时间。
preload
预加载#
preload
指令用于提前请求页面渲染必须的资源:
<link rel="preload" href="/font.woff2" as="font" crossorigin>
如果 <link>
元素在 preload
指令中没有设置 as
属性,资源会被下载两次。关于各种 as
属性的值,可查阅 MDN 文档中的相关说明。
prefetch
预取#
prefetch
指令用于启动对可能在未来导航中用到的资源的低优先级请求:
<link rel="prefetch" href="/next-page.css" as="style">
在某些情况下,prefetch
可以很有帮助 —— 比如,如果你识别出网站上大多数用户都会遵循的特定用户流程,对这些未来页面的关键资源进行预加载(prefetch
)有助于缩短它们的加载时间。
注意:由于 prefetch
本质上是一种推测性操作,它的一个潜在缺点是,如果用户没有访问到最终需要该预加载资源的页面,那么获取资源所消耗的数据可能就会浪费。你需要依据网站的分析数据或其他使用模式信息来决定是否应用 prefetch
。此外,对于那些设置了减少数据使用偏好的用户,你也可以利用 Save-Data
提示来避免进行预加载操作。
通常建议避免使用 <link rel="prefetch">
预获取跨域文档,有一个关于预获取跨域文档的公开问题,它会导致发起重复请求。 同样,应该避免预获取那些已个性化的同源文档 —— 比如,为认证会话动态生成的 HTML 响应,因为这类资源一般不会被缓存,极可能闲置不用,从而最终浪费了带宽。
在基于 Chromium 的浏览器中,可以利用 Speculation Rules API 来预获取文档,Speculation Rules 定义为 JSON 对象,可以内嵌于页面的 HTML,或通过 JavaScript 动态注入:
<script type="speculationrules">
{
"prefetch": [{
"source": "list",
"urls": ["/page-a", "/page-b"]
}]
}
</script>
像 Quicklink 这类库,通过对用户可视范围内的页面链接进行动态预获取或预渲染,来优化页面导航体验,与预获取页面上所有链接的方式相比,这种方法更能提高用户最终浏览到这些页面的概率。
Prerender
预渲染页面#
除了预获取资源,还可以通过浏览器来提前渲染用户即将访问的页面。这种做法能够实现近乎瞬时的页面加载,因为页面及其资源已经在后台加载并处理好了。当用户访问该页面时,它会立即显示。
预渲染功能可以通过 Speculation Rules API 来实现:
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/page-a", "page-b"]
}
]
}
</script>
Chrome 同样支持使用 <link rel="prerender" href="/page">
这种资源提示方式。不过,从 Chrome 63 开始,这种做法引入了无状态预获取 (NoState Prefetch),仅用于加载页面所需的资源,而不会进行页面渲染或执行 JavaScript。
完整预渲染也会运行预渲染页面中的 JavaScript,鉴于 JavaScript 是一种体积大、计算量高的资源,建议尽可能谨慎地使用 prerender
,并且只有在您确信用户准备访问那个预渲染页面的情况下才使用。
Service Worker 预缓存#
Service worker 的预缓存功能可以借助 Cache API 去获取并存储资源,这使得浏览器能够仅通过 Cache API 响应请求,而无需联网操作。Service worker 预缓存采用了一种非常高效的缓存策略,即所谓的 “仅缓存策略”,这种方式极其高效,资源一旦存入 service worker 缓存,在请求时可以几乎瞬间被取用。
要利用 service worker 对资源进行预缓存,可以采用 Workbox 这个工具。当然,如果更希望手动控制,也完全可以自己编写代码来缓存特定的文件集合。
无论你以哪种方式来实现资源预缓存,都必须明白,这个过程是在 service worker 安装的时候进行的。一旦安装完成,所有预缓存的资源就可以被你网站上任何一个由 service worker 管理的页面调用和使用。
Workbox | Chrome for Developers
像使用资源提示或猜测规则进行资源的预获取或预渲染一样,service worker 的预缓存同样会占用网络带宽、存储空间和 CPU 处理能力。因此,建议只对可能会被使用的资源进行预缓存,避免在预缓存列表中包含过多的资源。当不确定需要预缓存哪些资源时,宁可少缓存一些,而将填充 service worker 缓存的工作交给运行时缓存,并采用多种模式以协调加载速度与资源更新度。要获取关于预缓存资源的更多实践提示和禁忌,请阅读《预缓存的正确与错误之道》。
Fetch Priority API
#
通过 fetchpriority
属性,可以利用 [Fetch Priority API](https://web.dev/articles/fetch-priority)
提升资源的加载优先级。该属性适用于 <link>
、<img>
和 <script>
元素。
<div class="gallery">
<div class="poster">
<img src="img/poster-1.jpg" fetchpriority="high">
</div>
<div class="thumbnails">
<img src="img/thumbnail-2.jpg" fetchpriority="low">
</div>
</div>
图像#
图像通常是网页中体积最大且最常见的资源。因此,优化图像可以显著提高网页的性能。大多数情况下,优化图像意味着减少传输的数据量来缩短网络传输时间,但也可以通过提供适合用户设备大小的图像来优化传输数据量。
现代浏览器支持多种图像文件格式。相比 PNG 或 JPEG,现代图像格式如 WebP 和 AVIF 能提供更好的压缩效果,从而使图像文件体积更小,下载时间更短。使用现代格式服务图像,能够减少资源的加载时间,可能会降低最大内容渲染(Largest Contentful Paint, LCP)。
JavaScript 代码拆分#
加载大型 JavaScript 资源会严重影响页面加载速度,如果把 JavaScript 分割成更小的分块 (chunk),并且在页面启动时只下载对页面功能来说必要的代码,能够显著提升页面的加载响应速度。
当页面下载、解析并编译大型 JavaScript 文件时,可能会出现暂时性的无响应状态,虽然页面元素已经可见,因为它们属于页面最初的 HTML,且已应用 CSS 样式。然而,负责驱动这些互动元素的 JavaScript 以及其他被页面加载的脚本可能正在执行,导致它们功能失常,这导致用户可能感受到交互被明显延迟,或甚至完全不可用。
Lighthouse 会在 JavaScript 执行时间超过 2 秒时显示警告,超过 3.5 秒时则判断为失败。无论在页面生命周期的哪个阶段,过度的 JavaScript 解析和执行都有可能问题,因为这可能会在用户与页面互动时增加输入延迟,尤其是与 JavaScript 处理和执行任务的主线程同步运行时。
此外,过量的 JavaScript 执行和解析在页面初始化加载期间特别问题,因为这时用户非常可能与页面进行互动。实际上,总阻塞时间(Total Blocking Time, TBT)— 一项衡量加载响应性的指标 — 与交互到下一次绘制(INP)高度相关,这意味着用户在页面最初加载时尝试交互的可能性很高。
通过使用动态 import()
函数,可以实现代码分割。这种函数 —— 与在启动时请求特定 JavaScript 资源的 <script>
元素不同 —— 可以在页面生命周期的后期阶段请求 JavaScript 资源。
document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
// Get the form validation named export from the module through destructuring:
const { validateForm } = await import('/validate-form.mjs');
// Validate the form:
validateForm();
}, { once: true });