(译)2019年前端性能优化清单 — 中篇

17,612 阅读1小时+

目录

资源优化

17. 使用 Brotli 或 Zopfli 进行纯文本压缩

在 2005 年,Google推出Brotli,一个新的开源无损数据压缩格式,现在已经被 所有的现代浏览器所支持。实际上,Brotli 比 Gzip 和 Deflate 更有效。压缩速度可能会非常慢,但这取决于设置信息,可是缓慢的压缩过程会提高压缩率。它仍然可以快速解压缩,并且您还可以估算您网站的Brotli压缩成本

只有当用户通过 HTTPS 访问网站时,浏览器才会采用。Brotli 现在还不能预装在某些服务器上,而且如果不自己构建 NGINX 和 UBUNTU 的话很难部署。不过这也并不难,而且它的支持即将到来,例如,从Apache 2.4.26开始 就可以使用它了。Brotli 得到了广泛的支持,许多 CDN 支持它( AkamaiAWSKeyCDNFastlyCloudlareCDN77),您甚至可以在 还不支持它的CDN上 启用Brotli(与 service worker 一起)。

在最高级别的压缩下,Brotli 的速度会变得非常慢,以至于服务器在等待动态压缩资源时开始发送响应所花费的时间可能会使我们对文件大小的优化无效。但是,对于静态压缩,高压缩比的设置比较受欢迎—— (感谢 Jeremy!)

或者,你可以考虑使用 Zopfli的压缩算法,将数据编码为 DeflateGzipZlib 格式。Zopfli 改进的 Deflate 编码使得任何使用 Gzip 压缩的文件受益,因为这些文件大小比 用Zlib 最强压缩后还要小 3% 到 8%。问题在于压缩文件的时间是原来的大约 80倍。这就是为什么虽然 使用 Zopfli 是一个好主意但是变化并不大,文件都需要设计为只压缩一次可以多次下载的。

比较好的方法是你可以绕过动态压缩静态资源的成本。Brotli 和 Zopfli 都可以用于明文传输 —— HTML,CSS,SVG,JavaScript 等。

有什么方法呢?在最高等级和 Brotli 的 1-4 级动态压缩 HTML 使用Brotli+Gzip 预压缩静态资源。同时,检查 Brotli 是否支持 CDN,(例如KeyCDN,CDN77,Fastly)。确保服务器能够使用 Brotli 或 gzip 处理内容。如果你不能安装或者维护服务器上的 Brotli,那么请使用 Zopfli。

18. 使用 响应式图像WebP

尽可能通过 srcsetsizes<picture> 元素使用响应式图片。也可以通过 <picture> 元素使用 WebP格式的图像(Chrom,Opera,Firefox soon支持),或者一个 JPEG 的回调(见 Andreas Bovens 的 code snippet)或者通过使用内容协商(使用 Accept 头信息)。Ire Aderinokun 中也有一个关于 将图像转换为WebP非常详细的教程

Sketch 本身就支持 WebP,并且 WebP 图像可以通过使用 WebP插件 从 PhotoShop 中导出。您也有 其他选择 可以使用。你可以使用 WordPress 或者 Joomla,也有其他可以轻松支持 WebP 的扩展,例如 OptimusCache Enabler 以及 Joomla自己支持的扩展(通过 Cody Arsenault)。

您需要注意的是,虽然WebP图像文件大小与 Guetzli和Zopfli相比大小都差不多,但格式 不支持像 JPEG 这样的渐进式渲染,这就是为什么用户在使用良好的旧 JPEG 时会更快地看到实际图像,尽管 WebP 图像可能在网络中加载速度会更快。使用 JPEG,我们可以用一半甚至四分之一的数据保证良好的用户体验,然后再加载其余的数据,而不会像WebP那样显示半空的图像。您的决定取决于您的目标:如果您使用WebP,那么将会减少网络的负载,如果使用JPEG,您将提高用户体验。

在 Smashing Magazine 上,我们使用后缀-opt作为图像名称—例如brotli-compression-opt.png;每当图像包含该后缀时,团队中的成员就会知道该图像已经被优化。还有 —— shamleless plug!-Jeremy Wagner 在 WebP上发表了一本Smashing书

响应式图像断点生成器 自动生成图像和标记生成。

19. 图像是否已恰当优化

现在有一个至关重要着陆页,有一个特定的图片的加载速度非常关键,确保 JPEGs 是渐进式的并且使用Adept、mozJPEG(通过操纵扫描级来改善开始渲染时间)或者 Guetzli 压缩,谷歌新的开源编码器重点是能够感官的性能,并借鉴 Zopfli 和 WebP。唯一的不足是:处理的时间慢(每百万像素 CPU 一分钟)。至于 png,我们可以使用 Pingo 和 svgo,对于 SVG 的处理,我们使用 SVGOSVGOMG

每一个图像优化的文章会说明,但始终会提醒要保持矢量资源干净和紧密。确保清理未使用的资源,删除不必要的元数据,并减少图稿中的路径点数量(从而减少SVG代码)。(感谢,Jeremy!)

不过下面还有一些更好的方法:

  • 使用 Squoosh 以最佳压缩级别(有损或无损)压缩,调整大小和操作图像。
  • 使用 响应式图像断点生成器CloudinaryImgix 等服务来自动优化图片。同样,在许多情况下,单独使用 srcsetsize 也会有很不错的效果。
  • 可以使用 映像堆 检查响应式标记的效率,这是一种命令行工具,也可以用来测量视口大小和设备像素比率的效率。
  • 延迟加载带有 lazysizes 的图像和iframe,这是一个库,可以检测页面上通过用户交互(或者我们将在稍后探索的IntersectionObserver)而触发的任何改变。
  • 格外注意那些会默认加载但可能永远不会显示的图像——例如carousels、accordions和image galleries。
  • 考虑通过根据媒体查询指定不同的图像显示尺寸来 交换具有sizes属性 的图像,例如操纵sizes以交换放大镜组件中的源。
  • 检查图像 下载的不一致性,以防止对前景和背景图片的意外下载。
  • 为了优化内部存储,你可以使用 Dropbox 新的 Lepton格式 进行压缩,平均将jpeg压缩22%。
  • 注意 aspect-ratio CSSintrinsicsize-attribute 中的属性,它们允许我们为图像设置宽高比和size,因此浏览器可以提前预留一个预定义的布局槽,以 避免 在页面加载期间出现 布局跳转
  • 如果你想使用其他方式,你可以尝试 Edge worker (一种基于CDN的实时过滤器)来剪切和重新排列HTTP/2流,从而更快地通过网络发送图片。Edge workers 使用您可以控制的块的JavaScript流(基本上它们是在CDN边缘上运行的可以修改流式响应的JavaScript),所以可以控制图像的传递。对于server worker 来说,为时已晚,因为您无法控制在线上的内容,但Edge workers 来说确实有效。因此,您可以在针对特定登录页面逐步保存的静态JPEG之上使用它们。

图像堆栈 输出的一个示例,这是一个命令行工具,用于测量视图大小和设备像素比的效率。

随着 客户端提示(client-hints) 的采用,响应图像的未来可能会发生巨大的变化。客户端提示是一个HTTP请求头字段,例如 DPRViewport-WidthWidthSave-DataAccept (指定图像格式首选项)等。它们通过告知服务器关于用户浏览器、屏幕、连接等的详细信息,然后服务器可以按照详细信息提供如合适格式、合适大小的图片来填充布局。通过客户端提示,我们可以发送一个将资源选择从HTML标记转移到客户端和服务器之间的请求-响应协商。

