[译] 2019 前端性能优化年度总结 — 第五部分

2019 前端性能优化年度总结 — 第五部分

让 2019 来得更迅速吧!你正在阅读的是 2019 年前端性能优化年度总结,始于 2016。

目录

交付优化

39. 是否所有的 JavaScript 库都采用了异步加载?

当用户请求页面时,浏览器获取 HTML 并构造 DOM,然后获取 CSS 并构造 CSSOM,然后通过匹配 DOM 和 CSSOM 生成渲染树。一旦出现需要解析的 JavaScript,浏览器将停止渲染,直到 JavaScript 被解析完成,从而造成渲染延迟。作为开发人员,我们必须明确告诉浏览器不要等待 JS 解析,直接渲染页面。对脚本执行此操作的方法是使用 HTML 中的 deferasync 属性。

在实践中,事实证明我们应该更倾向于使用 defer。使用 async 的话,Internet Explorer 9 及其之前的版本有兼容性问题,可能会破坏它们的脚本。根据Steve Souders 的讲述,一旦 async 脚本加载完成,它们就会立即执行。如果这种情况发生得非常快,例如当脚本处于缓存中时,它实际上可以阻止 HTML 解析器。使用 defer 的话,浏览器在解析 HTML 之前不会执行脚本。因此,除非在开始渲染之前需要执行 JavaScript,否则最好使用 defer

此外,如上所述,限制第三方库和脚本的可能造成的影响,尤其是社交分享按钮和嵌入式 <iframe>(如地图)。Size Limit 库可以帮助防止 JavaScript 库过大 :如果不小心添加了一个大的依赖项,该工具将通知你并抛出错误。可以使用静态的社交分享按钮(例如 SSBG)和交互式地图的静态链接

也可以试着修改非阻塞脚本加载器以实现 CSP 合规性

40. 使用 IntersectionObserver 加载大型组件

一般来说,延迟加载所有大型组件是一个好主意,例如大体积的 JavaScript、视频、iframe、小部件和潜在的图像。最高效的方法是使用Intersection Observer API,它对具有祖先元素或顶级文档视口的目标元素提供了一种异步观察交叉点变化的方法。基本用法是,创建一个新的 IntersectionObserver 对象,该对象接收回调函数和配置对象。然后再添加一个观察目标就可以了。

回调函数在目标变为可见或不可见时执行,因此当它截取视窗时,可以在元素变为可见之前开始执行某些操作。实际上,我们使用了 rootMargin(根周围的边距)和 threshold(单个数字或数字数组,表示目标可见性的百分比)对何时调用回调函数进行精确控制。

Alejandro Garcia Anglada 发表了一篇关于如何将其应用到实践中的简易教程,Rahul Nanwani 写了一篇关于延迟加载前景和背景图片的详细文章,Google Fundamentals 提供了关于 Intersection Observer 延迟加载图像和视频的详细教程。还记得使用动静结合的物体进行艺术指导的长篇故事吗?你也可以使用 Intersection Observer 实现高性能的滚动型讲述

