2021 年前端性能:交付优化
已发表: 2022-03-10本指南得到了我们在 LogRocket 的朋友的大力支持,LogRocket 是一项结合前端性能监控、会话重放和产品分析的服务,可帮助您建立更好的客户体验。 LogRocket跟踪关键指标,包括。 DOM 完成、第一个字节的时间、第一个输入延迟、客户端 CPU 和内存使用情况。 立即免费试用 LogRocket。
目录
- 准备:计划和指标
- 设定切合实际的目标
- 定义环境
- 资产优化
- 构建优化
- 交付优化
- 网络、HTTP/2、HTTP/3
- 测试和监控
- 快速获胜
- 一切都在一页上
- 下载清单(PDF、Apple Pages、MS Word)
- 订阅我们的电子邮件通讯不要错过下一个指南。
交付优化
- 我们是否使用
defer
来异步加载关键的 JavaScript?
当用户请求一个页面时,浏览器获取 HTML 并构造 DOM,然后获取 CSS 并构造 CSSOM,然后通过匹配 DOM 和 CSSOM 生成渲染树。 如果需要解析任何 JavaScript,浏览器在解析之前不会开始渲染页面,从而延迟渲染。 作为开发人员,我们必须明确告诉浏览器不要等待并开始渲染页面。 对脚本执行此操作的方法是使用 HTML 中的defer
和async
属性。在实践中,事实证明最好使用
defer
而不是async
。 啊,又有什么区别呢? 根据 Steve Souders 的说法,一旦async
脚本到达,它们就会立即执行——只要脚本准备好。 如果这种情况发生得非常快,例如当脚本处于缓存中时,它实际上会阻塞 HTML 解析器。 使用defer
,浏览器在解析 HTML 之前不会执行脚本。 因此,除非您需要在开始渲染之前执行 JavaScript,否则最好使用defer
。 此外,多个异步文件将以不确定的顺序执行。值得注意的是,关于
async
和defer
存在一些误解。 最重要的是,async
并不意味着只要脚本准备好,代码就会运行。 这意味着只要脚本准备好并且所有先前的同步工作完成,它将运行。 用 Harry Roberts 的话来说,“如果你在同步脚本之后放置一个async
脚本,那么你的async
脚本只会和最慢的同步脚本一样快。”此外,不建议同时使用
async
和defer
。 现代浏览器同时支持这两种属性,但只要同时使用这两种属性,async
总是会胜出的。如果您想深入了解更多细节,Milica Mihajlija 编写了一份关于更快构建 DOM 的非常详细的指南,详细介绍了推测解析、异步和延迟。
- 使用 IntersectionObserver 和优先级提示延迟加载昂贵的组件。
一般来说,建议延迟加载所有昂贵的组件,例如繁重的 JavaScript、视频、iframe、小部件和潜在的图像。 本机延迟加载已经可用于具有loading
属性的图像和 iframe(仅限 Chromium)。 在幕后,此属性延迟资源的加载,直到它到达与视口的计算距离。<!-- Lazy loading for images, iframes, scripts. Probably for images outside of the viewport. --> <img loading="lazy" ... /> <iframe loading="lazy" ... /> <!-- Prompt an early download of an asset. For critical images, eg hero images. --> <img loading="eager" ... /> <iframe loading="eager" ... />
该阈值取决于几件事,从要获取的图像资源的类型到有效的连接类型。 但在 Android 上使用 Chrome 进行的实验表明,在 4G 上,97.5% 的延迟加载的首屏图像在可见后 10 毫秒内完全加载,因此应该是安全的。
我们还可以在
<script>
、<img>
或<link>
元素(仅闪烁)上使用importance
属性(high
或low
)。 事实上,这是一种在轮播中取消图像优先级以及重新确定脚本优先级的好方法。 但是,有时我们可能需要更精细的控制。<!-- When the browser assigns "High" priority to an image, but we don't actually want that. --> <img src="less-important-image.svg" importance="low" ... /> <!-- We want to initiate an early fetch for a resource, but also deprioritize it. --> <link rel="preload" importance="low" href="/script.js" as="script" />
执行稍微复杂一点的延迟加载的最高效方法是使用 Intersection Observer API,它提供了一种异步观察目标元素与祖先元素或顶级文档视口的交集变化的方法。 基本上,您需要创建一个新的
IntersectionObserver
对象,该对象接收一个回调函数和一组选项。 然后我们添加一个目标来观察。回调函数在目标变得可见或不可见时执行,因此当它拦截视口时,您可以在元素变得可见之前开始执行一些操作。 事实上,我们可以通过
rootMargin
(根周围的边距)和threshold
(单个数字或数字数组,指示我们瞄准的目标可见性的百分比)对何时调用观察者的回调进行精细控制。Alejandro Garcia Anglada 发布了一个关于如何实际实现它的方便教程,Rahul Nanwani 写了一篇关于延迟加载前景和背景图像的详细文章,Google Fundamentals 也提供了关于延迟加载图像和视频的详细教程,以及 Intersection Observer。
还记得带有移动和粘性物体的艺术指导的长篇故事吗? 您也可以使用 Intersection Observer 实现高性能的滚动显示。
再次检查您可以延迟加载的其他内容。 甚至延迟加载翻译字符串和表情符号也会有所帮助。 通过这样做,Mobile Twitter 成功地将新的国际化管道的 JavaScript 执行速度提高了 80%。
一个简短的警告:值得注意的是延迟加载应该是一个例外而不是规则。 延迟加载您真正希望人们快速看到的任何内容可能是不合理的,例如产品页面图像、英雄图像或主导航变为交互所需的脚本。
- 逐步加载图像。
您甚至可以通过向页面添加渐进式图像加载来将延迟加载提升到一个新的水平。 与 Facebook、Pinterest、Medium 和 Wolt 类似,您可以先加载低质量甚至模糊的图像,然后随着页面继续加载,使用 BlurHash 技术或 LQIP(低质量图像占位符)将它们替换为完整质量版本技术。如果这些技术是否改善了用户体验,意见会有所不同,但它肯定会缩短首次内容绘制的时间。 我们甚至可以通过使用 SQIP 来自动化它,该 SQIP 创建一个低质量版本的图像作为 SVG 占位符,或带有 CSS 线性渐变的渐变图像占位符。
这些占位符可以嵌入到 HTML 中,因为它们自然可以很好地使用文本压缩方法进行压缩。 在他的文章中,Dean Hume 描述了如何使用 Intersection Observer 来实现这种技术。
倒退? 如果浏览器不支持交叉点观察器,我们仍然可以延迟加载 polyfill 或立即加载图像。 甚至还有一个图书馆。
想要更高级? 您可以跟踪图像并使用原始形状和边缘来创建轻量级 SVG 占位符,首先加载它,然后从占位符矢量图像过渡到(加载的)位图图像。
- 您是否延迟渲染
content-visibility
?
对于包含大量内容块、图像和视频的复杂布局,解码数据和渲染像素可能是一项相当昂贵的操作——尤其是在低端设备上。 使用content-visibility: auto
,我们可以在容器位于视口之外时提示浏览器跳过子项的布局。例如,您可能会在初始加载时跳过页脚和后期部分的呈现:
footer { content-visibility: auto; contain-intrinsic-size: 1000px; /* 1000px is an estimated height for sections that are not rendered yet. */ }
注意content-visibility: auto; 表现得像溢出:隐藏; ,但您可以通过应用
padding-left
和padding-right
而不是默认的margin-left: auto;
,margin-right: auto;
和声明的宽度。 填充基本上允许元素溢出内容框并进入填充框,而不会离开整个盒子模型并被切断。另外,请记住,当最终呈现新内容时,您可能会引入一些 CLS,因此最好将
contain-intrinsic-size
与适当大小的占位符一起使用(谢谢,Una! )。Thijs Terluin 有更多关于这两个属性以及浏览器如何计算 contains
contain-intrinsic-size
的详细信息,Malte Ubl 展示了如何计算它,Jake 和 Surma 的简短视频解释器解释了它是如何工作的。如果您需要更细化,使用 CSS 包含,如果您只需要其他元素的大小、对齐或计算样式,您可以手动跳过 DOM 节点后代的布局、样式和绘制工作——或者该元素当前是画布外。
- 您是否使用
decoding="async"
延迟解码?
有时内容会出现在屏幕外,但我们希望确保它在客户需要时可用——理想情况下,不阻塞关键路径中的任何内容,而是异步解码和渲染。 我们可以使用decodedecoding="async"
来授予浏览器从主线程解码图像的权限,避免用户对用于解码图像的CPU时间的影响(通过Malte Ubl):<img decoding="async" … />
或者,对于屏幕外的图像,我们可以先显示一个占位符,当图像在视口内时,使用 IntersectionObserver 触发网络调用以在后台下载图像。 此外,如果 Image Decode API 不可用,我们可以将渲染推迟到使用 img.decode() 解码或下载图像。
例如,在渲染图像时,我们可以使用淡入动画。 Katie Hempenius 和 Addy Osmani 在他们的演讲 Speed at Scale: Web Performance Tips and Tricks from the Trenches 中分享了更多见解。
- 您是否生成并提供关键的 CSS?
为了确保浏览器尽快开始呈现您的页面,收集开始呈现页面第一个可见部分所需的所有 CSS(称为“关键 CSS”或“首屏 CSS”已成为一种常见做法") 并将其包含在页面的<head>
中,从而减少往返。 由于在慢启动阶段交换的包的大小有限,关键 CSS 的预算约为 14KB。如果超出此范围,浏览器将需要额外的往返来获取更多样式。 CriticalCSS 和 Critical 使您能够为您正在使用的每个模板输出关键 CSS。 不过,根据我们的经验,没有任何自动系统比为每个模板手动收集关键 CSS 更好,而这确实是我们最近重新采用的方法。
然后,您可以使用 critters Webpack 插件内联关键 CSS 并延迟加载其余部分。 如果可能,请考虑使用 Filament Group 使用的条件内联方法,或动态将内联代码转换为静态资源。
如果您当前使用诸如 loadCSS 之类的库异步加载完整的 CSS ,则实际上没有必要。 使用
media="print"
,您可以欺骗浏览器异步获取 CSS,但在加载后应用到屏幕环境。 (谢谢,斯科特! )<!-- Via Scott Jehl. https://www.filamentgroup.com/lab/load-css-simpler/ --> <!-- Load CSS asynchronously, with low priority --> <link rel="stylesheet" href="full.css" media="print" onload="this.media='all'" />
在收集每个模板的所有关键 CSS 时,通常只探索“首屏”区域。 但是,对于复杂的布局,最好将布局的基础也包括在内,以避免大量的重新计算和重新绘制成本,从而损害您的 Core Web Vitals 分数。
如果用户得到一个直接链接到页面中间的 URL 但 CSS 尚未下载怎么办? 在这种情况下,隐藏非关键内容变得很常见,例如
opacity: 0;
内联 CSS 和opacity: 1
在完整的 CSS 文件中,并在 CSS 可用时显示。 但它有一个主要缺点,因为连接速度较慢的用户可能永远无法阅读页面的内容。 这就是为什么最好始终保持内容可见,即使它的样式可能不正确。由于缓存,将关键 CSS(和其他重要资产)放在根域上的单独文件中具有好处,有时甚至超过内联。 Chrome 在请求页面时会推测性地打开到根域的第二个 HTTP 连接,这样就不需要 TCP 连接来获取此 CSS。 这意味着您可以创建一组关键的-CSS 文件(例如, critical-homepage.css 、 critical-product-page.css等)并从您的根目录提供它们,而无需内联它们。 (谢谢,菲利普! )
需要注意的是:使用 HTTP/2,关键 CSS 可以存储在单独的 CSS 文件中,并通过服务器推送交付,而不会使 HTML 膨胀。 问题是服务器推送对于跨浏览器的许多陷阱和竞争条件很麻烦。 它从来没有得到一致的支持,并且存在一些缓存问题(参见 Hooman Beheshti 演示文稿的幻灯片 114)。
事实上,这种影响可能是负面的,并且会使网络缓冲区膨胀,从而阻止传递文档中的真实帧。 因此,Chrome 计划暂时取消对 Server Push 的支持也就不足为奇了。
- 尝试重新组合 CSS 规则。
我们已经习惯了关键的 CSS,但还有一些优化可以超越这一点。 哈里·罗伯茨(Harry Roberts)进行了一项非凡的研究,结果令人惊讶。 例如,将主 CSS 文件拆分为单独的媒体查询可能是个好主意。 这样,浏览器将检索具有高优先级的关键 CSS,并以低优先级检索其他所有内容——完全脱离关键路径。另外,避免将
<link rel="stylesheet" />
放在async
片段之前。 如果脚本不依赖于样式表,请考虑将阻塞脚本置于阻塞样式之上。 如果是这样,则将该 JavaScript 分成两部分并将其加载到 CSS 的任一侧。Scott Jehl 通过使用 service worker 缓存内联 CSS 文件解决了另一个有趣的问题,如果您使用关键 CSS,这是一个常见的问题。 基本上,我们在
style
元素上添加一个 ID 属性,以便使用 JavaScript 轻松找到它,然后一小段 JavaScript 找到该 CSS 并使用 Cache API 将其存储在本地浏览器缓存中(内容类型为text/css
) 用于后续页面。 为了避免在后续页面上内联,而是在外部引用缓存的资产,我们在第一次访问网站时设置了一个 cookie。 瞧!值得注意的是,动态样式也可能很昂贵,但通常仅在您依赖数百个同时呈现的组合组件的情况下。 因此,如果您使用 CSS-in-JS,请确保您的 CSS-in-JS 库在您的 CSS 不依赖主题或道具时优化执行,并且不要过度组合样式化的组件。 Aggelos Arvanitakis 分享了关于 CSS-in-JS 的性能成本的更多见解。
- 您是否流式传输响应?
流经常被遗忘和忽视,流提供了一个用于读取或写入异步数据块的接口,在任何给定时间,内存中可能只有其中的一个子集可用。 基本上,它们允许发出原始请求的页面在第一块数据可用时立即开始处理响应,并使用针对流式传输优化的解析器来逐步显示内容。我们可以从多个来源创建一个流。 例如,您可以让 service worker构建一个流,其中 shell 来自缓存,而主体来自网络,而不是提供一个空的 UI shell 并让 JavaScript 填充它。 正如 Jeff Posnick 所指出的,如果您的 Web 应用程序由 CMS 提供支持,该 CMS 通过将部分模板拼接在一起来呈现 HTML,该模型直接转换为使用流式响应,模板逻辑复制到服务工作者而不是您的服务器中。 Jake Archibald 的 The Year of Web Streams 文章重点介绍了如何构建它。 性能提升非常明显。
流式传输整个 HTML 响应的一个重要优势是,在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。 在页面加载后插入到文档中的 HTML 块(这在通过 JavaScript 填充的内容很常见)不能利用这种优化。
浏览器支持? Chrome、Firefox、Safari 和 Edge 的部分支持仍然支持 API 和所有现代浏览器都支持的 Service Worker。 如果您再次感到冒险,您可以检查流式请求的实验性实现,它允许您在仍然生成正文的同时开始发送请求。 在 Chrome 85 中可用。
- 考虑使您的组件具有连接意识。
数据可能很昂贵,并且随着负载的增加,我们需要尊重在访问我们的网站或应用程序时选择节省数据的用户。 Save-Data 客户端提示请求标头允许我们为成本和性能受限的用户定制应用程序和有效负载。实际上,您可以将对高 DPI 图像的请求重写为低 DPI 图像、删除 Web 字体、花哨的视差效果、预览缩略图和无限滚动、关闭视频自动播放、服务器推送、减少显示项目的数量和降低图像质量,或者甚至改变你提供标记的方式。 Tim Vereecke 发表了一篇关于 data-s(h)aver 策略的非常详细的文章,其中包含许多数据保存选项。
谁在使用
save-data
,您可能想知道? 全球 18% 的 Android Chrome 用户启用了精简模式(启用Save-Data
),而且这个数字可能会更高。 根据 Simon Hearne 的研究,廉价设备的选择加入率最高,但也有很多异常值。 例如:加拿大用户的选择加入率超过 34%(相比之下,美国约为 7%),三星最新旗舰产品的全球用户选择加入率接近 18%。启用
Save-Data
模式后,Chrome Mobile 将提供优化的体验,即具有延迟脚本、强制font-display: swap
和强制延迟加载的代理网络体验。 自己构建体验比依赖浏览器进行这些优化更明智。当前仅在 Chromium、Android 版本的 Chrome 或桌面设备上的数据保护程序扩展中支持该标头。 最后,您还可以使用网络信息 API 来根据网络类型交付昂贵的 JavaScript 模块、高分辨率图像和视频。 网络信息 API,特别是
navigator.connection.effectiveType
使用RTT
、downlink
、effectiveType
值(以及其他一些值)来提供用户可以处理的连接和数据的表示。在这种情况下,Max Bock 谈到了连接感知组件,而 Addy Osmani 谈到了自适应模块服务。 例如,使用 React,我们可以编写一个针对不同连接类型呈现不同的组件。 正如 Max 所建议的,新闻文章中的
<Media />
组件可能会输出:-
Offline
:带有alt
文字的占位符, -
2G
/save-data
模式:低分辨率图像, - 非 Retina 屏幕上的
3G
:中等分辨率图像, - Retina 屏幕上的
3G
:高分辨率 Retina 图像, -
4G
:高清视频。
Dean Hume 使用服务工作者提供了类似逻辑的实际实现。 对于视频,我们可以默认显示视频海报,然后在更好的连接上显示“播放”图标以及视频播放器外壳、视频元数据等。 作为不支持浏览器的后备方案,我们可以监听
canplaythrough
事件并使用Promise.race()
如果canplaythrough
事件在 2 秒内未触发,则使源加载超时。如果您想更深入地研究,这里有一些资源可以开始:
- Addy Osmani 展示了如何在 React 中实现自适应服务。
- React Adaptive Loading Hooks & Utilities 提供了 React 的代码片段,
- Netanel Basel 探索 Angular 中的连接感知组件,
- Theodore Vorilas 分享了在 Vue 中使用网络信息 API 服务自适应组件的工作原理。
- Umar Hansa 展示了如何选择性地下载/执行昂贵的 JavaScript。
-
- 考虑让您的组件具有设备内存感知能力。
但是,网络连接只为我们提供了一种关于用户上下文的视角。 更进一步,您还可以使用设备内存 API 根据可用设备内存动态调整资源。navigator.deviceMemory
返回设备有多少 RAM(以 GB 为单位),向下舍入到最接近的 2 次幂。 该 API 还具有客户端提示标头Device-Memory
,它报告相同的值。奖励: Umar Hansa 展示了如何通过动态导入推迟昂贵的脚本,以根据设备内存、网络连接和硬件并发性来改变体验。
- 预热连接以加快交付速度。
使用资源提示来节省dns-prefetch
(在后台执行 DNS 查找)、preconnect
(要求浏览器在后台启动连接握手(DNS、TCP、TLS))、prefetch
(要求浏览器请求资源)和preload
(预取资源而不执行资源等)。 在现代浏览器中得到很好的支持,很快就会支持 Firefox。还记得
prerender
吗? 用于提示浏览器在后台构建整个页面以进行下一次导航的资源提示。 实施问题非常成问题,从巨大的内存占用和带宽使用到多次注册的分析点击和广告印象。不出所料,它已被弃用,但 Chrome 团队已将其作为 NoState Prefetch 机制恢复。 事实上,Chrome 将
prerender
提示视为 NoState Prefetch,因此我们今天仍然可以使用它。 正如 Katie Hempenius 在那篇文章中解释的那样,“与预渲染一样, NoState Prefetch 会提前获取资源;但与预渲染不同的是,它不执行 JavaScript或提前渲染页面的任何部分。”NoState Prefetch 仅使用约 45MiB 的内存,并且提取的子资源将以
IDLE
Net Priority 进行提取。 从 Chrome 69 开始,NoState Prefetch 添加了 header目的:Prefetch到所有请求,以使它们与正常浏览区分开来。此外,请注意预渲染替代方案和门户,这是对隐私意识预渲染的一项新努力,它将为无缝导航提供内容的嵌入
preview
。使用资源提示可能是提高性能的最简单方法,而且确实效果很好。 什么时候用什么? 正如 Addy Osmani 所解释的,预加载我们知道很可能在当前页面上使用的资源以及未来跨多个导航边界的导航是合理的,例如用户尚未访问的页面所需的 Webpack 包。
Addy 关于“在 Chrome 中加载优先级”的文章展示了 Chrome 如何准确地解释资源提示,因此一旦您确定了哪些资产对渲染至关重要,您就可以为它们分配高优先级。 要查看您的请求的优先级,您可以在 Chrome DevTools 网络请求表(以及 Safari)中启用“优先级”列。
这些天的大部分时间,我们至少会使用
preconnect
和dns-prefetch
,并且我们会谨慎使用prefetch
、preload
和prerender
。 请注意,即使使用preconnect
和dns-prefetch
,浏览器也会限制并行查找/连接的主机数量,因此可以安全地根据优先级对它们进行排序(感谢 Philip Tellis! )。由于字体通常是页面上的重要资产,有时请求浏览器下载带有
preload
的关键字体是个好主意。 但是,请仔细检查它是否真的有助于性能,因为在预加载字体时存在优先级难题:由于preload
被视为非常重要,它可以超越更关键的资源,例如关键的 CSS。 (谢谢,巴里! )<!-- Loading two rendering-critical fonts, but not all their weights. --> <!-- crossorigin="anonymous" is required due to CORS. Without it, preloaded fonts will be ignored. https://github.com/w3c/preload/issues/32 via https://twitter.com/iamakulov/status/1275790151642423303 --> <link rel="preload" as="font" href="Elena-Regular.woff2" type="font/woff2" crossorigin="anonymous" media="only screen and (min-width: 48rem)" /> <link rel="preload" as="font" href="Mija-Bold.woff2" type="font/woff2" crossorigin="anonymous" media="only screen and (min-width: 48rem)" />
<!-- Loading two rendering-critical fonts, but not all their weights. --> <!-- crossorigin="anonymous" is required due to CORS. Without it, preloaded fonts will be ignored. https://github.com/w3c/preload/issues/32 via https://twitter.com/iamakulov/status/1275790151642423303 --> <link rel="preload" as="font" href="Elena-Regular.woff2" type="font/woff2" crossorigin="anonymous" media="only screen and (min-width: 48rem)" /> <link rel="preload" as="font" href="Mija-Bold.woff2" type="font/woff2" crossorigin="anonymous" media="only screen and (min-width: 48rem)" />
由于
<link rel="preload">
接受media
属性,您可以选择根据@media
查询规则有选择地下载资源,如上所示。此外,我们可以使用
imagesrcset
和imagesizes
属性更快地预加载后期发现的英雄图像,或者通过 JavaScript 加载的任何图像,例如电影海报:<!-- Addy Osmani. https://addyosmani.com/blog/preload-hero-images/ --> <link rel="preload" as="image" href="poster.jpg" image image>
<!-- Addy Osmani. https://addyosmani.com/blog/preload-hero-images/ --> <link rel="preload" as="image" href="poster.jpg" image image>
我们还可以将 JSON 预加载为fetch ,以便在 JavaScript 请求它之前发现它:
<!-- Addy Osmani. https://addyosmani.com/blog/preload-hero-images/ --> <link rel="preload" as="fetch" href="foo.com/api/movies.json" crossorigin>
我们还可以动态加载 JavaScript,有效地延迟执行脚本。
/* Adding a preload hint to the head */ var preload = document.createElement("link"); link.href = "myscript.js"; link.rel = "preload"; link.as = "script"; document.head.appendChild(link); /* Injecting a script when we want it to execute */ var script = document.createElement("script"); script.src = "myscript.js"; document.body.appendChild(script);
需要记住的一些问题:
preload
有助于将资产的开始下载时间移到更接近初始请求的位置,但预加载的资产会落在与发出请求的页面相关的内存缓存中。preload
与 HTTP 缓存配合得很好:如果项目已经在 HTTP 缓存中,则永远不会发送网络请求。因此,它对于后期发现的资源、通过
background-image
加载的英雄图像、内联关键 CSS(或 JavaScript)以及预加载其余的 CSS(或 JavaScript)很有用。只有在浏览器从服务器接收到 HTML 并且前瞻解析器找到了
preload
标签后,preload
标签才能启动预加载。 通过 HTTP 标头预加载可能会更快一些,因为我们不需要等待浏览器解析 HTML 来启动请求(尽管有争议)。Early Hints 将进一步提供帮助,甚至可以在发送 HTML 的响应标头之前启动预加载(在 Chromium、Firefox 的路线图上)。 另外,优先级提示将帮助我们指示脚本的加载优先级。
注意:如果您使用
preload
as
必须定义或不加载任何内容,加上没有crossorigin
属性的预加载字体将双重获取。 如果您使用的是prefetch
,请注意 Firefox 中的Age
标头问题。
- 使用服务工作者进行缓存和网络回退。
网络上的任何性能优化都不会比用户机器上本地存储的缓存更快(但也有例外)。 如果您的网站通过 HTTPS 运行,我们可以在 service worker 缓存中缓存静态资产并存储离线回退(甚至离线页面)并从用户的机器上检索它们,而不是通过网络。正如 Phil Walton 所建议的,对于服务工作者,我们可以通过以编程方式生成响应来发送更小的 HTML 有效负载。 服务工作者可以从服务器请求它需要的最少的数据(例如 HTML 内容部分、Markdown 文件、JSON 数据等),然后它可以以编程方式将该数据转换为完整的 HTML 文档。 因此,一旦用户访问站点并安装了 service worker,用户将永远不会再次请求完整的 HTML 页面。 性能影响可能相当可观。
浏览器支持? 服务工作者受到广泛支持,无论如何,后备是网络。 它有助于提高性能吗? 哦,是的,确实如此。 而且它正在变得越来越好,例如 Background Fetch 也允许通过 service worker 进行后台上传/下载。
服务工作者有许多用例。 例如,您可以实现“离线保存”功能、处理损坏的图像、在选项卡之间引入消息传递或根据请求类型提供不同的缓存策略。 一般来说,一个常见的可靠策略是将应用程序外壳与一些关键页面一起存储在服务工作者的缓存中,例如离线页面、首页以及对您的情况可能很重要的任何其他内容。
不过,有一些问题需要牢记。 有了服务工作者,我们需要注意 Safari 中的范围请求(如果您将 Workbox 用于服务工作者,它有一个范围请求模块)。 如果你曾经偶然发现
DOMException: Quota exceeded.
浏览器控制台出错,然后查看 Gerardo 的文章When 7KB equals 7MB。正如 Gerardo 所写,“如果您正在构建一个渐进式 Web 应用程序,并且当您的 Service Worker 缓存从 CDN 提供的静态资产时,您遇到了膨胀的缓存存储,请确保跨域资源存在正确的 CORS 响应标头,您不要缓存不透明的响应与您的服务人员无意中,您通过将
crossorigin
属性添加到<img>
标记来选择跨域图像资产进入 CORS 模式。”有很多很棒的资源可以帮助您开始使用服务人员:
- Service Worker Mindset,它可以帮助您了解 Service Worker 如何在幕后工作以及在构建服务人员时要了解的内容。
- Chris Ferdinandi 提供了一系列关于服务工作者的精彩文章,解释了如何创建离线应用程序并涵盖了各种场景,从离线保存最近查看的页面到为服务工作者缓存中的项目设置过期日期。
- Service Worker 陷阱和最佳实践,包括一些关于范围、延迟注册 Service Worker 和 Service Worker 缓存的提示。
- Ire Aderinokun 与 Service Worker 的“离线优先”系列,其中包含预缓存应用程序外壳的策略。
- Service Worker:介绍如何使用 Service Worker 获得丰富的离线体验、定期后台同步和推送通知的实用技巧。
- 总是值得参考优秀的 ol' Jake Archibald 的离线食谱,其中包含许多关于如何烘焙自己的服务人员的食谱。
- Workbox 是一组专门为构建渐进式 Web 应用程序而构建的服务工作者库。
- 您是否在 CDN/Edge 上运行服务器工作者,例如用于 A/B 测试?
在这一点上,我们已经习惯了在客户端运行服务工作者,但是通过在服务器上实现它们的 CDN,我们也可以使用它们来调整边缘的性能。例如,在 A/B 测试中,当 HTML 需要为不同的用户改变其内容时,我们可以使用 CDN 服务器上的 Service Worker 来处理逻辑。 我们还可以流式传输 HTML 重写以加速使用 Google 字体的网站。
- 优化渲染性能。
每当应用程序运行缓慢时,都会立即引起注意。 因此,我们需要确保在滚动页面或动画元素时没有延迟,并且您始终保持每秒 60 帧的速度。 如果这不可能,那么至少使每秒帧数保持一致比 60 到 15 的混合范围更可取。使用 CSS 的will-change
通知浏览器哪些元素和属性将发生变化。每当您遇到时,请在 DevTools 中调试不必要的重绘:
- 测量运行时渲染性能。 查看一些有关如何理解它的有用提示。
- 要开始使用,请查看 Paul Lewis 关于浏览器渲染优化的免费 Udacity 课程和 Georgy Marchuk 关于浏览器绘画和 Web 性能考虑的文章。
- 在 Firefox DevTools 的“更多工具 → 渲染 → 画图闪烁”中启用画图闪烁。
- 在 React DevTools 中,选中“突出显示更新”并启用“记录每个组件呈现的原因”,
- 您还可以使用Why Did You Render,因此当重新渲染组件时,Flash 会通知您更改。
您使用的是砌体布局吗? 请记住,很快就可以单独使用 CSS 网格构建 Masonry 布局。
如果您想深入探讨该主题,Nolan Lawson 在他的文章中分享了准确测量布局性能的技巧,Jason Miller 也提出了替代技术。 我们还有一篇 Sergey Chikuyonok 撰写的关于如何正确设置 GPU 动画的文章。
注意:对 GPU 合成层的更改是最便宜的,因此如果您可以通过
opacity
和transform
仅触发合成来摆脱困境,那么您将走在正确的轨道上。 Anna Migas 在她关于调试 UI 渲染性能的演讲中也提供了很多实用的建议。 要了解如何在 DevTools 中调试绘制性能,请查看 Umar 的绘制性能审核视频。 - 您是否针对感知性能进行了优化?
虽然组件出现在页面上的顺序,以及我们如何为浏览器提供资源的策略很重要,但我们也不应该低估感知性能的作用。 这个概念涉及等待的心理方面,基本上是让客户在其他事情发生时保持忙碌或参与。 这就是感知管理、抢先开始、提前完成和容忍管理发挥作用的地方。这是什么意思呢? 在加载资产时,我们可以尝试始终领先于客户一步,因此在后台发生很多事情的同时,体验感觉很快。 为了保持客户的参与度,我们可以测试骨架屏幕(实现演示)而不是加载指示器,添加过渡/动画,并且基本上在没有什么可以优化的情况下欺骗用户体验。
在他们关于 UI 骨架艺术的案例研究中,Kumar McMillan 分享了一些关于如何模拟动态列表、文本和最终屏幕的想法和技术,以及如何使用 React 考虑骨架思维。
但请注意:在部署之前应该测试骨架屏幕,因为一些测试表明骨架屏幕在所有指标上都表现最差。
- 您是否防止布局变化和重绘?
在感知性能领域,可能更具破坏性的体验之一是布局转换或重排,这是由重新缩放的图像和视频、网络字体、注入的广告或用实际内容填充组件的后期发现的脚本引起的。 结果,客户可能开始阅读一篇文章,只是被阅读区域上方的布局跳转打断。 这种体验通常是突然的并且非常令人迷惑:这可能是加载需要重新考虑的优先级的情况。社区已经开发了一些技术和解决方法来避免回流。 一般来说,避免在现有内容之上插入新内容是个好主意,除非它是为了响应用户交互而发生的。 始终在图像上设置宽度和高度属性,因此现代浏览器默认分配框并保留空间(Firefox、Chrome)。
对于图像或视频,我们可以使用 SVG 占位符来保留媒体将出现的显示框。这意味着当您需要保持其纵横比时,该区域将被正确保留。 我们还可以为广告和动态内容使用占位符或后备图像,以及预分配布局插槽。
仅在不支持本地延迟加载时才加载外部脚本时,考虑使用本地延迟加载或混合延迟加载,而不是使用外部脚本延迟加载图像。
如上所述,始终将 Web 字体重绘和从所有后备字体转换为所有Web 字体一次进行分组——只需确保该切换不会太突然,通过使用 font-style-matcher 调整字体之间的行高和间距.
要覆盖备用字体的字体指标以模拟 Web 字体,我们可以使用 @font-face 描述符来覆盖字体指标(演示,在 Chrome 87 中启用)。 (请注意,调整会因复杂的字体堆栈而变得复杂。)
对于后期 CSS,我们可以确保布局关键的 CSS 内联在每个模板的标题中。 更进一步:对于长页面,当添加垂直滚动条时,它会将主要内容向左移动 16px。 为了尽早显示滚动条,我们可以在
html
上添加overflow-y: scroll
以在第一次绘制时强制滚动条。 后者有帮助,因为当宽度改变时,由于折叠内容重排,滚动条会导致非平凡的布局变化。 不过,应该主要发生在具有非覆盖滚动条的平台上,例如 Windows。 但是:中断position: sticky
,因为这些元素永远不会滚动出容器。如果您处理在滚动时固定或粘在页面顶部的页眉,请在页眉松动时为页眉保留空间,例如在内容上使用占位符元素或
margin-top
。 一个例外应该是不应该对 CLS 产生影响的 cookie 同意横幅,但有时他们会这样做:这取决于实施。 在这个 Twitter 线程中有一些有趣的策略和要点。对于可能包含大量文本的选项卡组件,您可以使用 CSS 网格堆栈来防止布局变化。 通过将每个选项卡的内容放在同一个网格区域,并一次隐藏其中一个,我们可以确保容器始终采用较大元素的高度,因此不会发生布局偏移。
啊,当然,如果列表下方有内容(例如页脚),无限滚动和“加载更多”也会导致布局变化。 要改进 CLS,请在用户滚动到页面的该部分之前为将要加载的内容保留足够的空间,删除页脚或页面底部可能因内容加载而被压下的任何 DOM 元素。此外,为首屏内容预取数据和图像,这样当用户滚动到那么远时,它就已经存在了。 您也可以使用 react-window 之类的列表虚拟化库来优化长列表(感谢 Addy Osmani! )。
为确保包含回流的影响,请使用 Layout Instability API 测量布局稳定性。 有了它,您可以计算 Cumulative Layout Shift ( CLS ) 分数并将其作为测试要求包含在内,因此每当出现回归时,您都可以对其进行跟踪和修复。
为了计算布局偏移分数,浏览器查看视口大小和两个渲染帧之间的视口中不稳定元素的移动。 理想情况下,分数将接近
0
。 Milica Mihajlija 和 Philip Walton 就 CLS 是什么以及如何测量它提供了一个很棒的指南。 这是衡量和维护感知性能并避免中断的良好起点,尤其是对于关键业务任务。快速提示:要了解导致 DevTools 中布局变化的原因,您可以在性能面板的“体验”下探索布局变化。
奖励:如果您想减少重排和重绘,请查看 Charis Theodoulou 的《最小化 DOM 重排/布局抖动指南》和 Paul Irish 的强制布局/重排的列表以及 CSSTriggers.com,这是一个关于触发布局、绘制的 CSS 属性的参考表和合成。
目录
- 准备:计划和指标
- 设定切合实际的目标
- 定义环境
- 资产优化
- 构建优化
- 交付优化
- 网络、HTTP/2、HTTP/3
- 测试和监控
- 快速获胜
- 一切都在一页上
- 下载清单(PDF、Apple Pages、MS Word)
- 订阅我们的电子邮件通讯不要错过下一个指南。