正如 Ilya Grigorik所指出的,客户端提示完成了图像——它们不是响应图像的替代方案。<picture>元素在HTML标记中提供必要的艺术方向控制。客户端提示为生成的图像请求提供注释,从而支持资源选择自动化。Service Worker在客户端上提供完整的请求和响应管理功能。例如,service Worker可以向请求添加新的客户端提示标头值,以重写URL并将图像请求指向CDN,根据连接和用户首选项等来调整响应。它不仅适用于图像资源,而且还是适用于几乎其他的所有请求。

对于支持客户端提示的客户端,可以在图像上 节省42%的字节,在70%以上的百分比上节省1MB以上的字节。在Smashing Magazine上,我们也可以测量 19-32%的改进。糟糕的是,客户端提示仍然需要 获得一些浏览器支持Firefox正在考虑中。但是,如果同时提供正常响应图像标记和 <meta> 客户端提示标记,那么浏览器将评估响应图像标记并使用客户端提示HTTP头请求适当的图像资源。

如果这样子做还不够的话。您还可以使用 背景 图像 技术 提高图像的性能。请记住,使用对比度 和模糊不必要的细节(或删除颜色)也可以减少文件大小。如果你需要在不影响图片质量的情况下放大一张小照片的话,可以考虑使用 Letsenhance.io

到目前为止,这些优化只覆盖了基础。Addy Osmani已经发布了一份非常详细的 关于基本图像优化的指南,其中深入介绍了图像压缩和颜色管理的细节。例如,您可以模糊掉图像中不必要的部分(通过对它们应用高斯模糊过滤器)以减小文件大小,甚至可以删除颜色或将图像变为黑白,以进一步缩小图像尺寸。如果是背景图像,从Photoshop中导出0到10%质量的照片也是绝对可以接受的。奉劝大家 不要在网上使用JPEG-XR

20. 视频是否已恰当优化

到目前为止,我们已经谈论了一些图片,但是我们一直没有说到GIF图片。坦白的说,如果不是加载影响渲染性能和宽带的重型动画GIF,建议最好切换到动画WebP(GIF是后备)或者用 循环HTML5 video 替换它们。是的,浏览器 处理 video 的速度很慢,而且与图像不同的是,浏览器不会预加载 video 内容,但 video 往往要比gif更轻更小。至少我们可以用 Lossy GIFgifsiclegiflossy 来给GIF添加有损压缩。

早期测试表明,img 标签内的内嵌视频 显示速度提高了20倍,解码速度比相同大小的 GIF动图要 快7倍,另外文件大小尺寸也会优于GIF。尽管对 Safari技术预览版 的支持 <img src=".mp4"> 已经 落实,但还远未被广泛采用,因为它 不会很快进入Blink

Addy Osmani 建议用循环的内联视频来替换动画GIF。文件大小的差异很明显(节省了80%)。

然而,在这片充满好消息的土地上,视频格式多年来一直在大规模发展。很长一段时间以来,我们一直希望WebM能够成为规范所有这些格式的格式,而 WebP (基本上是 WebM 视频容器中的一个静态图像)将成为过时的图像格式的替代品。但是,尽管 WebP 和 WebM 最近 获得了 支持,可是他们没有什么实质性的突破。

2018年,开放媒体联盟(Alliance of Open Media)发布了一种名为 AV1 的新型视频格式。AV1 的压缩类似于 H.265 编解码器(H.264的演变),与后者不同的是,AV1是免费的。H.265的收费制使得浏览器厂商开始采用性能 相对较高 的 AV1: AV1(类似H.265)的压缩效果是WebP的两倍

AV1很有可能成为网络视频的终极标准。(图片来源: Wikimedia.org)

事实上,苹果目前使用的是 HEIF 格式和 HEVC (H.265),最新iOS上的所有照片和视频都以这些格式保存,而非JPEG格式。虽然 HEIFHEVC (H.265) 还没有完全公开到网络上,但是 AV1 已经完全公开在网络上并且 正在获得浏览器支持。因此,AV1在您的 <video>标记中添加源是合理的,因为所有浏览器供应商都在为支持AV1做准备。

目前,最广泛使用和支持的编码是H.264,由MP4文件提供服务,因此在提供文件之前,请确保使用 多通道编码 处理MP4 ,模糊了frei0r iirblur效果(如果适用)和 moov atom metadata 移动到文件的头部,而服务器 接受字节服务。Boris Schapira 为 FFmpeg 提供了最大限度优化视频的 标准说明。当然,提供WebM格式作为替代方案也会有所帮助。

视频播放性能本身就是一个故事,如果您想深入了解它,请阅读Doug Sillar关于 视频视频传输最佳实践的最新实践系列,其中包括关于视频传输度量、视频预加载、压缩和流媒体的详细信息。

Zach Leatherman 的 字体加载策略综合指南 为更好的 web 字体交付提供了十几种选择。

21. Web 字体是否已恰当优化

首先需要问一个问题,你是否能不使用 UI 系统字体。 如果不可以,那么你有很大可能使用 Web 网络字体,会包含字形和额外的功能以及用不到的加粗。如果您使用的是开源字体,您可以向字体设计公司 获取 网络字体子集或子集,也可以使用 GlyphhangerFontsquirrel 对其进行子集化。您甚至可以使用 Peter Muller 的 subfont 将整个流程自动化,因为他是一个命令行工具,它通过静态地分析您的页面,然后生成最优的web字体子集,最后将它们注入到您的页面中。

WOFF2的支持 非常好,对于不支持WOFF2的浏览器,你可以使用 WOFF 和 OTF 作为不支持它的浏览器的备选。另外,从 Zach Leatherman 的 《字体加载策略综合指南》(代码片段也 可以作为Web字体 加载片段)中选择一种策略,并使用服务器缓存持久地缓存字体。

可能今天要考虑的更好的选择是 关键FOFT预加载折中 方法。它们都使用两阶段渲染来逐步提供Web字体——首先是使用web字体快速准确地呈现页面所需的小超子集,然后加载其余的异步操作。不同之处在于,折中 技术仅在不支持 字体加载事件 时才异步加载polyfill ,因此默认情况下不需要加载polyfill。需要速战速决吗?Zach Leatherman有一个 23分钟的快速教程 和案例研究,以使您的字体优化。

通常,使用 preload 资源提示来预加载字体是一个好办法,但在标记中要包含关键CSS和JavaScript链接后的提示。否则,字体加载将在第一次渲染时造成损失。不过,有 选择性 地选择最重要的文件可能是一个好主意,比如那些对渲染至关重要的文件,或者那些可以帮助您避免可见和破坏性文本重拍的文件。一般来说,Zach建议 预加载每个系列的一到两种字体——如果字体不是很重要,那么建议延迟加载。

我相信没有人喜欢等待内容的显示。使用 font-display CSS描述符,我们可以控制字体加载行为,并使内容立即可读 (font-display: optional) 或几乎立即可读 (font-display: swap)。但是,如果您想 避免文本重排,我们仍然需要使用字体加载API,特别是对 重组 进行 分组,或者当您使用第三方主机时,除非您可以 将Google字体与Cloudflare Workers一起使用。谈到谷歌字体::可以考虑使用 Google -webfonts-helper,这是一种轻松自我托管Google字体的方式。如果可以的话,采用 始终自行托管自身项目的字体 的方式来获得最大程度的控制。

一般情况下,如果您使用font-display: optional,也可能不是一个好主意,因为preload会提前触发Web字体请求(如果您有其他需要获取的关键路径资源,则会导致网络拥塞)。使用preconnect可以实现更快的跨域字体的请求,但需要注意的是,preload从不同来源预载的字体wlll会导致网络占用。所有这些技术都包含在 Zach 的 Web字体加载方式 中。