另外,请注意 lazyload 属性,它将允许我们以原生的方式指定哪些图像和 iframe 应该是延迟加载。功能说明:LazyLoad 将提供一种机制,允许我们强制在每个域的基础上选择加入或退出 LazyLoad 功能(类似于内容安全政策的功能。惊喜:一旦启用,优先提示 priority hints 将允许我们在标题中指定脚本和预加载资源的权重(目前已在 Chrome Canary 中实现)。

41. 渐进式加载图片

你甚至可以通过向页面添加渐进式图像加载技术将延迟加载提升到新的水平。与 Facebook,Pinterest 和 Medium 类似,可以先加载质量较差甚至模糊的图像,然后在页面继续加载时,使用 Guy Podjarny 提出的 LQIP(低质量图像占位符)技术将其替换为原图。

对于这项技术是否提升了用户体验,大家各执一词,但它一定缩短了第一次有效的绘图时间。我们甚至可以使用 SQIP 将其创建为 SVG 占位符或带有 CSS 线性渐变的渐变图像占位符。这些占位符可以嵌入 HTML 中,因为它们可以使用文本压缩方法自然地压缩。Dean Hume 在他的文章中描述了 如何使用 Intersection Observer 实现此技术。

浏览器支持怎么样呢?主流浏览器、Chrome、Firefox、Edge 和三星的浏览器均有支持。WebKit 状态目前已在预览中支持。如何优雅降级?如果浏览器不支持 intersection observer,我们仍然可以使用 polyfill延迟加载或立即加载图像。甚至有一个可以用来实现它。

想成为一名发烧友?你可以追踪你的图像并使用原始形状和边框来创建一个轻量级的 SVG 占位符,首先加载它,然后把占位符矢量图像转换为(已加载的)位图图像。

José M. Pérez 的 SVG 延迟加载技术

José M. Pérez的 SVG 延迟加载技术。(大图预览

42. 你是否发送了关键的 css?

为了确保浏览器尽快开始渲染页面,通常做法是收集开始渲染页面的第一个可见部分所需的所有 CSS(称为“关键 CSS”或“首页 CSS”)并将其以内联的形式添加到页面的 “” 中,从而减少往返请求。由于在慢启动阶段交换的包的大小有限,因此关键 CSS 的预算大小约为 14 KB。

如果超出此范围,浏览器将需要额外的开销来获取更多样式。CriticalCSSCritical 使你能够做到这一点。你可能需要为正在使用的每个模板执行此操作。如果可能的话,请考虑使用 Filament Group 使用的条件内联方法,或动态地将内联代码转换为静态资源

使用 HTTP/2,关键的 CSS 可以存储在单独的 CSS 文件中,并通过服务器推送传送,而不会增加 HTML 的大小。问题是,服务器推送很麻烦 ,浏览器存在许多陷阱和竞争条件。往往并不能始终支持,且伴有一些缓存问题(参见 Hooman Beheshti 演示文稿幻灯片的 114 页)。事实上,这种影响可能是负面的,它会使网络缓冲区膨胀,从而导致文档中真实帧的传递被阻止。此外,由于 TCP 启动缓慢,服务器推送似乎在热连接上更有效

即使使用 HTTP/1,将关键 CSS 放在根域名下的单独文件中也是有好处的,由于缓存的原因,有时甚至比内联更优。Chrome 在请求页面时会尝试打开根域名下的第二个 HTTP 连接,从而无需 TCP 连接来获取此 CSS(感谢 Philip!

需要记住的一些问题是:与可以从任何域触发预加载的“预加载”不同,你只能从自己的域或认证过的域中推送资源。一旦服务器从客户端获得了第一个请求,就可以启动该连接。服务器推送资源落在 Push 缓存中,并在连接终止时被删除。但是,由于 HTTP/2 连接可以在多个选项卡中重复使用,因此也可以使用通过其他选项卡的请求声明推送的资源(感谢 Inian!)。

目前,服务器没有简单的方法可以知道要推送资源是否已经存在于用户缓存之中中,每个用户访问的时候都会推送资源。因此,你可能需要创建 HTTP/2 的缓存感知服务器推送机制。如果发现已存在,则可以尝试根据缓存中已有内容的索引从缓存中获取它们,从而避免服务器的全量推送。

但请记住,新的 cache-digest 规范否定了手动构建此类“缓存感知”服务器的需要,只需要在 HTTP/2 中声明一个新的帧类型,就可以传达该域名下缓存中已有的内容。因此,它对 CDN 也特别有用。

对于动态内容,当服务器需要一些时间来生成响应时,浏览器无法发出任何请求,因为它不知道页面可能引用的任何子资源。对于这种情况,我们可以预热连接并增加 TCP 拥塞窗口的数量,以便可以更快地完成将来的请求。此外,所有内联资源通常都是服务器推送的良好候选者。事实上,Inian Parameshwaran 针对 HTTP/2 推送与 HTTP 预加载做了很棒的比较的研究,这份高质量的资料包括了你可能想了解的各种细节。是否选择服务器推送?Colin Bendell 的我是否应该进行服务器推送?可能会为你指明方向。

一句话:正如 Sam Saccone 所说预加载适用于将资源的开始下载时间向初始请求靠拢,服务器推送适用于删除完整的 RTT(,具体取决于服务器的响应时间)- 前提是你得有一个 service worker 用来避免不必要的推送。

43. 尝试重组 CSS 规则

我们已经习惯了关键的 CSS,但还有一些优化可以超越这一点。Harry Roberts 进行了一项非凡的研究,得出了相当惊人的结果。例如,将主 CSS 文件拆分为单独的媒体查询可能是个好主意。这样,浏览器将检索具有高优先级的关键 CSS,以及其他具有低优先级的所有内容 —— 最终完全脱离关键路径。

另外,避免将 <link rel="stylesheet" /> 放在 async 标签之前。如果脚本不依赖于样式表,请考虑将阻塞脚本放在阻塞样式之前。如果脚本依赖样式,请将该 JavaScript 一分为二,然后对应将其加载到 CSS 的前后。

Scott Jehl 通过使用 service worker 缓存内联 CSS 文件解决了另一个有趣的问题,这是使用关键 CSS 时常见的问题。基本上,我们将 ID 属性添加到 style 元素中,以便使用 JavaScript 时可以轻松找到它,然后一小块 JavaScript 发现 CSS 并使用缓存 API 将其存储在本地浏览器缓存中(其内容类型为 text/css),以便在后续页面中使用。为了不在后续页面上内联引用,而是从外部引用缓存的资源,我们在第一次访问站点时设置了一个 cookie。瞧!

我们是否以流的方式进行响应了?使用流,在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流 HTML 解析器。

44. 你有没有将请求设为 stream?

经常被遗忘和忽略的是 Streams提供了一个读或写异步数据块的接口,在任何给定的时间里,内存中可能只有一部分数据块可用。基本上,它们允许发出原始请求的页面在第一块数据可用时立即开始处理响应,并使用针对流优化的解析器逐步显示内容。

我们可以从多个来源创建一个流。例如,可以让 service worker 构造一个流,其中 shell 来自缓存,但主体来自网络,而不是提供一个空的 UI shell 并让 JavaScript 填充它。正如 Jeff Posnick 所说,如果你的 Web 应用程序由 CMS 提供支持,该 CMS 通过将部分模板缝合在一起呈现 HTML,则可以将该模型直接转换为使用流响应,模板逻辑将复制到 service worker而不是你的服务器中。Jake Archibald 的 Web Streams 之年文章重点介绍了如何准确地构建它。可以为性能带来相当明显的提升

流式处理整个 HTML 响应的一个重要优点是,在初始导航请求期间呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。页面加载后插入到文档中的 HTML 块(这在通过 JavaScript 填充的内容中很常见)则无法享受这种优化。

浏览器支持怎么样呢?主流浏览器,Chrome 52+、Firefox 57+、Safari 和 Edge 均支持该 API,而所有的现代浏览器中都支持 Service Workers。

45. 考虑使组件具有连接感知能力

随着不断增长的负载,数据的开销可能变得很大,我们需要尊重选择在访问我们的网站或应用程序时希望节省流量的用户。Save-Data 客户端提示请求头允许我们为受成本和性能限制的用户定制应用程序及其负载。事实上,你可以将高 DPI 图像的请求重写为低 DPI 图像请求,删除 Web 字体、花哨的视差效果、预览缩略图和无限滚动、关闭视频自动播放、服务器推送、减少显示项目的数量并降低图像质量,甚至改变交付标记的方式。Tim Vereecke 发表了一篇关于 data-s(h)aver 策略的非常详细的文章,其中介绍了许多用于数据保存的选项。

目前,只有 Chromium、Android 版本的 Chrome 或桌面设备上的 Data Saver 扩展才支持标识头。最后,你还可以使用 Network Information API 根据网络类型提供高/低分辨率的图像 和视频。Network Information API,特别是navigator.connection.effectiveType(Chrome62+)使用 RTTdownlinkeffectiveType(以及一些其他值)来为用户提供可处理的连接和数据表示。

在这种情况下,Max Stoiber 谈到连接感知组件。例如,使用 React 时,我们可以编写一个为不同连接类型呈现不同元素的组件。正如 Max 建议的那样,新闻文章中的 <Media /> 组件或许应该输出为下列的几种形式:

  • Offline:带有 alt 文本的占位符,
  • 2G / 省流 模式:低分辨率图像,
  • 非视网膜屏的 3G:中等分辨率图像,
  • 视网膜屏的 3G:高分辨率视网膜图像,
  • 4G:高清视频。

DeanHume 提供了一个使用 service worker 的类似逻辑的实现。对于视频,我们可以在默认情况下显示视频海报,然后显示“播放”图标,在网络更好的情况下显示视频播放器外壳、视频元数据等。作为浏览器不兼容的降级方案,我们可以监听 canplaythrough 事件,并在 canplaythrough 事件 2 秒内未触发的情况下使用 Promise.race() 来触发资源加载超时。

46. 考虑使组件具有设备内存感知能力

尽管如此,网络连接也只是为我们提供了关于用户上下文的一个视角。更进一步,你还可以动态地根据可用设备内存调整资源,使用 Device Memory API(Chrome63+)。navigator.deviceMemory 返回设备的RAM容量(以 GB 为单位),四舍五入到最近的 2 次方。该 API 还具有客户端提示标头 Device-Memory,该标头可以提供相同的值。

DevTools 中的“优先级”列

DevTools 中的“优先级”列。图片来源:Ben Schwarz,关键请求

47. 做好连接的热身准备以加速交付

使用资源提示来节省 dns-prefch(在后台执行 DNS 查找)的时间。preconnect 要求浏览器在后台启动连接握手(DNS、TCP、TLS),prefetch(要求浏览器请求资源)和 preload(除此之外,它并不需要执行它们即可预获取资源)。

现在大部分时间里,我们至少会使用 preconnectdns-prefetch,并且我们会谨慎地使用 prefetchpreload;只有当您对用户下一步需要哪些资源(例如,当用户处于购买漏斗模型中时)有信心时,才应该使用前者。

请注意,即使使用 preconnectdns-prefetch,浏览器对要并行查找/连接到的主机数量也有限制,因此基于优先级对它们进行排序是安全的(感谢 Philip!)。

事实上,使用资源提示可能是提高性能的最简单的方法,而且它确实很有效。什么时候用什么?正如 Addy Osmani 曾解释过的,我们应该预先加载我们高度信任的资源,以便在当前页面中使用这些资源。预获取资源可能会用于未来跨边界的导航,例如用户尚未访问的页面所需的 webpack bundles。

Addy 关于[“在 Chrome 中加载优先级”]的文章(medium.com/reloading/p…)准确地展示了 Chrome 是如何解释资源提示的,因此一旦确定了哪些资源对于渲染至关重要,就可以为它们分配高优先级。要查看请求的优先级,可以在 Chrome 的 DevTools 网络请求表(以及 Safari 的 Technology Preview)中启用“优先级”列。

例如,由于字体通常是页面上的重要资源,使用请求浏览器下载字体preload 一直是个好主意。你还可以动态加载 JavaScript,有效地执行延迟加载。另外,由于 <link rel="preload"> 接受一个 media 属性,因此可以基于 @media 查询规则选择可选的资源优先级

一些要记住的点preload 有利于使资源的开始下载时间更接近初始请求,但是,预加载的资源会存在内存缓存中,该缓存绑定到发出请求的页面上。preload 可以很好地处理 HTTP 缓存:如果 HTTP 缓存中已经存在该资源,则永远不会针对该资源去发送网络请求。

因此,对于最近发现的资源、通过后台图像加载的主页横幅、内联关键的 CSS(或 JavaScript)以及预加载 CSS(或 JavaScript)的其余部分,它非常有用。此外,preload 标记只能在浏览器接收到来自服务器的 HTML 并且先行解析器找到 preload 标记后才能启动预加载。

通过 HTTP 报头预加载要快一些,因为我们不需要等待浏览器解析 HTML 来启动请求。预提示 将提供更多帮助,即使在发送 HTML 的响应头和优先级提示即将发布)之前就启用预加载,将帮助我们指示脚本的加载优先级。

注意:如果你使用的是 preload预加载的内容 必须被定义 否则就不会加载任何内容,另外不使用预加载字体的话跨域属性会两次获取数据

48. 使用 service workers 进行缓存和网络降级

网络上的任何性能优化都赶不上从用户计算机上本地存储的缓存中取数据快。如果你的网站基于 HTTPS 协议,请使用“Service Workers 的实用指南”将静态资源缓存到 service worker 缓存中,并存储离线回退(甚至离线页),然后从用户的计算机检索它们,而不是转向网络。此外,请查看 Jake 的离线 Cookbook 和免费的 udacity 课程“离线 Web 应用”。

浏览器支持怎么样呢?如上所述,它得到了广泛支持(Chrome、Firefox、Safari TP、三星浏览器、Edge 17+),降级的话就是去发网络请求。它是否有助于提高性能呢?当然了,。而且它正在变得更好,例如通过后台抓取,允许从 service worker 进行后台上传/下载等。Chrome71 中已发布

service worker 有许多使用案例。例如,可以实现“离线保存”功能处理已损坏图像,介绍选项卡之间的消息传递根据请求类型提供不同的缓存策略。一般来说,一种常见的可靠策略是将应用程序外壳与几个关键页面一起存储在 service worker 的缓存中,例如离线页面、前端页面以及对具体场景中可能重要的任何其他页面。

尽管如此,还是有几个问题需要记住。使用 service worker 时,我们需要注意 Safari 中的范围请求(如果你使用的是 service worker 的工作框,它有一个范围请求模块)。如果你在浏览器控制台中偶然发现了 DOMException: Quota exceeded. 错误,那么请查看 Gerardo 的文章当 7KB 等于 7Mb

Gerardo 写道:“如果你正在构建一个渐进式 Web 应用程序,并且使用 service worker 缓存来自 CDN 的静态资源,并正在经历高速缓存存储膨胀,请确保跨域资源有适当的 CORS 响应头存在不要缓存不透明的响应,通过给 <img> 标签设置 crossorigin 属性,将跨域图像资源设为 CORS 模式“。

使用 service worker 的一个很好的起点是 workbox,这是一组专门为构建渐进式 Web 应用程序而构建的 service worker 库。

49. 是否在 CDN/Edge 上使用了 service workers,例如,用于 A/B 测试?

在这一点上,我们已经习惯于在客户端上运行 service worker,但是通过在 CDN 服务器上使用它们,我们也可以实现用它们来调整边缘性能。

例如,在 A/B 测试中,当 HTML 需要为不同的用户改变其内容时,我们可以使用 CDN 服务器上的 service worker 来处理逻辑。我们还可以通过重写 HTML 流来加速使用谷歌字体的站点。

50. 优化渲染性能

使用CSS容器隔离开销大的组件 —— 例如,限制浏览器样式、画布和画图用于画布外导航或第三方小部件的范围。请确保在滚动页面或设置元素动画时没有延迟,并且始终达到每秒 60 帧。如果这无法实现,那么至少使每秒的帧数保持一致,这比 60 到 15 之间的不定值更可取。使用 CSS 的 will-change 去通知浏览器哪些元素和属性将更改。

此外,度量运行时渲染性能(例如,使用 DevTools 中的 rendering 工具)。想要快速上手,可以查看 Paul Lewis 关于浏览器渲染优化的免费 udacity 课程和 Georgy Marchuk 关于浏览器绘制和 Web 性能思考的文章

如果你想深入探讨这个话题,Nolan Lawson 在他的文章中分享了精确测量布局性能的技巧,Jason Miller 也给出了替代技术的建议。 我们还有 Sergey Chikuyonok 撰写的一篇关于如何正确制作 GPU 动画的文章。快速提示:对 GPU 合成层的更改是开销最小的,因此,如果你只通过 opacitytransform 触发合成,那就对了。Anna Migas 在她关于调试 UI 呈现性能的演讲中也提供了很多实用的建议。

51. 是否优化了渲染体验?

虽然组件在页面上的显示顺序以及我们如何将资源提供给浏览器的策略很重要,但我们不应低估感知性能的作用。这一概念涉及到等待时的心理效应,基本上是让顾客在其他事情发生的时候保持有事可做。这就是感知管理抢先启动提前完成容忍度管理开始发挥作用。

这一切意味着什么?在加载资源时,我们可以尝试始终领先于客户一步,这样在后台繁忙的时候,用户依然感觉页面速度很快。为了让客户参与进来,我们可以测试框架屏幕实现演示),而不是loading指示器。添加过渡/动画,简单的欺骗用户体验。不过,请注意:在部署之前应该对骨架屏幕进行测试,因为从各项指标来看,有些测试表明,骨架屏幕的性能最差

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

猜你喜欢

转载自juejin.im/post/5c60ed6cf265da2dd4274724