此外,如果用户在可访问性首选项中启用了 Reduce Motion,或者选择了Data Saver模式(请参见 Save-Data header),或者当用户连接速度较慢时(通过 网络信息API),则最好选择退出Web字体(或至少是第二阶段渲染)。

要测量Web字体加载性能,请考虑 所有文本可见 度量标准(所有字体已加载且所有内容以 Web字体显示的时刻),以及首次渲染后的 Web字体重排计数。显然,两个指标越低,性能越好。重要的是要注意 可变 字体 可能需要 显著的性能考虑。它们为设计人员提供了更广阔的设计空间来选择字体,但它的代价是单个串行请求,而不是单个文件请求。单个请求可能会阻碍页面上的整个排版外观。但好消息是,当我们有了可变字体,在默认情况下只需要获得一个 reflow,而不需要 JavaScript 对重新绘制进行分组。

怎么才能是一个无漏洞的字体加载策略? 从font-display开始,然后到 Font Loading API,然后到 Bram Stein 的 Font Face Observer(感谢 Jeremy!)如果你有兴趣从用户的角度来衡量字体加载的性能, Andreas Marschke 探索了 使用 Font API 和 UserTiming API 进行 性能跟踪

此外,不要忘记包含 font-display:optional 描述符来提供弹性和快速的字体回退,unicode-range 将大字体分解成更小的语言特定的字体,以及 Monica Dinculescu 的 字体样式匹配器 用来解决由于两种字体之间的大小差异,最大限度地减少了布局上的震动的问题。

构建优化

22. 分清轻重缓急

你应该知道优先处理什么。运行你所有静态资源(JavaScript、图片、字体、第三方脚本和页面中“昂贵的”模块,比如:轮播图、复杂的图表和多媒体内容),并将它们划分成组。

建立一个电子表格。针对传统浏览器定义基本的核心体验(即完全可访问的核心内容),针对多功能的浏览器定义增强的体验(即丰富的、完整的体验)和额外的体验(不是绝对需要的并且可以延迟加载的资源,如web字体、不必要的样式、轮播图、视频播放器、社交媒体按钮、大图片等)。不久前,我们发表了一篇关于 Improving Smashing Magazine的性能 的文章,上面有该方法的详细介绍。

我们在优化性能时候的优先级是:首先加载核心体验,然后是增强功能,最后才是附加功能。

23. 考虑使用 cutting-the-mustard 技术

现在,我们仍然可以使用 cut -the-mustard 技术 将核心体验传递给传统浏览器,并提高对现代浏览器的体验。该技术的 下一版本 将使用 ES2015 + <script type="module">。现代浏览器会将脚本解释为JavaScript模块并按预期运行,而传统浏览器不会识别该属性并忽略它,因为它是未知的 HTML 语法。

目前我们需要记住的是,仅仅是特征检测不足以做出关于将有效载荷发送到浏览器的决定。从其自身来看,cutting-the-mustard 可以从浏览器版本中推断出设备的能力,这已经不是我们今天能够做到的事情了。

例如:在发展中国家,廉价的安卓手机主要运行 Chrome,虽然它们的内存和 CPU 有限,但仍能满足要求。最终,使用 Device Memory Client Hints Header,我们就能够更可靠地识别出低端设备。现在,只有在 Blink 中才支持 header (通常用于 client hints)。因为设备存储也有一个在Chrome 中可以调用的 JavaScript API,一种选择是基于 API 的特性检测,只在不支持的情况下回退到 符合标准 技术(谢谢,Yoav!)。

24. 解析 JavaScript 是昂贵的,所以保持小

在处理单页应用程序时,我们需要一些时间来初始化应用程序,然后才能呈现页面。您的设置将需要定制的解决方案,但是您可以注意使用模块和技术来加快初始呈现时间。例如,下面是 如何调试 React 性能消除常见的 React 性能问题,以及 如何在 Angular 中提高性能。通常,大多数性能问题来自于启动应用程序的初始解析时间。

JavaScript 有成本,但不一定是文件大小影响性能。解析和执行时间的不同很大程度依赖设备的硬件。在一台普通的手机上(Moto G4),仅解析 1MB (未压缩的)的 JavaScript 大概需要 1.3-1.4 秒,会有 15 - 20% 的时间耗费在手机的解析上。在执行编译过程中,只是用在 JavaScript 准备平均需要 4 秒,在手机上页面展示其主要内容所需的时间(First Meaningful Paint)需要 11 秒。解释:在低端移动设备上,解析和执行时间可以轻松提高 2 至 5 倍

作为一个开发人员,为了保证高性能,我们需要找到编写和部署更少 JavaScript 的方法,这就是详细检查每个 JavaScript 依赖项的好处所在。

下面这些工具可以帮助您对依赖关系和可行替代方案的影响做出明智的决定:

使用 Ember 在 2017 年引入的 二进制模板( binary templates) 可以巧妙的避免解析开销过大。使用它们,Ember的建议:使用二进制模板将 JavaScript 解析替换为 JSON 解析,解析速度可能会更快。(谢谢,Leonardo,Yoav !)

衡量 JavaScript 的解析和编译时间。我们可以使用综合测试工具和浏览器跟踪来跟踪解析时间,浏览器开发商正在讨论在将来 公开 RUM-based 的处理时间。或者,你也可以考虑使用 Etsy 的 DeviceTiming,一个让你可以指导你的 JavaScript 在任何设备或浏览器上测量解析和执行时间的小工具。

最重要的是:虽然脚本大小很重要,但它并不是一切。因为当脚本大小增加时,解析和编译时间 不一定会随着脚本的变大而相应的增加

25. 使用 无用代码移除(Tree-shaking) ,作用域提升(Scope hoisting)和代码分割(Code-splitting)来减少有效负载

Tree-shaking是一种清理构建过程的方法,通过只加载生产中实际使用的代码并清除在 Webpack 中 未使用的 import。使用 Webpack 和 Rollup,当然我们还可以使用 scope hoisting(作用域提升),scope hoisting 允许工具检测哪些 import 可以被提升或者可以转换成一个内联函数。有了 Webpack ,你现在可以使用 JSON Tree Shaking

而且,你需要考虑如何 编写高效的 CSS 选择器 以及如何 避免编写臃肿和开销浪费的样式。你也可以使用 Webpack 缩短类名和在编译时使用独立作用域来 动态地重命名 CSS 类

Code-splitting 是 Webpack 的另一个特性,可将你的代码分解为按需加载的 。并不是所有的 JavaScript 都是必须下载、解析和编译的。一旦你在代码中确定了分割点,Webpack 会处理这些依赖关系和输出文件。这样,在应用发送请求的时候,基本上确保初始的下载足够小并且实现按需加载。Alexander Kondrov 对 使用 Webpack 和 React 进行代码拆分 做了精彩的介绍。

另外,考虑使用 preload-webpack-plugin 获取代码拆分的路径,然后使用 <link rel="preload"> 或者 <link rel="prefetch"> 提示浏览器预加载它们。Webpack 内联指令 还提供了一些对 preload / prefetch 的控制。

在哪里定义分离点?通过追踪哪些 CSS/JavaScript 块被使用和哪些没有被使用。Umar Hansa 解释 了你如何使用 Devtools 的 Code Coverage 来实现。

如果你没有使用 Webpack,那么相比于 Browserify 的输出结果,Rollup 的输出更好一些。我们在此过程中,可以查看 rollup-plug -closure-compilerRollupify,它们将 ECMAScript 2015 模块转换成一个大的 CommonJS module —— 因为根据您对 bundler 和 module system 的选择,小模块的 性能成本会高得惊人

26. 能否将 JavaScript 卸载到 Web Worker 中

为了减少对时间与交互的影响,可以考虑将繁重的 JavaScript 卸载到 Web Worker 或通过 Service Worker 进行缓存。

随着代码库的代码量不断增长会导致UI性能出现瓶颈,这会直接导致用户体验降低。这是因为 DOM 操作在主线程上与 JavaScript 一起运行。通过 web workers,我们可以将这些繁琐的操作转移到不同线程上的后台进程上运行。web workers 的典型用例是通过 预取数据和渐进式 web 应用程序,来提前加载和存储一些数据,以便在需要的时候使用。您还可以使用 Comlink 来简化主页和 worker 之间的通信。我们正在无限接近目标,但在这个过程中我们还有一些工作要做。

Workerize 允许你将一个模块移动到一个Web Worker 中,自动将导出的函数反映为异步代理。您可以在 Webpack 中使用 workerize-loader 或者 worker-plugin

请注意,Web Worker 不能访问 DOM,因为 DOM 不是一个 安全线程,并且执行的代码需要包含在一个单独的文件中。

27. 能否将 JavaScript 卸载到 WebAssembl 中

我们还可以将 JavaScript 转换成 WebAssembly(一种二进制指令格式),然后设计成一个便携式目标,用于编译如 C/ c++ /Rust 这种高级语言。它的 浏览器支持也是非常好,而且随着 JavaSript 和 WASM 之间的函数调用越来越快(至少在Firefox中是这样),它最近已经可以实现了。

在实际场景中,如果一个在一个较小的数组上,JavaScript 的性能要优于 WebAssembly,反之则是 WebAssembly 的性能优于 JavaScript)。对于大多数 web 应用程序而言,JavaScript是更好的选择,而 WebAssembly 更适合用于类似 web 游戏这种计算密集型的 web 应用程序。所以,切换到 WebAssembly 是否会带来显著的性能提升是值得研究去好好研究一番。

如果你想了解更多关于 WebAssembly 的信息:

Milica Mihajlija 为我们提供了 一个关于WebAssembly的工作原理以及它的用处的概述

28. 您使用的是预编译器吗

使用 预编译器 可以将一些客户端渲染卸载到 服务器,从而快速输出可用的结果。最后,可以考虑使用 [optimization.js(github.com/nolanlawson…) 来封装急切调用的函数(不过 可能不再需要它了)来加快初始加载速度。

Addy Osmani 的 从快速默认到现代加载的最佳实践

29. 仅将遗留代码提供给传统浏览器

随着 现代浏览器 对 ES2015 的支持越来越好,考虑 使用 babel-preset-env 只转换现代浏览器不支持的 ES2015+ 的特性。然后 设置两个构建,一个为 ES6 一个为 ES5。就像上面所说的那样,现在所有 主流浏览器都支持 JavaScript 模块,我们可以 使用 script type="module" 让具有 ES 模块支持的浏览器加载文件,而老的浏览器可以加载传统的 script nomodule。我们可以使用 Webpack ESNext Boilerplate 自动完成整个流程。

请注意,现在我们可以在不需要编译器或绑定器的情况下编写在浏览器中本地运行的基于模块的 JavaScript。<link rel="modulepreload"> header 提供一种方法来启动模块脚本的早期(高优先级)加载。基本上,这是一种有助于最大化带宽使用的有效方式,通过告知浏览器它需要获取什么,以使其不会在那些长时间往返的过程中被卡住。Jake Archibald 也发表了一篇详细的文章,其中 介绍了 ES 模块中需要记住的问题和内容,值得一看。

对于 loadsh,使用 babel-plugin-lodash 将只加载你仅在源码中使用的模块。您的依赖项可能还依赖于 Lodash 的其他版本,因此您需要 将通用 lodash 转换成适合自己项目的 loadsh,以避免代码重复。这可能会为您节省相当多的 JavaScript 负载。

Shubham Kanodia 编写了一份 关于智能捆绑的详细低维护指南:将遗留代码与您可以立即使用的代码片段一起交付到生产环境中的传统浏览器。

Jake Archibald 发布了一篇详细的文章,介绍了 ES 模块中需要记住的问题和内容,例如,内联脚本被推迟到阻塞外部脚本和内联脚本执行时才执行。

30. 您是否在JavaScript中使用差异化服务

我们想通过网络发送必要的 JavaScript 代码,但这意味着对这些资源的交付要更加专注和细致。不久前,Philip Walton 提出了差异化服务 的概念。其思想是编译并提供两个独立的 JavaScript 包:一个是 常规 (带有 Babel-transforms 和 polyfill )的构建包,另一个是没有 Babel-transforms 或 polyfill 的包,这两个包都是只提供给实际需要它们的传统浏览器。

因此,我们通过降低浏览器需要处理的脚本的数量来减少阻塞主线程的进程。Jeremy Wagner 发表了一篇文章,全面的介绍了差异化服务 以及如何在2019年的构建管道中设置它,从设置 Babel,到需要在 Webpack 中进行哪些调整,以及做这些工作的好处。

31. 通过增量解耦识别和重写遗留代码

长期存在的项目有收集灰尘和过时代码的趋势。重新考虑你的依赖,评估需要多少时间来重构或重写那些最近一直在导致问题的遗留代码。如果您了解了遗留代码的影响,您就可以从 增量解耦 开始解决这项艰巨的任务。

首先,建立一个度量标准,跟踪遗留代码调用的比例,看看是保持不变还是下降,如果调用不是上升,那就公开阻止团队使用这个库,并确保 CI 在 pull 请求中使用得时候可以 通知 到开发人员。polyfills 可以使用标准浏览器功能帮助遗留代码重写代码库。

32. 识别并删除未使用的 CSS / JavaScript

Chrome 中的 CSS 和 JavaScript 代码覆盖 允许您了解哪些代码已经执行/应用,哪些代码还没有执行。您可以开始记录覆盖率,在页面上执行操作,然后研究代码覆盖率结果。检测到未使用的代码后,使用 import() 查找这些模块和延迟加载(参见整个线程)。然后重复覆盖率配置文件,并验证它现在在初始加载时提供的代码更少。

您可以使用 Puppeteer 以编程方式收集代码覆盖率,Canary 已经允许您 导出代码覆盖率结果。正如 Andy Davies 所指出的,您可能希望 为现代浏览器和传统浏览器收集代码覆盖率。对于 Puppeteer,还有许多 其他的用例,例如,在每次构建时自动地对未使用的CSS进行视觉区分监视

此外,purgecssUnCSSHelium 可以帮助您从 CSS 中删除未使用的样式。如果你不确定代码是否被使用,您可以按照 Harry Roberts的建议:为一个特定类创建一个 1×1 px 透明的 GIF 图片并将其放在 dead/ 目录下,例如:/assets/img/dead/comments.gif。然后,在CSS中相应的选择器上将特定的图像设置为背景,如果文件出现在日志中,则等待几个月。如果文件没有出现在日志中,那就是没有人在屏幕上渲染该遗留组件,您可以把它全部删除。

对于一个有想法的小伙伴来说,可以考虑通过 使用并监视DevTools,在一组页面上自动收集未使用的 CSS。

33. 修剪 JavaScript 依赖项的大小

正如 Addy Osmani 所指出的,当您只需要一小部分 JavaScript库时,您很可能会提供完整的JavaScript 库,以及不需要这些库的浏览器的过时 polyfill,或者只是重复代码。为了避免这种开销,可以考虑使用 webpack-libs-optimization来删除构建过程中未使用的方法和 polyfill

将捆绑审核对于您多年前添加的大型库,可能有一些轻量级的替代方案,例如Moment.js可以用 date-fnsLuxon 代替。BenediktRötsch 的研究 表明,从 Moment.jsdate-fns 的转换可能会使 3G 和低端手机上的首次使用时间节省大约 300ms。

这就是像 Bundlephobia 这样的工具可以帮助我们监测到向包中添加 npm 包的成本。您甚至可以 将这些成本与 Lighthouse Custom Audit 相结合。这也适用于框架。通过删除或修剪 Vue MDC适配器(Vue的材料组件),样式大小从 194KB 下降到 10KB。

喜欢冒险吗?你可以看看 Prepack。它将JavaScript 编译成等效的 JavaScript 代码,但与 Babel 或 Uglify 不同的是,它允许编写正常的 JavaScript 代码,并输出运行速度更快的等效 JavaScript 代码。

作为整个框架的替代,您甚至可以修剪框架并将其编译为不需要额外代码的原始JavaScript包。Svelte 做到了,还有 Rawact Babel plugin 也做到了。在构建时将 React.js 组件转换为本地 DOM 操作。为什么?正如维护者所解释的,react-dom 包含了所有可能被渲染的组件/HTMLElement 的代码,包括用于增量渲染、调度、事件处理等的代码。但是有些应用程序(在初始页面加载时)不需要所有这些特性。对于这样的应用程序,使用原生DOM操作来构建交互式用户界面可能更好。

Benedikt Rotsch 的文章 中,展示了一个从 Moment.jsdate-fns,在 3G 和低端手机上首次使用时,可以节省大约 300ms 的时间的转变。

34. 您是否正在使用 JavaScript 块的预测预取

我们可以使用 heuristics 来决定何时预加载 JavaScript 块。Guess.js 是一套使用谷歌分析数据的工具和库,用来确定用户最可能从特定页面访问的页面。根据从谷歌分析或其他来源收集的用户导航模式,Guess.js 构建了一个机器学习模型来预测和预取每个后续页面所需的 JavaScript。

因此,每个交互元素都会收到一个参与概率得分,基于这个分数,客户端脚本决定提前预取资源。你可以把这项技术应用到 Next.js applicationAngular和React,还有一个 Webpack 插件,它也可以自动完成设置过程。

显然,很明显,你可能会让浏览器读取不需要的数据并提前获取不需要的页面,所以在这些预先获取的请求的数量上做个相对保守的选择是个好主意。一个好的用例在检查过程中预取所需的验证脚本,或者在关键的动作调用进入视图时进行预取。

需要不那么复杂的东西吗?Quicklink 是一个小库,它在空闲时间自动预取视图中的链接,以加快加载下一页导航的速度。不过,它也考虑到了数据,因此它不会在 2G 或 Data-Saver 时预取。

####35. 利用针对目标 JavaScript 引擎的优化

研究 JavaScript 引擎在用户基础中占的比例,然后探索优化它们的方法。例如,当优化的 V8 引擎是用在 Blink 浏览器,Node.js 运行和 Electron 的时候,对每个脚本使用脚本流。一旦下载开始,它允许 asyncdefer scripts在一个单独的后台线程进行解析,因此在某些情况下,提高 10% 的页面加载时间。实际上,在 <head> 中使用 <script defer>,以便 浏览器更早地可以发现资源,然后在后台线程中解析它。

警告:Opera Mini 不支持 defement 脚本,如果你正在印度和非洲从事开发工作,defer 将会被忽略,导致阻塞渲染直到脚本加载(感谢 Jeremy)!。

渐进引导:使用服务器端渲染获得首次有效绘制,但也包含一些最小必要的 JavaScript 来保持实时交互来接近首次有效绘制。

36. 客户端渲染还是服务器端渲染

在两种场景下,我们的目标应该是建立 渐进引导:使用服务端呈现获得首次有效绘制,而且还要包含一些最小必要的 JavaScript 来保持实时交互来接近首次有效绘制。如果 JavaScript 在首次有效绘制没有获取到,那么浏览器可能会在解析时 锁住主线程,编译和执行最新发现的 JavaScript,从而对 站点或应用程序交互性 造成限制。

为了避免这样做,总是将执行函数分离成一个个,异步任务和可能用到 requestIdleCallback 的地方。考虑 UI 的懒加载部分使用 WebPack 动态 import()支持,避免加载、解析和编译开销直到用户真的需要他们(感谢 Addy!)。

在本质上,交互时间(TTI)告诉我们导航和交互之间的时间。度量是通过在窗口初始内容呈现后的第一个五秒来定义的,在这个过程中,JavaScript 任务都不超过 50ms。如果发生超过 50ms 的任务,则重新开始搜索五秒钟的窗口。因此,浏览器首先会假定它达到了交互式(Interactive),只是切换到冻结状态(Frozen),最终切换回交互式(Interactive)。

一旦我们达到交互式(Interactive),然后,我们可以按需或等到时间允许,启动应用程序的非必需部分。不幸的是,随着 Paul Lewis 提到的,框架通常没有优先级的概念,因此渐进式引导很难用大多数库和框架实现。如果你有时间和资源,使用该策略可以极大地改善前端性能。

是在客户端渲染还是服务器端渲染?如果对用户的体验没有明显的提升,那么 可能我们不会在客户端渲染,因为在实际情况中,服务器端渲染的HTML可能更快。或许,您可以 用静态站点生成器预先加载一些内容 直接推到 CDN 上,并在顶部使用一些 JavaScript。

将客户端框架的使用限制在绝对需要它们的页面上。为了防止服务器渲染比客户端渲染慢,可以考虑 在构建时预渲染 和动态 CSS 内联,以生成可用于生产的静态文件。Addy Osmani 做了一个关于 JavaScript 成本精彩演讲,值得一看。

37. 限制第三方脚本的影响

随着所有性能优化的到位,我们常常无法控制来自业务需求的第三方脚本。第三方脚本的度量不受用户体验的影响,所以,一个单一的脚本常常会以调用令人讨厌的,长长的第三方脚本为结尾,因此,破坏了为性能专门作出的努力。为了控制和减轻这些脚本带来的性能损失,仅异步加载(可能通过 defer)和通过资源提示,如:dns-prefetch 或者 preconnect 加速他们是不足够的。

正如 Yoav Weiss 在他的 必须关注第三方脚本的通信 中解释的,在很多情况下,下载资源的这些脚本是动态的。页面负载之间的资源是变化的,因此我们不知道主机是从哪下载的资源以及这些资源是什么。

这时,我们有什么选择?考虑通过一个超时来使用 service workers 下载资源,如果在特定的时间间隔内资源没有响应,返回一个空的响应告知浏览器执行解析页面。你可以记录或者限制那些失败的第三方请求和没有执行特定标准请求。您还可以选择,从 您自己的服务器 而不是从供应商的服务器 加载第三方脚本

Casper.com 发表了一篇详细的案例研究,讲述了他们是如何通过自我托管的优化,使网站缩短了 1.7s。这也许是值得的。

另一个选择是建立一个 内容安全策略(CSP) 来限制第三方脚本的影响,比如:不允许下载音频和视频。最好的选择是通过 <iframe> 嵌入脚本使得脚本运行在 iframe 环境中,因此如果没有接入页面 DOM 的权限,在你的域下不能运行任何代码。Iframe 可以 使用 sandbox 属性进一步限制,因此你可以禁止 iframe 的任何功能,比如阻止脚本运行,阻止警告、表单提交、插件、访问顶部导航等等。

例如,它可能必须要允许脚本运行 <iframe sandbox="allow-scripts">。每一个限制都可以通过多种 [allowsandbox 属性中(几乎处处支持)解除,所以将它们限制在允许做的最低限度。

考虑使用Intersection Observer;这将使广告嵌入 iframe 的同时仍然调度事件或需要从 DOM 获取信息(例如广告知名度)。注意新的策略如 功能策略、资源的大小限制、CPU 和带宽优先级限制损害的网络功能和会减慢浏览器的脚本,例如:同步脚本,同步 XHR 请求,document.write 和超时的实现。

对第三方进行压力测试,在 DevTools 上自底向上概要地检查页面的性能,测试在请求被阻止或超时后会发生什么情况,对于后者,你可以使用 WebPageTest 的 Blackhole 服务器 Blackhole .webpagetest.org,你可以在你的 hosts 文件中指定特定的域名。最好是 自主主机并使用单个主机名,但是同时 生成一个请求映射,当脚本变化时,暴露给第四方调用和检测。您可以使用 Harry Roberts 的方法来 审计第三方,并生成 这样的 电子表格。Harry 还在他 关于第三方性能和审计的演讲 中解释了审计工作流。

图片来源: Harry Roberts

38. 正确设置 HTTP 缓存头

再次检查一遍 expirescache-controlmax-age 和其他 HTTP cache 头部都是否设置正确。通常,资源应该是可缓存的,不管是短时间的(如果它们很可能改变),还是无限期的(如果它们是静态的)——你可以在需要更新的时候,改变它们 URL 中的版本即可。在任何资源上禁止头部 Last-Modified 都会导致一个 If-Modified-Since 条件查询,即使资源在缓存中。与 Etag 一样,即使它在使用中。

使用 Cache-control: immutable,其实是为了解决fingerprinted静态资源的缓存问题而被设计出来的,解决了客户端revalidation问题(截至 2017年12月,在 FireFox,Edge 和 Safari 中支持;只有 FireFox 在 https:// 中支持)。实际上,HTTP存档中的所有页面,2% 的请求和 30% 的站点几乎都包含 至少1个不可更改的响应。此外,大多数使用它的网站都有一个指令,这个指令是对资源具有较长的新鲜度生命周期。

还记得 stale-while-revalidate 吗?您可能知道,我们使用 Cache-Control 响应头指定缓存时间,例如:Cache-Control: max-age=604800。等到 604800s 过后,缓存会重新获取请求的内容,这直接导致页面加载速度变慢。我们可以使用 stale-while-revalidate 来避免这种情况的发生:它基本上是定义了一个额外的时间窗口,只要在此期间后台重新验证它的异步性,那么缓存久可以直接使用过期的资源。因此,它在客户端可以 隐藏 延迟(包括网络和服务器上的延迟)。

在2018年10月,Chrome 发布了一个 意图,打算在 HTTP Cache-Control 报头中提供 stale-while-revalidate,因此,随着过期资源不再处于关键的状态,它将会提高后续页面的加载时间。结果:重复视图的RTT为零

你也可以使用 Heroku 的 HTTP 缓存头部,Jake Archibald 的 缓存最佳实践(Caching Best Practices) ,以及 Ilya Grigorik 的 HTTP缓存入门(HTTP caching primer) 作为指导。而且,注意 不同的头部,尤其是在 关系到 CDN 时,并且要注意 关键头文件,有助于避免在新请求稍有差异时进行额外的验证,但从以前请求标准,并不是必要的(感谢,Guy!)。

另外,要仔细检查您是否发送了 不必要的标题 (例如,x-power -bypragmax-ua-compatibleexpires等),以及是否包含了 有用的安全性和性能标题 (例如 Content-Security-PolicyX-XSS-ProtectionX-Content-Type-Options等)。最后,请记住单页面应用程序中 CORS请求的性能成本

交付优化

39. 异步加载 JavaScript

当用户请求页面时,浏览器获取 HTML 并构造 DOM,然后获取 CSS 并构造 CSSOM,然后通过匹配 DOM 和 CSSOM 生成一个渲染树。如果有任何的 JavaScript 需要解决,浏览器将不会开始渲染页面,直到 JavaScript 解决完毕,这样就会延迟渲染。 作为开发人员,我们必须明确告诉浏览器不要等待并立即开始渲染页面。 为脚本执行此操作的方法是使用 HTML 中的 deferasync 属性。

事实证明,我们 应该 把 async 改为 defer(因为 ie9 及以下 不支持 async)。根据 Steve Souders 的说法,一旦加载到 async 脚本时,它们就会立即执行。如果这种情况发生得非常快,并且 async 脚本已经处于缓存中时,它实际上会阻塞 HTML 解析器。所以我们可以使用延迟,来保证浏览器在解析 HTML 之前不会执行脚本。因此,除非您在开始渲染之前需要执行 JavaScript ,否则最好使用 defer

另外,从上面的描述来看,我们需要限制第三方库和脚本的影响,特别是使用社交共享按钮和嵌入的 <iframe>(如地图)。大小限制 有助于 防止 JavaScript 库过大:如果您不小心添加了大量依赖项,该工具将通知您并抛出错误。您可以使用静态社交分享按钮(如通过 SSBG)和 静态链接来代替交互式地图。您还可以 修改非阻塞脚本加载程序以实现 CSP 合规性

40. 使用 IntersectionObserver 延迟加载昂贵的组件

如果您需要延迟加载图片、视频、广告脚本、A/B 测试脚本或任何其他资源,最有效的方法是使用 Intersection Observer API,该 API 提供了一种 异步观察目标元素与祖先元素或顶级文档视口的交集变化的方法。基本上,您需要创建一个新的 IntersectionObserver 对象,它接收回调函数和一组选项。然后我们添加一个目标来观察。

当目标变得可见或不可见时,回调函数就会执行,因此当它拦截viewport时,您可以在元素变得可见之前开始执行一些操作。事实上,我们可以精确地控制观察者的回调何时被调用,使用 rootMarginthreshold(一个数字或者一个数字数组来表示目标可见度的百分比)。

Alejandro Garcia Anglada 发表了一个关于如何在实际应用场景当中去实现它的 简易教程,Rahul Nanwani 写了一篇详细介绍 关于延迟加载前景和背景图像文章,Google Fundamentals 也提供了一篇 关于 Intersection Observer 延迟加载图像和视频详细教程。您也可以 使用 Intersection Observer 实现 高效的滚动测试(performant scrollytelling)

另外,请了解一下 lazyload,它是一个允许我们指定哪些图像和 iframes 应该本地延迟加载的 attribute。LazyLoad 将提供一种允许我们在每个域的基础上强制选择是否使用 LazyLoad 功能的机制(类似于 内容安全策略 的工作方式)。额外的好处:一旦发布,优先级提示将允许我们在标题中指定脚本和预加载的重要性(目前在 Chrome Canary 中)。

41. 逐步加载图像

你甚至可以通过向你的网页添加 渐进式图片加载 来将延迟加载提升到新的水平。 与 Facebook,Pinterest 和 Medium 类似,你可以先加载低质量或模糊的图像,然后当页面继续加载时,使用 Guy Podjarny 提出的 LQIP (Low Quality Image Placeholders) technique(低质量图像占位符)技术 替换它们的清晰版本。

如果技术改善了用户体验,观点就不一样了,但它肯定会提高第一次有意义的绘画的时间。我们甚至可以通过使用 SQIP 创建图像的低质量版本作为 SVG 占位符或者使用带有 CSS 线性渐变的 渐变图像占位符 来实现自动化。 这些占位符可以嵌入 HTML 中,因为它们自然可以用文本压缩方法压缩。 Dean Hume 在 他的文章 中描述了如何使用 Intersection Observer 来实现这种技术。

浏览器支持吗?可以说相当好,Chrome、Firefox、Edge 和 Samsung Internet 这些浏览器都已经支持。WebKit 目前 正在开发中。如果浏览器不支持呢? 如果不支持Intersection Observer,我们仍然可以 延迟加载 一个 polyfill 或立即加载图像。甚至还有一个 库(library)

想把延迟加载做到极致?您可以尝试着 跟踪图像,并使用原始形状和边缘创建轻量级SVG占位符,先加载它,然后从占位符矢量图像过渡到(加载的)位图图像。

Jose M. Perez的 SVG 延迟加载技术

42. 快速推送关键 CSS

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

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

使用 HTTP/2,关键 CSS 可以存储在一个单独的 CSS 文件中,并通过 服务器推送 来传递,而不会增大 HTML 的大小。 问题在于,服务器推送是很 麻烦,因为浏览器中存在许多问题和竞争条件。 它一直不被支持,并有一些缓存问题(参见 [Hooman Beheshti介绍的文章](Hooman Beheshti's presentation) 114 页内容)。事实上,这种影响可能是 负面的,会使网络缓冲区膨胀,从而阻止文档中的真实帧被传送。 而且,由于 TCP 启动缓慢,似乎服务器推送 在热连接上更加有效

即使使用 HTTP/1,将关键 CSS 放在根目录上的单独文件中也是 有好处 的,有时甚至比缓存和内联更为有效。 Chrome 请求这个页面的时候会再发送一个 HTTP 连接到根目录,从而不需要 TCP 连接来获取这个 CSS。(感谢 Philip!)

需要注意的一些问题是:和 preload 不同的是,preload 可以触发来自任何域的预加载,而你只能从你自己的域或你所授权的域中推送资源。 一旦服务器得到来自客户端的第一个请求,就可以启动它。 服务器将资源压入缓存,并在连接终止时被删除。 但是,由于可以在多个选项卡之间重复使用 HTTP/2 连接,所以推送的资源也可以被来自其他选项卡的请求声明。(感谢 Inian!)

目前,服务器并没有一个简单的方法得知被推送的资源是否已经存在于 用户的缓存中,因此每个用户的访问都会继续推送资源。因此,您可能需要创建一个缓存 监测 HTTP/2 服务器推送机制。如果被提取,您可以尝试从缓存中获取它们,这样可以避免再次推送。

但请记住,新的 cache-digest 规范 无需手动建立这样的 缓存感知 的服务器,基本上在 HTTP/2 中声明的一个新的帧类型就可以表达该主机的内容。因此,它对于 CDN 也是特别有用的。

对于动态内容,当服务器需要一些时间来生成响应时,浏览器无法发出任何请求,因为它不知道页面可能引用的任何子资源。 在这种情况下,我们可以预热连接并增加 TCP 拥塞窗口大小,以便将来的请求可以更快地完成。 而且,所有内联配置对于服务器推送都是较好的选择。事实上,Inian Parameshwaran 对 HTTP/2 Push 和 HTTP Preload 进行了比较 深入的研究,内容很不错,其中包含了您可能需要的所有细节。服务器到底是推送还是不推送呢?你可以阅读一下 Colin Bendell 的 Should I Push?。可能会给你指出正确的方向。

一句话:正如 Sam Saccone 所说preload 有利于将资源的开始下载时间更接近初始请求, 而服务器推送是一个完整的 RTT(或 更多,这取决于您的服务器反应时间) —— 如果你有一个服务器可以防止不必要的推送。

43. 尝试重新组合 CSS 规则

我们经常要使用到关键的 CSS,但是还有一些优化可以做得更好。哈里·罗伯茨进行了一项引人注目的研究,得出了相当惊人的结果。例如,将主CSS文件拆分为独立的媒体查,这样,浏览器将检索具有高优先级的关键CSS,然后把具有低优先级的其他所有内容分离到非关键CSS当中。

此外,避免 <link rel="stylesheet" />async 代码片段之前放置。如果一个脚本不依赖于任何样式表,可以考虑将独立脚本放在独立样式之上。如果是这样,我们可以将这个脚本分割成一个单独的模块,并将其加载到 CSS 的任何一侧。

Scott Jehl 通过 使用 service worker 缓存内联CSS文件 解决了另一个有趣的问题。基本上,我们在 style 元素上添加一个 ID 属性,以便使用 JavaScript 快速引用,然后一小段 JavaScript 找到目标 CSS 并使用 Cache API 将其存储在本地浏览器缓存中(内容类型为 text/css),以便在后续页面上使用。所以我们为了避免在后续页面上引用内联 CSS 并在外部引用缓存资产,就会在第一次访问站点时设置 cookie。

你使用 流响应 吗?通过流,在初始导航请求中呈现的 HTML 可以充分利用浏览器的流式 HTML 解析器。

44. 流响应

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

我们可以从多个源创建一个流。例如,您可以让 service worker 构建一个 streams,其中框架 (shell)来自缓存,内容来自网络的流,而不是提供一个空的 UI 外壳并让JavaScript填充它。正如 Jeff Posnick 指出的,如果您的 web 应用程序由 CMS 提供支持的,那么服务器渲染 HTML 是通过将部分模板拼接在一起来呈现的,该模型将直接转换为使用流式响应,而模板逻辑将从服务器复制而不是你的服务器。Jake Archibald 的 The Year of Web Streams 文章重点介绍了如何构建它。对于性能的提升是非常明显

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

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

浏览器支持程度如何呢?使用 Chrome 52 +,Firefox 57+,Safari 和 Edge,支持所有现代浏览器 支持的API和 service worker。

45. 考虑使组件连接/设备内存感知

特别是在新兴市场工作时,你可能需要考虑优化用户选择节省数据的体验。保存数据客户端提示请求头 允许我们为成本和性能受限的用户定制应用程序和有效载荷。实际上,您可以 将高DPI图像的请求重写为低DPI图像,删除Web字体和花哨的特效,预览缩略图和无限滚动,关闭视频自动播放,服务器推送,减少显示项目数量,降低图像质量,甚至更改 提供标记的方式。Tim Vereecke 发表了一篇关于 Data-s(h)aver 策略 的详细文章,其中包含许多 Data-saver 选项。

该头部目前仅支持 Chromium,Android 版 Chrome 或 桌面设备上的 Data Saver 扩展。当然,您还可以使用 Network Information API 根据网络类型交付 低/高分辨率的图像 和视频。网络信息API 和 navigator.connection.effectiveType (Chrome 62+),都是使用 RTT 值、下行(downlink)值、有效类型(effectiveType)值(和其他一些值)来表示连接和用户可以处理的数据。

在这种情况下,Max Stoiber 谈到了 连接感知组件。例如,使用 React,我们编写一个组件,在不同的渲染方式下可能会加载成不同的元素。Max建议,一个 <Media /> 组件在新闻板块的输出可能会出现情况:

  • Offline:带 alt 文本的占位符
  • 2G / save-data mode:低分辨率图像
  • 3G 在非Retina屏幕上:中等分辨率图像
  • 3G 在Retina屏幕上:高分辨率Retina图像
  • 4G 高清视频

Dean Hume使用service worker提供了一个 类似逻辑的实际实现。对于视频,我们可以默认显示一个视频海报,然后在更好的连接上显示 播放 图标以及视频播放器外壳、视频的元数据等。对于不支持的浏览器,我们可 以侦听canplaythrough事件,如果 canplaythrough 事件在 2s 内没有触发,则使用 Promise.race() 来超时加载资源。

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

网络连接只是为用户提供了一个视口。如果想更进一步,您还可以使用 Device Memory API(Chrome 63+)根据可用的设备内存动态调整资源。返回设备有多少RAM(千兆字节),向下舍入到最接近的2的幂。navigator.deviceMemory 返回设备的RAM大小(千兆字节),四舍五入到最接近的2次方。该API还具有一个客户端提示头,Device-Memory,并且给出相同的值。

DevTools 中的 Priority 列。图片来源:Ben Schwarz,重要的请求

47. 预热连接以加快传输速度

使用 资源提示 来节约时间,如 dns-prefetch(在后台执行 DNS 查询),preconnect(告诉浏览器在后台进行连接握手(DNS, TCP, TLS)),prefetch(告诉浏览器请求一个资源) 和 preload(预先获取资源而不执行他们)。

大多数时候,我们至少会使用 preconnectdns-prefetch,我们会小心使用 prefetchpreload;前者只能在你非常确定用户后续需要什么资源的情况下使用(类似于采购渠道)。注意,prerender 已被弃用,不再被支持。

请注意,即使使用 preconnectdns-prefetch,浏览器也会对它将并行查找或连接的主机数量进行限制,因此最好是将它们根据优先级进行排序(感谢 Philip!)。

事实上,使用资源提示可能是最简单的提高性能的方法,它确实很有效。什么时候该使用呢?Addy Osmani已经做了 解释,我们应该预加载确定将在当前页面中使用的资源。预获取可能用于未来页面的资源,例如用户尚未访问的页面所需的 Webpack 包。

Addy 的 关于 Chrome 中加载优先级 的文章展示了Chrome 是如何精确地解析资源提示的,因此一旦你决定哪些资源对页面渲染比较重要,你就可以给它们赋予比较高的优先级。你可以在 Chrome DevTools 网络请求表格(或者 Safari Technology Preview)中启动 priority 列来查看你的请求的优先级。

例如,由于字体通常是页面上的重要资源,所以最好使用 preload 请求浏览器下载字体。你也可以 动态加载 JavaScript,从而有效的执行延迟加载。同样的,因为 <link rel="preload"> 接收一个 media 属性,你可以基于媒体查询规则来 有选择性地优先加载资源

需要注意的一些问题是:preload 可以将 资源的下载时间 移到请求开始时,但是这些缓存在内存中的预先加载的资源是绑定在所发送请求的页面上,也就是说预先加载的请求不能被页面所共享。而且,preload 与 HTTP 缓存配合得也很好:如果缓存命中则不会发送网络请求。

因此,它对后发现的资源也非常有用,如:通过 background-image 加载的一幅 hero image,内联关键 CSS (或 JavaScript),并预先加载其他 CSS (或 JavaScript)。

此外,只有当浏览器从服务器接收 HTML,并且前面的解析器找到了 preload 标签后,preload 标签才可以启动预加载。

由于我们不等待浏览器解析 HTML 以启动请求,所以通过 HTTP 头进行预加载要快一些。Early Hints 将进一步提供帮助,甚至可以在发送HTML的响应标头之前启用预加载,而 Priority Hints即将推出)将帮助我们指示脚本的加载优先级。

请注意:如果你正在使用 preloadas 必须定义,否则什么都不会加载;还有,预加载字体时如果没有 crossorigin 属性将会获取两次。

48. 使用 Service workers 进行缓存和网络回退

网络上的任何性能优化都不会比用户计算机上的本地存储缓存更快。如果您的网站是通过 HTTPS 运行的,请使用 Service Workers 实用指南 将静态资源缓存在 service worker 缓存中,并存储脱机回退(甚至离线页面),然后从用户的计算机中检索它们,而不是转到网络。另外,查看 Jake 的 Offline Cookbook 和免费的 Udacity课程 离线Web应用程序

浏览器支持吗?如上所述,它得到了广泛的支持(Chrome、Firefox、Safari TP、三星互联网、Edge 17+),无论如何,网络是它的退路。它有助于提高性能吗?是的,而且它正在变得更好,例如使用Background Fetch允许service worker进行后台上传/下载,运送到Chrome 71

service worker有许多用例。例如,您可以 实现 保存为脱机 特性处理损坏的图像在选项卡之间引入消息传递,或者 根据请求类型提供不同的缓存策略。通常,一种常见的可靠策略是将应用程序 shell 与一些关键页面一起存储在 service worker 的缓存中,例如离线页面、首页和其他可能对您的应用程序很重要的页面。

但是要记住一些问题。在有了service worker之后,我们需要 在Safari中提防范围请求(如果您正在为服务工作者使用 Workbox,它有一个范围请求模块)。如果您偶然发现控台出现 DOMException:Quota exceeded. 这样的错误提示,那么请查看 Gerardo 的文章 当7KB等于7MB时

正如 Gerardo 所写的一样,如果您正在构建一个渐进式Web应用程序,并且当您的 service worker 缓存来自 CDN 的静态资源时,这时候您的缓存存储空间会非常大。所以请确保对于跨源资源 存在正确的 CORS 响应头,这样您就不会在无意中使用 Service Workers 缓存不透明的响应,您还可以向 < img > 标记添加 crossorigin 属性来 选择将跨源图像资源缓存到 CORS 模式中

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

49. 您是否使用 CDN / Edge 上的 Service workers(例如,进行A / B测试)

这时候,我们已经习惯于在客户端上运行 Service workers,但是随着 CDNs 在服务器上实现它们,我们还可以使用它来调整边缘性能。

例如,在A / B测试中,当 HTML 需要为不同用户改变其内容时,我们可以 使用 CDN 服务器上的 Service Workers 来处理逻辑。我们还可以对 HTML 重写 进行 流式处理,以加快使用 Google 字体的网站的速度。

50. 您是否优化了渲染性能

使用 CSS containment 隔离昂贵的组件 - 例如,限制浏览器样式、用于非画布导航的布局和绘画工作,第三方组件的范围。确保在滚动页面没有延迟,或者当一个元素进行动画时,持续地达到每秒 60 帧。如果这是不可能的,那么至少要使每秒帧数持续保持在 60 到 15 的范围。使用 CSS 的 will-change 通知浏览器哪个元素的哪个属性将要发生变化。

此外,评估 运行时渲染性能(例如,使用 DevTools)。可以通过学习 Paul Lewis 的 关于浏览器渲染优化的 Udacity 课程(免费)和 Georgy Marchuk 关于浏览器绘制和web性能注意事项 的文章。

如果你想更深入地了解这个主题,Nolan Lawson 分享了一篇文章叫做 准确测量布局性能的技巧,而 Jason Miller 也给出了一个建议 替代的技术。同样,我们还有一篇由 Sergey Chikuyonok 写的关于 如何正确使用 GPU 动画 的文章。注意:对 GPU-composited 层的更改是代价最小 的,如果你能通过 opacitytransform 来触发合成,那么你就是在正确的道路上。Anna Migas 在她关于 调试UI渲染性能 的演讲中也提供了很多实用的建议。

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

组件以何种顺序显示在页面上以及我们如何给浏览器提供资源固然重要,但是我们也不应该低估了 感知性能 的作用。这一概念涉及到等待的心理学,主要是让用户在其他事情发生时保持忙碌。这就涉及到了 感知管理优先开始提前完成宽容管理

这一切意味着什么?在加载资源时,我们可以尝试始终领先于客户一步,所以将很多处理放置到后台,响应会很迅速。让客户参与进来,我们可以用 骨架屏幕(实例演示),而不是当没有更多优化可做时,用加载指示或者添加一些动画/过渡来 欺骗用户体验。但是要注意:在部署之前应该对骨架屏幕进行测试,因为一些 测试显示,从所有指标来看,骨架屏幕的 性能是最差的