2021 年前端性能清单(PDF、Apple Pages、MS Word)

已发表: 2022-03-10
快速总结↬让2021年……快点! 年度前端性能检查表(提供 PDF、Apple Pages、MS Word),包含您在当今网络上创建快速体验所需了解的一切,从指标到工具和前端技术。 自 2016 年以来更新。啊,您还可以在我们的电子邮件通讯中获得有用的前端提示。

Web 性能是一个棘手的问题,不是吗? 我们如何真正知道我们在性能方面的立场,以及我们的性能瓶颈到底是什么? 是昂贵的 JavaScript、缓慢的网络字体交付、沉重的图像还是缓慢的渲染? 我们是否对 tree-shaking、范围提升、代码分割以及所有带有交叉点观察器、渐进式水化、客户端提示、HTTP/3、服务工作者和 - 哦我的 - 边缘工作者的花哨的加载模式进行了足够的优化? 而且,最重要的是,我们甚至从哪里开始提高绩效,我们如何建立长期的绩效文化?

过去,性能往往只是事后才想到的。 通常推迟到项目的最后,它可以归结为缩小、连接、资产优化以及可能对服务器的config文件进行一些微调。 现在回想起来,事情似乎发生了很大的变化。

性能不仅仅是一个技术问题:它影响从可访问性到可用性再到搜索引擎优化的方方面面,并且在将其纳入工作流程时,必须根据其性能影响来制定设计决策。 性能必须不断地被测量、监控和改进,而网络日益复杂的问题带来了新的挑战,使得跟踪指标变得困难,因为数据会因设备、浏览器、协议、网络类型和延迟而有很大差异( CDN、ISP、缓存、代理、防火墙、负载平衡器和服务器都在性能中发挥作用)。

所以,如果我们对提高性能时必须牢记的所有事情进行概述——从项目开始到网站的最终发布——那会是什么样子? 您将在下面找到2021 年(希望是公正和客观的)前端性能检查表— 对您可能需要考虑的问题的更新概述,以确保您的响应时间快速、用户交互顺畅且您的网站不消耗用户的带宽。

目录

  • 全部在单独的页面上
  • 准备:计划和指标
    绩效文化、Core Web Vitals、绩效概况、CrUX、Lighthouse、FID、TTI、CLS、设备。
  • 设定切合实际的目标
    性能预算、性能目标、RAIL 框架、170KB/30KB 预算。
  • 定义环境
    选择框架、基线性能成本、Webpack、依赖项、CDN、前端架构、CSR、SSR、CSR + SSR、静态渲染、预渲染、PRPL 模式。
  • 资产优化
    Brotli、AVIF、WebP、响应式图像、AV1、自适应媒体加载、视频压缩、网络字体、谷歌字体。
  • 构建优化
    JavaScript 模块、模块/无模块模式、tree-shaking、代码拆分、范围提升、Webpack、差异服务、Web Worker、WebAssembly、JavaScript 包、React、SPA、部分水合、交互导入、第 3 方、缓存。
  • 交付优化
    延迟加载、交叉点观察器、延迟渲染和解码、关键 CSS、流式传输、资源提示、布局转换、服务工作者。
  • 网络、HTTP/2、HTTP/3
    OCSP 装订、EV/DV 证书、打包、IPv6、QUIC、HTTP/3。
  • 测试和监控
    审核工作流程、代理浏览器、404 页面、GDPR cookie 同意提示、性能诊断 CSS、可访问性。
  • 快速获胜
  • 下载清单(PDF、Apple Pages、MS Word)
  • 我们走了!

(您也可以只下载清单 PDF (166 KB) 或下载可编辑的 Apple Pages 文件 (275 KB) 或 .docx 文件 (151 KB)。祝大家优化愉快!)

准备:计划和指标

微优化对于保持性能正常运行非常有用,但牢记明确定义的目标至关重要——可衡量的目标会影响整个过程中做出的任何决策。 有几种不同的模型,下面讨论的模型非常固执——只要确保尽早设定自己的优先级即可。

  1. 建立绩效文化。
    在许多组织中,前端开发人员确切地知道常见的潜在问题是什么以及应该使用什么策略来解决这些问题。 然而,只要没有对绩效文化的既定认可,每个决策都会变成部门的战场,将组织打碎成孤岛。 您需要业务利益相关者的支持,并且要获得它,您需要建立案例研究或概念证明,以证明速度(尤其是我们稍后将详细介绍的核心 Web Vitals )收益指标和关键绩效指标( KPI )他们关心。

    例如,为了使性能更加有形,您可以通过显示转换率和应用程序加载时间以及渲染性能之间的相关性来揭示收入性能的影响。 或者搜索机器人的爬取率(PDF,第 27-50 页)。

    如果开发/设计和业务/营销团队之间没有强有力的一致性,绩效将无法长期维持。 研究客户服务和销售团队的常见投诉,研究高跳出率和转化率下降的分析。 探索提高性能如何帮助缓解其中一些常见问题。 根据您正在与之交谈的利益相关者群体调整论点。

    在移动设备和桌面设备上运行性能实验并衡量结果(例如,使用 Google Analytics)。 它将帮助您使用真实数据建立公司量身定制的案例研究。 此外,使用 WPO Stats 上发布的案例研究和实验数据将有助于提高企业对性能为何重要以及它对用户体验和业务指标的影响的敏感性。 但是,仅说明性能很重要是不够的——您还需要建立一些可衡量和可跟踪的目标,并随着时间的推移观察它们。

    如何到那? 在她关于建立长期绩效的演讲中,艾莉森麦克奈特分享了一个关于她如何帮助在 Etsy 建立绩效文化的综合案例研究(幻灯片)。 最近,Tammy Everts 谈到了小型和大型组织中高效绩效团队的习惯。

    在组织中进行这些对话时,重要的是要记住,就像 UX 是一系列体验一样,Web 性能也是一种分布。 正如 Karolina Szczur 指出的那样,“期望一个数字能够提供令人渴望的评级是一个有缺陷的假设。” 因此,绩效目标需要细化、可跟踪和有形。

在移动设备上,每个会话体验过快速加载时间的用户带来的收入比平均水平高出 17%
在移动设备上,每个会话体验过快速加载时间的用户带来的收入比平均水平高出 17%。 (Web 性能的影响,来自 Addy Osmani)
期望单个数字能够提供期望的评级是一个有缺陷的假设
期望单个数字能够提供期望的评级是一个有缺陷的假设。 (图片来源:性能是通过 Karolina Czczur 发布的)
  1. 目标:比你最快的竞争对手至少快 20%。
    根据心理学研究,如果你想让用户觉得你的网站比竞争对手的网站快,你需要至少快 20%。 研究您的主要竞争对手,收集有关他们在移动和桌面上的表现的指标,并设置有助于您超越他们的阈值。 不过,要获得准确的结果和目标,请确保首先通过研究分析来全面了解用户体验。 然后,您可以模仿第 90 个百分位数的体验进行测试。

    为了对竞争对手的表现有一个良好的第一印象,您可以使用 Chrome UX Report( CrUX ,一个现成的 RUM 数据集,Ilya Grigorik 的视频介绍和 Rick Viscomi 的详细指南),或 Treo,一个 RUM 监控工具由 Chrome 用户体验报告提供支持。 这些数据是从 Chrome 浏览器用户那里收集的,因此报告将是特定于 Chrome 的,但它们会为您提供相当全面的性能分布,最重要的是 Core Web Vitals 分数,分布在您的各种访问者中。 请注意,新的 CrUX 数据集在每个月的第二个星期二发布。

    或者,您也可以使用:

    • Addy Osmani 的 Chrome 用户体验报告比较工具,
    • 速度记分卡(还提供收入影响估算器),
    • 真实用户体验测试比较或
    • SiteSpeed CI(基于综合测试)。

    注意:如果您使用 Page Speed Insights 或 Page Speed Insights API(不,它没有被弃用!),您可以获得特定页面的 CrUX 性能数据,而不仅仅是聚合。 此数据对于为“着陆页”或“产品列表”等资产设置性能目标可能更有用。 如果您使用 CI 测试预算,如果您使用 CrUX 设置目标,则需要确保您的测试环境与 CrUX 匹配(感谢 Patrick Meenan! )。

    如果您需要一些帮助来展示速度优先级背后的原因,或者您希望在性能较慢的情况下可视化转换率衰减或跳出率增加,或者您可能需要在您的组织中倡导 RUM 解决方案,Sergey Chernyshev 构建了一个 UX 速度计算器,这是一个开源工具,可帮助您模拟数据并将其可视化以推动您的观点。

    CrUX 生成随时间推移的性能分布概览,并从 Google Chrome 用户收集流量
    CrUX 生成随时间推移的性能分布概览,并收集来自 Google Chrome 用户的流量。 您可以在 Chrome UX Dashboard 上创建自己的。 (大预览)
    就在您需要证明性能以推动您的观点时:UX 速度计算器根据真实数据可视化性能对跳出率、转化率和总收入的影响
    就在您需要证明性能以推动您的观点时:UX 速度计算器根据真实数据可视化性能对跳出率、转化率和总收入的影响。 (大预览)

    有时您可能想更深入一点,将来自 CrUX 的数据与您已经必须快速找出减速、盲点和低效率所在的任何其他数据相结合——为您的竞争对手或您的项目。 在他的工作中,Harry Roberts 一直在使用 Site-Speed Topography 电子表格,他用它来按关键页面类型分解性能,并跟踪它们之间的不同关键指标。 您可以将电子表格下载为 Google 表格、Excel、OpenOffice 文档或 CSV。

    网站速度地形,其中关键指标代表网站上的关键页面
    网站速度地形,其中关键指标代表网站上的关键页面。 (大预览)

    如果您想一路走下去,您可以在站点的每个页面上运行 Lighthouse 性能审计(通过 Lightouse Parade),并将输出保存为 CSV。 这将帮助您确定竞争对手的哪些特定页面(或页面类型)表现更差或更好,以及您可能希望将精力集中在哪些方面。 (对于您自己的站点,最好将数据发送到分析端点!)。

    使用 Lighthouse Parade,您可以在站点的每个页面上运行 Lighthouse 性能审计,并将输出保存为 CSV
    使用 Lighthouse Parade,您可以在站点的每个页面上运行 Lighthouse 性能审计,并将输出保存为 CSV。 (大预览)

    以这种方式收集数据、设置电子表格、削减 20% 并设置您的目标(绩效预算)。 现在你有一些可测量的东西来测试。 如果您牢记预算并尝试仅发送最少的有效负载以快速进行交互,那么您就走在了合理的道路上。

    需要资源才能开始?

    • Addy Osmani 写了一篇非常详细的文章,内容涉及如何开始性能预算、如何量化新功能的影响以及超出预算时从哪里开始。
    • Lara Hogan 关于如何使用性能预算进行设计的指南可以为设计师提供有用的指导。
    • Harry Roberts 发布了关于设置 Google Sheet 的指南,以显示第三方脚本对性能的影响,使用 Request Map,
    • Jonathan Fielding 的绩效预算计算器、Katie Hempenius 的 perf-budget-calculator 和 Browser Calories 可以帮助创建预算(感谢 Karolina Szczur 的提醒)。
    • 在许多公司中,绩效预算不应该是雄心勃勃的,而是务实的,作为避免滑过某个点的标志。 在这种情况下,您可以选择过去两周内最差的数据点作为阈值,然后从那里获取。 绩效预算,务实地向您展示了实现这一目标的策略。
    • 此外,通过设置带有图表报告构建大小的仪表板,使性能预算和当前性能都可见。 有许多工具可以让您实现这一目标:SiteSpeed.io 仪表板(开源)、SpeedCurve 和 Calibre 只是其中的一小部分,您可以在 perf.rocks 上找到更多工具。
    浏览器卡路里可帮助您设置性能预算并衡量页面是否超过这些数字,
    浏览器卡路里可帮助您设置性能预算并衡量页面是否超过这些数字。 (大预览)

    一旦你有了预算,就可以将它们整合到你的构建过程中,使用 Webpack Performance Hints 和 Bundlesize、Lighthouse CI、PWMetrics 或 Sitespeed CI,以强制执行拉取请求的预算,并在 PR 评论中提供分数历史记录。

    要将性能预算公开给整个团队,请通过 Lightwallet 在 Lighthouse 中集成性能预算,或使用 LHCI Action 进行快速 Github Actions 集成。 如果你需要一些自定义的东西,你可以使用webpagetest-charts-api,一个端点API,从WebPagetest结果构建图表。

    不过,性能意识不应仅来自性能预算。 就像 Pinterest 一样,您可以创建一个自定义eslint规则,该规则禁止从已知依赖重的文件和目录导入,并且会使包膨胀。 设置可以在整个团队中共享的“安全”包列表。

    此外,考虑对您的业务最有利的关键客户任务。 研究、讨论和定义关键操作的可接受时间阈值,并建立整个组织已批准的“UX 就绪”用户时间标记。 在许多情况下,用户旅程将涉及许多不同部门的工作,因此在可接受的时间安排方面保持一致将有助于支持或阻止未来的性能讨论。 确保增加的资源和功能的额外成本是可见和理解的。

    将绩效工作与其他技术计划相结合,从正在构建的产品的新功能到重构,再到接触新的全球受众。 因此,每次关于进一步发展的对话发生时,性能也是对话的一部分。 当代码库是新鲜的或刚刚被重构时,实现性能目标要容易得多。

    此外,正如 Patrick Meenan 所建议的,在设计过程中计划加载顺序和权衡是值得的。 如果您尽早确定哪些部分更关键,并定义它们应该出现的顺序,您还将知道什么可以延迟。 理想情况下,该顺序也将反映 CSS 和 JavaScript 导入的顺序,因此在构建过程中处理它们会更容易。 此外,请考虑在加载页面时(例如,尚未加载 Web 字体时)处于“中间”状态的视觉体验应该是什么。

    一旦你在你的组织中建立了强大的绩效文化,目标是比你以前的自己快 20% ,以便随着时间的推移保持优先事项的机智(谢谢,Guy Podjarny! )。 但是要考虑客户的不同类型和使用行为(Tobias Baldauf 称之为节奏和群组),以及机器人流量和季节性影响。

    计划,计划,计划。 尽早进行一些快速的“唾手可得”优化可能很诱人——这可能是一个快速获胜的好策略——但如果没有计划和设置现实,公司将很难将性能放在首位- 量身定制的绩效目标。

Treo Sites 提供基于真实数据的竞争分析
Treo 提供基于真实数据的竞争分析。 (大预览)
新指标于 2020 年初登陆 Lighthouse v6
新指标于 2020 年初登陆 Lighthouse v6。(大预览)
  1. 选择正确的指标。
    并非所有指标都同样重要。 研究哪些指标对您的应用程序最重要:通常,它将由您可以多快开始渲染界面中最重要的像素以及您可以多快为这些渲染像素提供输入响应来定义。 这些知识将为您提供持续努力的最佳优化目标。 最后,定义体验的不是加载事件或服务器响应时间,而是对界面感觉有多快的感知。

    这是什么意思? 与其关注整个页面加载时间(例如,通过onLoadDOMContentLoaded时间),不如优先考虑客户认为的页面加载。 这意味着专注于一组略有不同的指标。 事实上,选择正确的指标是一个没有明显赢家的过程。

    根据 Tim Kadlec 的研究和 Marcos Iglesias 在他的演讲中的笔记,传统指标可以分为几组。 通常,我们需要所有这些来全面了解性能,在您的特定情况下,其中一些会比其他更重要。

    • 基于数量的指标衡量请求的数量、权重和性能得分。 有利于发出警报和监控随时间的变化,但不利于理解用户体验。
    • 里程碑指标使用加载过程生命周期中的状态,例如Time To First ByteTime To Interactive 。 适合描述用户体验和监控,不太适合了解里程碑之间发生的情况。
    • 渲染指标提供对内容渲染速度的估计(例如,开始渲染时间、速度指数)。 适合测量和调整渲染性能,但不适合测量重要内容何时出现并可以与之交互。
    • 自定义指标为用户测量特定的自定义事件,例如 Twitter 的 Time To First Tweet 和 Pinterest 的 PinnerWaitTime。 适合准确描述用户体验,但不适合扩展指标和与竞争对手进行比较。

    为了完成这幅图,我们通常会在所有这些组中寻找有用的指标。 通常,最具体和最相关的是:

    • 互动时间(TTI)
      布局稳定的点,关键的网络字体可见,主线程足以处理用户输入——基本上是用户可以与 UI 交互的时间标记。 了解用户必须等待多长时间才能无延迟地使用该网站的关键指标。 Boris Schapira 写了一篇关于如何可靠地测量 TTI 的详细文章。
    • 第一输入延迟(FID)输入响应
      从用户第一次与您的网站交互到浏览器实际能够响应该交互的时间。 很好地补充了 TTI,因为它描述了图片的缺失部分:当用户实际与网站交互时会发生什么。 仅用作 RUM 指标。 在浏览器中有一个用于测量 FID 的 JavaScript 库。
    • 最大含量涂料(LCP)
      当页面的重要内容可能已加载时,标记页面加载时间线中的点。 假设页面中最重要的元素是用户视口中可见的最大元素。 如果元素在折叠的上方和下方都呈现,则只有可见部分被认为是相关的。
    • 总阻塞时间 ( TBT )
      有助于量化页面在变得可靠交互之前的非交互性严重程度的指标(即,主线程在至少 5 秒内没有运行超过 50 毫秒(长任务)的任何任务)。 该指标测量第一次绘制和交互时间 (TTI) 之间的总时间量,其中主线程被阻塞足够长的时间以防止输入响应。 因此,难怪低 TBT 是良好性能的良好指标。 (谢谢,阿尔乔姆,菲尔)
    • 累积布局移位 ( CLS )
      该指标突出显示了用户在访问网站时遇到意外布局变化(重)的频率。 它检查了不稳定的元素及其对整体体验的影响。 分数越低越好。
    • 速度指数
      衡量页面内容在视觉上填充的速度; 分数越低越好。 速度指数分数是根据视觉进度的速度计算的,但它只是一个计算值。 它对视口大小也很敏感,因此您需要定义一系列与您的目标受众相匹配的测试配置。 请注意,随着 LCP 成为更相关的指标,它变得不那么重要了(感谢 Boris,Artem! )。
    • 花费的 CPU 时间
      一个指标,显示主线程被阻塞的频率时间,用于绘画、渲染、脚本和加载。 高 CPU 时间是janky体验的明确指标,即当用户在他们的操作和响应之间体验到明显的延迟时。 使用 WebPageTest,您可以在“Chrome”选项卡上选择“捕获开发工具时间线”,以在使用 WebPageTest 在任何设备上运行时公开主线程的故障。
    • 组件级 CPU 成本
      就像所花费的 CPU 时间一样,这个由 Stoyan Stefanov 提出的指标探讨了JavaScript 对 CPU 的影响。 这个想法是使用每个组件的 CPU 指令计数来单独了解它对整体体验的影响。 可以使用 Puppeteer 和 Chrome 来实现。
    • 挫折指数
      虽然上面提到的许多指标解释了特定事件何时发生,但 Tim Vereecke 的 FrustrationIndex 关注的是指标之间的差距,而不是单独查看它们。 它查看最终用户感知的关键里程碑,例如标题可见、第一个内容可见、视觉准备就绪和页面准备就绪,并计算一个分数,表明加载页面时的挫败程度。 差距越大,用户感到沮丧的机会就越大。 可能是用户体验的良好 KPI。 Tim 发表了一篇关于 FrustrationIndex 及其工作原理的详细文章。
    • 广告权重影响
      如果您的网站依赖于广告产生的收入,那么跟踪广告相关代码的权重很有用。 Paddy Ganti 的脚本构建了两个 URL(一个正常,一个阻止广告),通过 WebPageTest 提示生成视频比较并报告一个增量。
    • 偏差指标
      正如 Wikipedia 工程师所指出的,您的结果中存在多少差异的数据可以告诉您您的仪器有多可靠,以及您应该对偏差和异常值给予多少关注。 较大的差异表明设置中需要进行调整。 它还有助于了解某些页面是否更难以可靠地测量,例如由于第三方脚本导致显着变化。 跟踪浏览器版本以了解推出新浏览器版本时的性能变化可能也是一个好主意。
    • 自定义指标
      自定义指标由您的业务需求和客户体验定义。 它要求您识别重要的像素、关键脚本、必要的 CSS 和相关资产,并衡量它们交付给用户的速度。 为此,您可以监控英雄渲染时间,或使用性能 API,为对您的业务重要的事件标记特定时间戳。 此外,您可以通过在测试结束时执行任意 JavaScript 来使用 WebPagetest 收集自定义指标。

    请注意,第一次有意义的绘画(FMP)没有出现在上面的概述中。 它用于深入了解服务器输出任何数据的速度。 Long FMP 通常表示 JavaScript 阻塞了主线程,但也可能与后端/服务器问题有关。 但是,该指标最近已被弃用,因为它在大约 20% 的情况下似乎不准确。 它被有效地替换为更可靠且更易于推理的 LCP。 Lighthouse 不再支持它。 仔细检查以用户为中心的最新性能指标和建议,以确保您在安全页面上(感谢 Patrick Meenan )。

    Steve Souders 详细解释了其中许多指标。 重要的是要注意,虽然 Time-To-Interactive 是通过在所谓的实验室环境中运行自动审计来衡量的,但 First Input Delay 代表实际的用户体验,实际用户会遇到明显的滞后。 一般来说,始终测量和跟踪它们可能是一个好主意。

    根据您的应用程序的上下文,首选指标可能会有所不同:例如,对于 Netflix TV UI,按键输入响应能力、内存使用率和 TTI 更为关键,而对于 Wikipedia,第一次/最后一次视觉变化和 CPU 时间花费指标更为重要。

    注意:FID 和 TTI 都不考虑滚动行为; 滚动可以独立发生,因为它是脱离主线程的,所以对于许多内容消费网站来说,这些指标可能不那么重要(谢谢,Patrick! )。

以用户为中心的性能指标可以更好地洞察实际用户体验
以用户为中心的性能指标可以更好地了解实际用户体验。 首次输入延迟 (FID) 是一个试图实现这一目标的新指标。 (大预览)
新的核心 Web Vitals 概览,LCP < 2.5s,FID <100ms,CLS < 0.1
新的核心 Web Vitals 概览,LCP < 2.5s,FID <100ms,CLS < 0.1。 (Core Web Vitals,来自 Addy Osmani)
  1. 测量和优化 Core Web Vitals
    长期以来,性能指标都是相当技术性的,专注于服务器响应速度和浏览器加载速度的工程视图。 多年来,这些指标发生了变化——试图找到一种方法来捕捉实际的用户体验,而不是服务器时间。 2020 年 5 月,Google 发布了 Core Web Vitals,这是一组以用户为中心的新性能指标,每个指标都代表了用户体验的不同方面。

    对于其中的每一个,谷歌都推荐了一系列可接受的速度目标。 至少75% 的页面浏览量应超过良好范围才能通过此评估。 这些指标迅速获得关注,随着 Core Web Vitals 在 2021 年 5 月成为 Google 搜索的排名信号(页面体验排名算法更新),许多公司将注意力转向了他们的绩效得分。

    让我们一一分解每个核心网络生命力,以及有用的技术和工具,以优化您在考虑这些指标的情况下的体验。 (值得注意的是,通过遵循本文中的一般建议,您最终会获得更好的 Core Web Vitals 分数。)

    • 最大内容涂料( LCP ) < 2.5 秒。
      测量页面的加载,并报告在视口中可见的最大图像或文本块的渲染时间。 因此,LCP 会受到延迟渲染重要信息的所有因素的影响——无论是服务器响应时间慢、CSS 阻塞、运行中的 JavaScript(第一方或第三方)、Web 字体加载、昂贵的渲染或绘画操作、懒惰- 加载的图像、骨架屏幕或客户端渲染。

      为了获得良好的体验,LCP 应在页面首次开始加载后的2.5 秒内发生。 这意味着我们需要尽早渲染页面的第一个可见部分。 这将需要为每个模板定制关键 CSS,编排<head> -order 并预取关键资产(我们稍后会介绍)。

      LCP 分数低的主要原因通常是图像。 要在 Fast 3G 上以 <2.5 秒的时间交付 LCP(托管在经过良好优化的服务器上,全是静态的,没有客户端渲染并且图像来自专用图像 CDN)意味着最大理论图像大小仅为 144KB 左右。 这就是为什么响应式图像很重要,以及提前预加载关键图像(使用preload )。

      快速提示:要发现页面上的 LCP,在 DevTools 中,您可以将鼠标悬停在性能面板中“Timings”下的 LCP 徽章上(感谢 Tim Kadlec !)。

    • 首次输入延迟( FID ) < 100ms。
      衡量 UI 的响应能力,即浏览器在对离散的用户输入事件(如点击或单击)做出反应之前忙于其他任务的时间。 它旨在捕获因主线程繁忙而导致的延迟,尤其是在页面加载期间。

      目标是每次交互都保持在 50-100 毫秒内。 为此,我们需要识别长任务(阻塞主线程超过 50 毫秒)并将它们分解,将一个包代码拆分为多个块,减少 JavaScript 执行时间,优化数据获取,延迟第三方的脚本执行,将 JavaScript 移动到 Web 工作者的后台线程,并使用渐进式水化来降低 SPA 中的再水化成本。

      快速提示:一般来说,获得更好 FID 分数的可靠策略是通过将较大的捆绑包分解成较小的捆绑包并在用户需要时提供所需的服务,从而最大限度地减少主线程上的工作,因此不会延迟用户交互. 我们将在下面详细介绍。

    • 累积布局偏移( CLS ) < 0.1。
      测量 UI 的视觉稳定性以确保流畅和自然的交互,即在页面生命周期内发生的每个意外布局转换的所有单独布局转换分数的总和。 每当已经可见的元素更改其在页面上的位置时,就会发生单独的布局转换。 它的评分基于内容的大小和移动的距离。

      因此,每次出现转变时——例如,当备用字体和网络字体具有不同的字体指标,或者广告、嵌入或 iframe 迟到,或者图像/视频尺寸未保留,或者后期 CSS 强制重绘,或者更改由后期 JavaScript——它对 CLS 分数有影响。 良好体验的推荐值是 CLS < 0.1。

    值得注意的是,Core Web Vitals 应该随着时间的推移而发展,具有可预测的年度周期。 对于第一年的更新,我们可能期望将 First Contentful Paint 提升为 Core Web Vitals,降低 FID 阈值并更好地支持单页应用程序。 我们可能还会看到负载增加后对用户输入的响应,以及安全、隐私和可访问性 (!) 考虑。

    与 Core Web Vitals 相关,有很多有用的资源和文章值得研究:

    • Web Vitals 排行榜可让您将自己的分数与移动设备、平板电脑、台式机以及 3G 和 4G 上的竞争对手进行比较。
    • Core SERP Vitals,一个 Chrome 扩展程序,可在 Google 搜索结果中显示来自 CrUX 的 Core Web Vitals。
    • 布局转换 GIF 生成器,使用简单的 GIF 可视化 CLS(也可从命令行获得)。
    • web-vitals 库可以收集核心 Web Vitals 并将其发送到 Google Analytics、Google Tag Manager 或任何其他分析端点。
    • 使用 WebPageTest 分析 Web Vitals,Patrick Meenan 在其中探讨了 WebPageTest 如何公开有关 Core Web Vitals 的数据。
    • 使用 Core Web Vitals 进行优化,Addy Osmani 的 50 分钟视频,其中他重点介绍了如何在电子商务案例研究中改进 Core Web Vitals。
    • Cumulative Layout Shift in Practice 和 Cumulative Layout Shift in the Real World 是 Nic Jansma 撰写的综合性文章,几乎涵盖了有关 CLS 的所有内容,以及它与跳出率、会话时间或 Rage Clicks 等关键指标的关系。
    • What Forces Reflow,带有属性或方法的概述,当在 JavaScript 中请求/调用时,将触发浏览器同步计算样式和布局。
    • CSS Triggers 显示了哪些 CSS 属性触发了 Layout、Paint 和 Composite。
    • Fixing Layout Instability 是使用 WebPageTest 来识别和修复布局不稳定问题的演练。
    • Cumulative Layout Shift, The Layout Instability Metric,Boris Schapira 关于 CLS 的另一个非常详细的指南,它是如何计算的,如何测量以及如何优化它。
    • How To Improvement Core Web Vitals,Simon Hearne 关于每个指标(包括其他 Web Vitals,例如 FCP、TTI、TBT)的详细指南,以及它们何时发生以及如何衡量。

    那么,Core Web Vitals 是要遵循的最终指标吗? 不完全的。 它们确实已经暴露在大多数 RUM 解决方案和平台中,包括 Cloudflare、Treo、SpeedCurve、Calibre、WebPageTest(已经在幻灯片视图中)、Newrelic、Shopify、Next.js、所有 Google 工具(PageSpeed Insights、Lighthouse + CI、Search控制台等)和许多其他。

    然而,正如 Katie Sylor-Miller 解释的那样,Core Web Vitals 的一些主要问题是缺乏跨浏览器支持,我们并没有真正衡量用户体验的整个生命周期,而且很难关联 FID 和具有业务成果的 CLS。

    由于我们应该期待 Core Web Vitals 不断发展,因此始终Web Vitals 与您定制的指标结合起来似乎是合理的,以更好地了解您在性能方面的立场。

  2. 在代表您的受众的设备上收集数据。
    为了收集准确的数据,我们需要彻底选择要测试的设备。 在大多数公司中,这意味着研究分析并根据最常见的设备类型创建用户配置文件。 然而,通常仅靠分析并不能提供完整的画面。 很大一部分目标受众可能会因为体验太慢而放弃该网站(并且不再返回),因此他们的设备不太可能成为分析中最受欢迎的设备。 因此,另外对目标群体中的常用设备进行研究可能是一个好主意。

    根据 IDC 的数据,在 2020 年全球范围内,所有出货的手机中有 84.8% 是 Android 设备。 普通消费者每 2 年升级一次手机,在美国,手机更换周期为 33 个月。 全球最畅销的手机平均售价不到 200 美元。

    那么,一个具有代表性的设备是至少 24 个月大的 Android 设备,成本为 200 美元或更少,运行速度较慢,3G,400ms RTT 和 400kbps 传输,只是稍微悲观一些。 当然,这对于您的公司来说可能会非常不同,但这已经足够接近大多数客户了。 事实上,为您的目标市场调查当前的亚马逊畅销书可能是一个好主意。 (感谢 Tim Kadlec、Henri Helvetica 和 Alex Russell 的指点! )。

    在构建新网站或应用程序时,请始终首先查看目标市场的当前亚马逊畅销书
    在构建新网站或应用程序时,请始终首先检查目标市场的当前亚马逊畅销书。 (大预览)

    那么选择什么测试设备呢? 与上面概述的配置文件非常吻合的那些。 选择稍旧的 Moto G4/G5 Plus、中端三星设备(Galaxy A50、S8)、中端设备(如 Nexus 5X、小米 Mi A3 或小米红米 Note)是一个不错的选择7 和慢速设备,如 Alcatel 1X 或 Cubot X19,可能在开放设备实验室中。 要在速度较慢的热节流设备上进行测试,您还可以购买 Nexus 4,价格仅为 100 美元左右。

    此外,请检查每台设备中使用的芯片组,不要过度代表一个芯片组:几代 Snapdragon 和 Apple 以及低端的瑞芯微、联发科就足够了(谢谢,帕特里克!)

    如果您手头没有设备,可以通过在受限制的 3G 网络(例如 300 毫秒 RTT、1.6 Mbps 下行、0.8 Mbps 上行)和 CPU 限制(5 倍减速)上进行测试来模拟台式机上的移动体验。 最终切换到常规 3G、慢速 4G(例如 170 毫秒 RTT、9 Mbps 下行、9Mbps 上行)和 Wi-Fi。 为了使性能影响更加明显,您甚至可以引入 2G 星期二或在办公室设置节流 3G/4G 网络以加快测试速度。

    请记住,在移动设备上,与台式机相比,我们应该期待 4 倍至 5 倍的减速。 移动设备具有不同的 GPU、CPU、内存和不同的电池特性。 这就是为什么拥有一个普通设备的良好配置文件并始终在这样的设备上进行测试很重要的原因。

  3. 介绍一周中最慢的一天
    介绍一周中最慢的一天。 Facebook 推出了 2G 星期二,以提高慢速连接的可见性和敏感性。 (图片来源)

    幸运的是,有许多很棒的选项可以帮助您自动收集数据并根据这些指标衡量您的网站在一段时间内的表现。 请记住,良好的性能图片涵盖一组性能指标、实验室数据和现场数据:

    • 综合测试工具通过预定义的设备和网络设置(例如LighthouseCalibreWebPageTest )和
    • 真实用户监控( RUM ) 工具持续评估用户交互并收集现场数据(例如SpeedCurveNew Relic——这些工具也提供综合测试)。

    前者在开发过程中特别有用,因为它可以帮助您在开发产品时识别、隔离和修复性能问题。 后者对于长期维护很有用,因为它将帮助您了解实时发生的性能瓶颈——当用户实际访问该站点时。

    通过利用内置的 RUM API,例如导航计时、资源计时、绘制计时、长任务等,综合测试工具和 RUM 一起提供了应用程序性能的完整图景。 您可以使用 Calibre、Treo、SpeedCurve、mPulse 和 Boomerang、Sitespeed.io,它们都是性能监控的绝佳选择。 此外,使用 Server Timing 标头,您甚至可以在一个地方监控后端和前端性能。

    注意:选择浏览器外部的网络级节流器总是更安全的选择,例如,DevTools 与 HTTP/2 推送交互时存在问题,这是由于它的实现方式(感谢 Yoav,Patrick !)。 对于 Mac OS,我们可以使用 Network Link Conditioner、Windows Windows Traffic Shaper、Linux netem 和 FreeBSD dummynet。

    由于您可能会在 Lighthouse 中进行测试,请记住您可以:

    • 使用 Lighthouse CI 随时间跟踪 Lighthouse 分数(这非常令人印象深刻),
    • 在 GitHub Actions 中运行 Lighthouse 以在每个 PR 旁边获得一份 Lighthouse 报告,
    • 在站点的每个页面上运行 Lighthouse 性能审计(通过 Lightouse Parade),输出保存为 CSV,
    • 如果您需要深入了解更多细节,请使用 Lighthouse 分数计算器和 Lighthouse 指标权重。
    • Lighthouse 也可用于 Firefox,但在底层它使用 PageSpeed Insights API 并基于无头 Chrome 79 用户代理生成报告。
Lighthouse CI 非常了不起:一套工具可以持续运行、保存、检索和断言 Lighthouse 结果
Lighthouse CI 非常了不起:一套工具可以持续运行、保存、检索和断言 Lighthouse 结果。 (大预览)
  1. 设置“干净”和“客户”配置文件进行测试。
    在被动监控工具中运行测试时,关闭防病毒和后台 CPU 任务、删除后台带宽传输并使用没有浏览器扩展的干净用户配置文件进行测试以避免结果偏差(在 Firefox 和 Chrome 中)是一种常见策略。
    DebugBear 的报告重点介绍了 20 个最慢的扩展,包括密码管理器、广告拦截器以及 Evernote 和 Grammarly 等流行应用程序
    DebugBear 的报告重点介绍了 20 个最慢的扩展,包括密码管理器、广告拦截器以及 Evernote 和 Grammarly 等流行应用程序。 (大预览)

    但是,研究您的客户经常使用哪些浏览器扩展,并使用专门的“客户”配置文件进行测试也是一个好主意。 事实上,某些扩展程序可能会对您的应用程序产生深远的性能影响(2020 Chrome 扩展程序性能报告),如果您的用户经常使用它们,您可能需要预先考虑。 因此,仅“干净”的配置文件结果就过于乐观,在现实生活中可能会被粉碎。

  2. 与您的同事分享绩效目标。
    确保团队中的每个成员都熟悉绩效目标,以避免产生误解。 每一个决定——无论是设计、营销还是介于两者之间的任何决定——都会对绩效产生影响,在整个团队中分配责任和所有权将简化以后以绩效为中心的决策。 根据性能预算和早期定义的优先级映射设计决策。

设定切合实际的目标

  1. 100 毫秒响应时间,60 fps。
    为了让交互感觉流畅,界面有 100 毫秒来响应用户的输入。 再长一点,用户就会认为应用程序滞后。 以用户为中心的性能模型 RAIL 为您提供了健康的目标:为了允许 <100 毫秒的响应,页面必须最迟在每 <50 毫秒后将控制权交还给主线程。 估计输入延迟告诉我们是否达到了该阈值,理想情况下,它应该低于 50 毫秒。 对于动画这样的高压点,能做的最好什么都不做,不能做的绝对最少。

    RAIL,以用户为中心的性能模型。
    RAIL,以用户为中心的性能模型。

    此外,每一帧动画应该在 16 毫秒内完成,从而达到每秒 60 帧(1 秒 ÷ 60 = 16.6 毫秒)——最好在 10 毫秒以下。 因为浏览器需要时间将新帧绘制到屏幕上,所以您的代码应该在达到 16.6 毫秒标记之前完成执行。 我们开始讨论 120fps(例如 iPad Pro 的屏幕以 120Hz 运行),Surma 已经介绍了一些 120fps 的渲染性能解决方案,但这可能不是我们目前正在关注目标。

    对性能预期持悲观态度,但对界面设计持乐观态度并明智地使用空闲时间(检查 idlize、idle-until-urgent 和 react-idle)。 显然,这些目标适用于运行时性能,而不是加载性能。

  2. FID < 100ms, LCP < 2.5s, TTI < 5s on 3G, 关键文件大小预算 < 170KB (gzipped)。
    虽然这可能很难实现,但一个好的最终目标是 Time to Interactive 低于 5 秒,而对于重复访问,目标是低于 2 秒(只有通过服务人员才能实现)。 以 2.5 秒以下的最大内容绘制为目标,并最大限度地减少总阻塞时间累积布局偏移。 可接受的首次输入延迟低于 100 毫秒至 70 毫秒。 如上所述,我们考虑的基准是 200 美元的 Android 手机(例如 Moto G4),在慢速 3G 网络上,模拟 400ms RTT 和 400kbps 传输速度。

    我们有两个主要的限制因素,它们有效地形成了在网络上快速交付内容的合理目标。 一方面,由于 TCP 慢启动,我们有网络传输限制。 HTML 的前 14KB——10 个 TCP 数据包,每个 1460 字节,大约 14.25 KB,尽管不是字面意思——是最关键的有效负载块,也是预算中唯一可以在第一次往返中交付的部分(由于移动唤醒时间,这是您在 400 毫秒 RTT 时 1 秒内获得的所有数据)。

    Ilya Grigorik 的高性能浏览器网络
    对于 TCP 连接,我们从一个小的拥塞窗口开始,并在每次往返时将其加倍。 在第一次往返中,我们可以容纳 14 KB。 来自:Ilya Grigorik 的高性能浏览器网络。 (大预览)

    注意:由于 TCP 通常在很大程度上未充分利用网络连接,因此 Google 开发了 TCP 瓶颈带宽和 RRT( BBR ),这是一种 TCP 延迟控制的 TCP 流量控制算法。专为现代网络设计,它可以响应实际的拥塞,而不是像 TCP 那样丢包,它明显更快,具有更高的吞吐量和更低的延迟——并且算法的工作方式不同。(谢谢,维克多,巴里!

    另一方面,由于 JavaScript 解析和执行时间,我们对内存和 CPU 有硬件限制(我们稍后会详细讨论)。 为了实现第一段中所述的目标,我们必须考虑 JavaScript 的关键文件大小预算。 对于预算应该是多少(这在很大程度上取决于您的项目的性质),意见各不相同,但 170KB JavaScript gzip 的预算已经在中端手机上解析和编译需要 1 秒。 假设 170KB 在解压后扩展为该大小的 3 倍(0.7MB),这可能是 Moto G4/G5 Plus 上“体面”用户体验的丧钟。

    以 Wikipedia 网站为例,2020 年,在全球范围内,Wikipedia 用户的代码执行速度提高了 19%。 因此,如果您的年度网络性能指标保持稳定,这通常是一个警告信号,因为随着环境的不断改善,您实际上正在倒退(Gilles Dubuc 的博客文章中有详细信息)。

    如果您想瞄准东南亚、非洲或印度等成长型市场,您将不得不考虑一组截然不同的限制条件。 Addy Osmani 涵盖了主要的功能手机限制,例如低成本、高质量的设备、高质量网络的不可用和昂贵的移动数据——以及这些环境的PRPL-30 预算和开发指南。

    根据 Addy Osmani 的说法,延迟加载路由的推荐大小也小于 35 KB
    根据 Addy Osmani 的说法,延迟加载路由的推荐大小也小于 35 KB。 (大预览)
    如果针对功能手机,Addy Osmani 建议 PRPL-30 性能预算(压缩后 30KB + 缩小的初始捆绑包)
    如果针对功能手机,Addy Osmani 建议使用 PRPL-30 性能预算(压缩后 30KB + 缩小的初始捆绑包)。 (大预览)

    事实上,Google 的 Alex Russell 建议将 gzip 压缩后的 130-170KB 作为合理的上限。 在现实世界的场景中,大多数产品甚至都没有接近:今天的中位数捆绑大小约为 452KB,与 2015 年初相比增长了 53.6%。在中产阶级移动设备上,这占 12-20 秒的时间-To-Interactive

    2019 年全球最畅销智能手机的 Geekbench CPU 性能基准测试。JavaScript 强调单核性能并且受 CPU 限制
    2019 年全球最畅销智能手机的 Geekbench CPU 性能基准测试。JavaScript 强调单核性能(请记住,它本质上比 Web 平台的其他部分更单线程)并且受 CPU 限制。 来自 Addy 的文章“在 20 美元的功能手机上快速加载网页”。 (大预览)

    不过,我们也可以超出捆绑包大小的预算。 例如,我们可以根据浏览器主线程的活动设置性能预算,即开始渲染之前的绘制时间,或跟踪前端 CPU 占用率。 Calibre、SpeedCurve 和 Bundlesize 等工具可以帮助您控制预算,并且可以集成到您的构建过程中。

    最后,性能预算可能不应该是一个固定值。 根据网络连接,性能预算应该适应,但较慢连接的有效负载更加“昂贵”,无论它们如何使用。

    注意:在广泛传播的 HTTP/2、即将到来的 5G 和 HTTP/3、快速发展的手机和蓬勃发展的 SPA 时代,设置如此严格的预算可能听起来很奇怪。 然而,当我们处理网络和硬件的不可预测性时,它们听起来确实是合理的,包括从拥挤的网络到缓慢发展的基础设施,再到数据上限、代理浏览器、保存数据模式和偷偷摸摸的漫游费用。

来自 Addy Osmani 的“默认快速:现代加载最佳实践”
默认情况下从快速开始:Addy Osmani 的现代加载最佳实践(幻灯片 19)
性能预算应根据普通移动设备的网络条件进行调整
性能预算应根据普通移动设备的网络条件进行调整。 (图片来源:Katie Hempenius)(大预览)

定义环境

  1. 选择并设置您的构建工具。
    不要过分关注这些天所谓的酷。 坚持您的构建环境,无论是 Grunt、Gulp、Webpack、Parcel 还是工具组合。 只要你得到你需要的结果并且你在维护你的构建过程没有问题,你就做得很好。

    在构建工具中,Rollup 和 Snowpack 一直受到关注,但 Webpack 似乎是最成熟的工具,有数百个插件可用于优化构建的大小。 注意 Webpack 路线图 2021。

    最近出现的最著名的策略之一是在 Next.js 和 Gatsby 中使用 Webpack 进行粒度分块,以最大限度地减少重复代码。 默认情况下,可以为不使用它的路由请求未在每个入口点共享的模块。 这最终成为一种开销,因为下载的代码比必要的多。 通过 Next.js 中的粒度分块,我们可以使用服务器端构建清单文件来确定哪些输出的块被不同的入口点使用。

    为了减少 Webpack 项目中的重复代码,我们可以使用粒度分块,在 Next.js 和 Gatsby 中默认启用
    为了减少 Webpack 项目中的重复代码,我们可以使用粒度分块,在 Next.js 和 Gatsby 中默认启用。 图片来源:艾迪·奥斯马尼。 (大预览)

    使用 SplitChunksPlugin,根据多个条件创建多个拆分块,以防止跨多个路由获取重复代码。 这改善了导航期间的页面加载时间和缓存。 在 Next.js 9.2 和 Gatsby v2.20.7 中发布。

    不过,开始使用 Webpack 可能会很困难。 所以如果你想深入研究 Webpack,这里有一些很棒的资源:

    • Webpack 文档——显然——是一个很好的起点,Webpack - Raja Rao 的 The Confusing Bits 和 Andrew Welch 的 An Annotated Webpack Config 也是如此。
    • Sean Larkin 有一个关于 Webpack 的免费课程:核心概念和 Jeffrey Way 为每个人发布了一个很棒的关于 Webpack 的免费课程。 它们都是深入研究 Webpack 的绝佳介绍。
    • Webpack Fundamentals 是一个非常全面的 4 小时课程,由 FrontendMasters 发布,由 Sean Larkin 教授。
    • Webpack 示例有数百个现成的 Webpack 配置,按主题和用途分类。 奖励:还有一个生成基本配置文件的 Webpack 配置配置器。
    • awesome-webpack 是一个有用的 Webpack 资源、库和工具的精选列表,包括 Angular、React 和框架无关项目的文章、视频、课程、书籍和示例。
    • 使用 Webpack 进行快速生产资产构建的旅程是 Etsy 的案例研究,该案例研究团队如何从使用基于 RequireJS 的 JavaScript 构建系统切换到使用 Webpack,以及他们如何优化他们的构建,平均在4 分钟内管理超过 13,200 个资产。
    • Webpack 性能技巧是 Ivan Akulov 的一个金矿主题,其中包含许多以性能为中心的技巧,包括专门针对 Webpack 的技巧。
    • awesome-webpack-perf 是一个金矿 GitHub 存储库,其中包含有用的 Webpack 工具和插件以提高性能。 也由 Ivan Akulov 维护。
Etsy 使用 Webpack 进行快速生产构建之旅的可视化
Etsy 的 Webpack 快速生产构建之旅(来自 Addy Osmani)(大预览)
  1. 使用渐进增强作为默认值。
    尽管如此,经过这么多年,将渐进增强作为前端架构和部署的指导原则是一个安全的选择。 首先设计和构建核心体验,然后使用功能强大的浏览器的高级功能增强体验,创造弹性体验。 如果您的网站在次优网络上运行速度较慢且浏览器较差且屏幕较差的机器上运行得很快,那么它只会在速度较快且浏览器良好且网络良好的机器上运行得更快。

    事实上,通过自适应模块服务,我们似乎正在将渐进增强提升到另一个层次,为低端设备提供“精简”核心体验,并为高端设备提供更复杂的功能。 渐进式增强不太可能很快消失。

  2. 选择强大的性能基准。
    有很多未知因素影响加载——网络、热节流、缓存驱逐、第三方脚本、解析器阻塞模式、磁盘 I/O、IPC 延迟、安装的扩展、防病毒软件和防火墙、后台 CPU 任务、硬件和内存限制, L2/L3 缓存、RTTS 的差异——JavaScript 的体验成本最高,仅次于默认阻止渲染的 Web 字体和通常消耗过多内存的图像。 随着性能瓶颈从服务器转移到客户端,作为开发人员,我们必须更详细地考虑所有这些未知因素。

    170KB 的预算已经包含关键路径 HTML/CSS/JavaScript、路由器、状态管理、实用程序、框架和应用程序逻辑,我们必须彻底检查网络传输成本、解析/编译时间和运行时成本我们选择的框架。 幸运的是,在过去几年中,我们已经看到浏览器解析和编译脚本的速度有了巨大的改进。 然而,JavaScript 的执行仍然是主要瓶颈,因此密切关注脚本执行时间和网络可能会产生影响。

    Tim Kadlec 对现代框架的性能进行了出色的研究,并在“JavaScript 框架有成本”一文中对其进行了总结。 我们经常谈论独立框架的影响,但正如 Tim 所说,在实践中,使用多个框架并不少见。 也许是旧版本的 jQuery 正在慢慢迁移到现代框架,以及一些使用旧版本 Angular 的遗留应用程序。 因此,探索 JavaScript 字节和 CPU 执行时间的累积成本更合理,这很容易使用户体验几乎无法使用,即使在高端设备上也是如此。

    一般来说,现代框架不会优先考虑功能较弱的设备,因此手机和台式机上的体验在性能方面通常会大不相同。 根据研究,使用 React 或 Angular 的网站在 CPU 上花费的时间比其他网站多(当然这并不一定说 React 在 CPU 上比 Vue.js 更昂贵)。

    根据 Tim 的说法,有一件事是显而易见的:“如果您使用框架来构建您的网站,那么您就需要在初始性能方面进行权衡——即使在最好的情况下也是如此。”

框架成本、JavaScript CPU 时间:SPA 网站表现不佳
框架的成本,JavaScript 再见:SPA 网站(仍然)表现不佳
移动设备的脚本相关 CPU 时间和 desktopv 设备的 JavaScript 字节。 一般来说,使用 React 或 Angular 的网站在 CPU 上花费的时间比其他网站多。 但这取决于您如何构建网站。 蒂姆·卡德莱克的研究。 (大预览)
  1. 评估框架和依赖项。
    现在,并不是每个项目都需要一个框架,也不是单页应用程序的每个页面都需要加载一个框架。 在 Netflix 的案例中,“从客户端移除 React、多个库和相应的应用程序代码将 JavaScript 的总量减少了超过 200KB,导致 Netflix 的注销主页的 Time-to-Interactivity 减少了 50% 以上。” 然后,团队利用用户在登陆页面上花费的时间来为用户可能登陆的后续页面预取 React(请继续阅读以了解详细信息)。

    那么,如果您完全删除关键页面上的现有框架怎么办? 使用 Gatsby,您可以检查 gatsby-plugin-no-javascript,它会从静态 HTML 文件中删除 Gatsby 创建的所有 JavaScript 文件。 在 Vercel 上,您还可以允许在生产中为某些页面禁用运行时 JavaScript(实验性)。

    一旦选择了一个框架,我们将至少使用它几年,所以如果我们需要使用一个框架,我们需要确保我们的选择是明智的和经过深思熟虑的——尤其是对于我们的关键性能指标关心。

    数据显示,默认情况下,框架非常昂贵:58.6% 的 React 页面交付超过 1 MB 的 JavaScript,36% 的 Vue.js 页面加载的 First Contentful Paint 小于 1.5 秒。 根据 Ankur Sethi 的一项研究,“无论你如何优化它,你的 React 应用程序在印度普通手机上的加载速度永远不会超过 1.1 秒。你的 Angular 应用程序总是至少需要 2.7 秒才能启动。您的 Vue 应用程序的用户需要等待至少 1 秒钟才能开始使用它。” 无论如何,您可能不会将印度作为您的主要市场,但在网络条件欠佳的情况下访问您的网站的用户将获得类似的体验。

    当然,可以快速制作 SPA,但它们并不是开箱即用的快速,因此我们需要考虑制作保持快速所需的时间和精力。 尽早选择轻量级的基准性能成本可能会更容易。

    那么我们如何选择框架呢? 在选择选项之前,至少考虑大小的总成本 + 初始执行时间是个好主意; Preact、Inferno、Vue、Svelte、Alpine 或 Polymer 等轻量级选项可以很好地完成工作。 基线的大小将定义应用程序代码的约束。

    正如 Seb Markbage 所指出的,衡量框架启动成本的一个好方法是首先渲染一个视图,然后删除它,然后再次渲染,因为它可以告诉您框架如何扩展。 第一次渲染往往会预热一堆延迟编译的代码,更大的树在扩展时可以从中受益。 第二个渲染基本上是模拟页面上的代码重用如何随着页面复杂性的增长而影响性能特征。

    您可以通过探索特性、可访问性、稳定性、性能、包生态系统、社区、学习曲线、文档、工具、跟踪记录,在 Sacha Greif 的 12 分制评分系统上评估您的候选人(或一般的任何 JavaScript 库) 、团队、兼容性、安全性等。

    Perf Track 大规模跟踪框架性能
    Perf Track 大规模跟踪框架性能。 (大预览)

    您还可以依赖在较长时间内在网络上收集的数据。 例如,Perf Track 大规模跟踪框架性能,显示使用 Angular、React、Vue、Polymer、Preact、Ember、Svelte 和 AMP 构建的网站的原始聚合Core Web Vitals 分数。 您甚至可以指定和比较使用 Gatsby、Next.js 或 Create React App 构建的网站,以及使用 Nuxt.js (Vue) 或 Sapper (Svelte) 构建的网站。

    一个好的起点是为您的应用程序选择一个好的默认堆栈。 Gatsby (React)、Next.js (React)、Vuepress (Vue)、Preact CLI 和 PWA Starter Kit 提供了合理的默认值,可以在普通移动硬件上开箱即用地快速加载。 另外,请查看针对 React 和 Angular 的 web.dev 框架特定的性能指南(感谢 Phillip! )。

    或许您可以采用一种更令人耳目一新的方法来完全构建单页应用程序——Turbolinks,一个 15KB 的 JavaScript 库,它使用 HTML 而不是 JSON 来呈现视图。 因此,当您点击链接时,Turbolinks 会自动获取页面,交换其<body>并合并其<head> ,所有这些都不会产生整个页面加载的成本。 您可以查看有关堆栈(Hotwire)的快速详细信息和完整文档。

显示畅销手机计算性能的直方图
畅销手机的 CPU 和计算性能(图片来源:Addy Osmani)(大预览)
  1. 客户端渲染还是服务器端渲染? 两个都!
    这是一个相当激烈的谈话。 最终的方法是设置某种渐进式引导:使用服务器端渲染来获得快速的 First Contentful Paint,但还包括一些最少的必要 JavaScript 以保持与 First Contentful Paint 接近的交互时间。 如果在 FCP 之后 JavaScript 来得太晚,浏览器会在解析、编译和执行后期发现的 JavaScript 时锁定主线程,从而束缚站点或应用程序的交互性。

    为避免这种情况,请始终将函数的执行分解为单独的异步任务,并在可能的情况下使用requestIdleCallback 。 考虑使用 WebPack 的动态import()支持延迟加载 UI 的部分,避免加载、解析和编译成本,直到用户真正需要它们(感谢 Addy! )。

    如上所述,交互时间 (TTI) 告诉我们导航和交互之间的时间。 具体来说,该指标是通过查看初始内容呈现后的第一个 5 秒窗口来定义的,其中没有任何 JavaScript 任务花费超过 50 毫秒长任务)。 如果发生超过 50 毫秒的任务,则重新开始搜索 5 秒窗口。 结果,浏览器将首先假定它到达Interactive ,只是为了切换到Frozen ,最终切换回Interactive

    一旦我们到达Interactive ,我们就可以 - 按需或在时间允许的情况下 - 启动应用程序的非必要部分。 不幸的是,正如 Paul Lewis 所注意到的,框架通常没有可以向开发人员展示的简单优先级概念,因此对于大多数库和框架来说,渐进式引导并不容易实现。

    不过,我们正在到达那里。 这些天来,我们可以探索几个选择,Houssein Djirdeh 和 Jason Miller 在他们关于 Rendering on the Web 的演讲以及 Jason 和 Addy 关于现代前端架构的文章中对这些选项进行了很好的概述。 下面的概述基于他们的出色工作。

    • 完整的服务器端渲染(SSR)
      在 WordPress 等经典 SSR 中,所有请求都完全在服务器上处理。 请求的内容作为完成的 HTML 页面返回,浏览器可以立即呈现它。 因此,例如,SSR 应用程序不能真正使用 DOM API。 First Contentful Paint 和 Time to Interactive 之间的差距通常很小,并且页面可以在 HTML 流式传输到浏览器时立即呈现。

      这避免了在客户端获取数据和模板的额外往返,因为它是在浏览器得到响应之前处理的。 但是,我们最终会得到更长的服务器思考时间,从而导致第一个字节的时间,并且我们没有利用现代应用程序的响应式和丰富的特性。

    • 静态渲染
      我们将产品构建为单页应用程序,但所有页面都使用最少的 JavaScript 作为构建步骤预先呈现为静态 HTML。 这意味着通过静态渲染,我们可以提前为每个可能的 URL生成单独的 HTML 文件,这是很多应用程序无法承受的。 但是由于不必动态生成页面的 HTML,我们可以实现始终如一的快速首字节时间。 因此,我们可以快速显示一个登录页面,然后为后续页面预取一个 SPA 框架。 Netflix 采用了这种方法,将加载和交互时间减少了 50%。

    • 使用(重新)水合的服务器端渲染(通用渲染,SSR + CSR)
      我们可以尝试使用两全其美的方法——SSR 和 CSR 方法。 通过混合水化,从服务器返回的 HTML 页面还包含一个脚本,用于加载成熟的客户端应用程序。 理想情况下,实现快速的 First Contentful Paint(如 SSR),然后通过(重新)水化继续渲染。 不幸的是,这种情况很少见。 更常见的情况是,页面看起来确实准备好了,但它无法响应用户的输入,从而产生愤怒的点击和放弃。

      使用 React,我们可以在 Express 等 Node 服务器上使用ReactDOMServer模块,然后调用renderToString方法将顶级组件呈现为静态 HTML 字符串。

      使用 Vue.js,我们可以使用 vue-server-renderer 使用renderToString将 Vue 实例渲染为 HTML。 在 Angular 中,我们可以使用@nguniversal将客户端请求转换为完全由服务器渲染的 HTML 页面。 使用 Next.js (React) 或 Nuxt.js (Vue) 也可以开箱即用地实现完全服务器渲染的体验。

      这种方法有其缺点。 因此,我们确实获得了客户端应用程序的完全灵活性,同时提供了更快的服务器端渲染,但我们最终也会在 First Contentful Paint 和 Time To Interactive 之间存在更长的差距,并且增加了 First Input Delay。 补液非常昂贵,通常仅此策略还不够好,因为它会严重延迟 Time To Interactive。

    • 使用渐进式水化 (SSR + CSR) 的流式服务器端渲染
      为了最小化 Time To Interactive 和 First Contentful Paint 之间的差距,我们一次渲染多个请求,并在生成内容时分块发送内容。 因此,我们不必在将内容发送到浏览器之前等待完整的 HTML 字符串,从而改进了 Time To First Byte。

      在 React 中,我们可以使用 renderToNodeStream() 来代替renderToString() () 来管道响应并以块的形式发送 HTML。 在 Vue 中,我们可以使用可以管道和流式传输的 renderToStream()。 使用 React Suspense,我们也可以为此目的使用异步渲染。

      在客户端,我们不是一次启动整个应用程序,而是逐步启动组件。 应用程序的部分首先通过代码拆分分解为独立的脚本,然后逐渐水合(按照我们的优先级顺序)。 事实上,我们可以先对关键成分进行水合,然后再对其余成分进行水合。 然后,每个组件可以不同地定义客户端和服务器端渲染的角色。 然后,我们还可以推迟某些组件的水合,直到它们出现,或者用户交互需要,或者浏览器空闲时。

      对于 Vue,Markus Oberlehner 发布了一份关于使用用户交互水合以及 vue-lazy-hydration 减少 SSR 应用程序的交互时间的指南,这是一个早期插件,可在可见性或特定用户交互上启用组件水合。 Angular 团队使用 Ivy Universal 进行渐进式补水。 您也可以使用 Preact 和 Next.js 实现部分水合。

    • 三态渲染
      有了服务工作者,我们可以使用流式服务器渲染来进行初始/非 JS 导航,然后让服务工作者在安装后为导航渲染 HTML。 在这种情况下,Service Worker 会预先呈现内容并启用 SPA 风格的导航,以便在同一会话中呈现新视图。 当您可以在服务器、客户端页面和服务工作者之间共享相同的模板和路由代码时,效果很好。

    一个插图展示了三态渲染如何在 DOM 渲染、Service Worker 预渲染和服务器端渲染等 3 个地方工作
    三态渲染,在任何 3 个地方都具有相同的代码渲染:在服务器上、在 DOM 中或在 service worker 中。 (图片来源:Google Developers)(大图预览)
    • 带有预渲染的 CSR
      预渲染类似于服务器端渲染,但不是在服务器上动态渲染页面,而是在构建时将应用程序渲染为静态 HTML。 虽然静态页面无需太多客户端 JavaScript 即可完全交互,但预渲染的工作方式有所不同。 基本上,它在构建时将客户端应用程序的初始状态捕获为静态 HTML,而通过预渲染,必须在客户端上启动应用程序才能使页面具有交互性。

      使用 Next.js,我们可以通过将应用程序预渲染为静态 HTML 来使用静态 HTML 导出。 在 Gatsby 中,一个使用 React 的开源静态站点生成器,在构建期间使用renderToStaticMarkup方法而不是renderToString方法,预加载主 JS 块并预取未来路由,没有简单静态页面不需要的 DOM 属性。

      对于 Vue,我们可以使用 Vuepress 来达到同样的目的。 你也可以在 Webpack 中使用 prerender-loader。 Navi 也提供静态渲染。

      结果是更好的 Time To First Byte 和 First Contentful Paint,我们减少了 Time To Interactive 和 First Contentful Paint 之间的差距。 如果预计内容会发生很大变化,我们就不能使用这种方法。 此外,必须提前知道所有 URL 才能生成所有页面。 所以一些组件可能会使用预渲染来渲染,但如果我们需要动态的东西,我们必须依赖应用程序来获取内容。

    • 完整的客户端渲染(CSR)
      所有逻辑、渲染和启动都在客户端完成。 结果通常是 Time To Interactive 和 First Contentful Paint 之间的巨大差距。 结果,应用程序经常感觉迟缓,因为整个应用程序必须在客户端上启动才能呈现任何内容。

      由于 JavaScript 有性能成本,随着 JavaScript 的数量随着应用程序的增长而增长,激进的代码拆分和延迟 JavaScript 将绝对有必要驯服 JavaScript 的影响。 对于这种情况,如果不需要太多交互性,服务器端渲染通常是更好的方法。 如果这不是一个选项,请考虑使用 App Shell 模型。

      一般来说,SSR 比 CSR 快。 然而,对于许多应用程序来说,它仍然是一个相当频繁的实现。

    那么,客户端还是服务器端? 一般来说,将完全客户端框架的使用限制在绝对需要它们的页面上是一个好主意。 对于高级应用程序,单独依赖服务器端渲染也不是一个好主意。 如果做得不好,服务器渲染和客户端渲染都是一场灾难。

    无论您是倾向于 CSR 还是 SSR,请确保您尽快渲染重要的像素,并将该渲染与 Time To Interactive 之间的差距最小化。 如果您的页面没有太大变化,请考虑预渲染,并尽可能推迟框架的启动。 使用服务器端渲染将 HTML 分块流式传输,并为客户端渲染实现渐进式水合- 并在可见性、交互或空闲时间进行水合,以获得两全其美的效果。

比较客户端与服务器端渲染选项的表格
客户端与服务器端渲染的选项范围。 此外,请查看 Jason 和 Houssein 在 Google I/O 上关于应用程序架构的性能影响的演讲。 (图片来源:Jason Miller)(大预览)
AirBnB 网站的示例,左侧显示没有渐进式水合作用,右侧显示渐进式水合作用
AirBnB 一直在尝试渐进式补水; 他们推迟了不需要的组件、用户交互(滚动)或空闲时间的负载,测试表明它可以提高 TTI。 (大预览)
  1. 我们可以静态服务多少?
    无论您是在处理大型应用程序还是小型站点,都值得考虑哪些内容可以从 CDN(即 JAM 堆栈)静态提供,而不是动态生成。 即使您拥有数千种产品和数百个具有大量个性化选项的过滤器,您仍可能希望静态提供关键登录页面,并将这些页面与您选择的框架分离。

    有很多静态站点生成器,它们生成的页面通常非常快。 我们可以提前预构建的内容越多,而不是在请求时在服务器或客户端上生成页面视图,我们将获得更好的性能。

    在构建部分水合、渐进增强的静态网站中,Markus Oberlehner 展示了如何使用静态站点生成器和 SPA 构建网站,同时实现渐进增强和最小的 JavaScript 包大小。 Markus 使用Eleventy 和 Preact作为他的工具,并展示了如何设置工具、添加部分水合、延迟水合、客户端入口文件、为 Preact 配置 Babel 以及将 Preact 与 Rollup 捆绑在一起——从头到尾。

    如今,随着 JAMStack 在大型网站上的使用,出现了一个新的性能考虑:构建时间。 事实上,即使每次新部署构建数千个页面也可能需要几分钟,因此很有希望看到 Gatsby 中的增量构建将构建时间缩短60 倍,并集成到流行的 CMS 解决方案中,如 WordPress、Contentful、Drupal、Netlify CMS和别的。

    左上角用户 1 和左下角用户 2 显示增量状态重新生成过程的流程图
    使用 Next.js 进行增量静态再生。 (图片来源:Prisma.io)(大预览)

    此外,Next.js 宣布了提前和增量静态生成,它允许我们在运行时添加新的静态页面,并在现有页面已经构建后更新它们,通过在流量进入时在后台重新渲染它们.

    需要更轻量级的方法? 在他关于 Eleventy、Alpine 和 Tailwind 的演讲中:迈向轻量级 Jamstack,Nicola Goutay 解释了 CSR、SSR 和介于两者之间的所有内容之间的区别,并展示了如何使用更轻量级的方法 - 以及显示该方法的 GitHub 存储库在实践中。

  2. 考虑使用 PRPL 模式和 app shell 架构。
    不同的框架会对性能产生不同的影响,并且需要不同的优化策略,因此您必须清楚地了解您将依赖的框架的所有细节。 在构建 Web 应用程序时,请查看 PRPL 模式和应用程序外壳架构。 这个想法非常简单:推送获得交互所需的最少代码以使初始路由快速呈现,然后使用 service worker 缓存和预缓存资源,然后异步延迟加载您需要的路由。
应用程序外壳架构中的 PRPL 模式
PRPL 代表推送关键资源、渲染初始路由、预缓存剩余路由和按需延迟加载剩余路由。
应用程序外壳架构
应用程序外壳是支持用户界面的最小 HTML、CSS 和 JavaScript。
  1. 您是否优化了 API 的性能?
    API 是应用程序通过端点向内部和第三方应用程序公开数据的通信渠道。 在设计和构建 API 时,我们需要一个合理的协议来实现服务器和第三方请求之间的通信。 Representational State Transfer ( REST ) 是一种成熟的、合乎逻辑的选择:它定义了一组开发人员遵循的约束,以使内容以高性能、可靠和可扩展的方式可访问。 符合 REST 约束的 Web 服务称为RESTful Web 服务

    与良好的 ol' HTTP 请求一样,当从 API 检索数据时,服务器响应的任何延迟都会传播到最终用户,从而延迟渲染。 当资源想要从 API 中检索一些数据时,它需要从相应的端点请求数据。 渲染来自多个资源的数据的组件,例如在每个评论中包含评论和作者照片的文章,可能需要多次往返服务器以获取所有数据,然后才能渲染它。 此外,通过 REST 返回的数据量通常超过渲染该组件所需的数据量。

    如果许多资源需要来自 API 的数据,则 API 可能会成为性能瓶颈。 GraphQL 为这些问题提供了一个高性能的解决方案。 就其本身而言,GraphQL 是一种用于 API 的查询语言,也是一种服务器端运行时,用于使用您为数据定义的类型系统来执行查询。 与 REST 不同,GraphQL 可以在单个请求中检索所有数据,并且响应将完全符合要求,而不会像 REST 通常发生的那样过度不足地获取数据。

    此外,由于 GraphQL 使用模式(说明数据结构的元数据),它已经可以将数据组织成首选结构,因此,例如,使用 GraphQL,我们可以删除用于处理状态管理的 JavaScript 代码,生成更简洁的应用程序代码,在客户端上运行得更快。

    如果您想开始使用 GraphQL 或遇到性能问题,这些文章可能会很有帮助:

    • GraphQL 入门:为什么我们需要一种新的 API,作者 Eric Baer,
    • A GraphQL Primer: The Evolution of API Design by Eric Baer,
    • Leonardo Losoviz 设计了一个 GraphQL 服务器以获得最佳性能,
    • Wojciech Trocki 解释了 GraphQL 性能。
两个使用 Redux/REST(左)和 Apollo/GraphQL(右)的消息移动接口示例
REST 和 GraphQL 之间的区别,左边是 Redux + REST,右边是 Apollo + GraphQL。 (图片来源:Hacker Noon)(大图预览)
  1. 您会使用 AMP 还是 Instant Articles?
    根据您组织的优先事项和战略,您可能需要考虑使用 Google 的 AMP 或 Facebook 的 Instant Articles 或 Apple 的 Apple News。 没有它们你也能获得良好的性能,但 AMP确实提供了一个可靠的性能框架和一个免费的内容交付网络 (CDN),而 Instant Articles 将提高你在 Facebook 上的知名度和性能。

    这些技术对用户来说看似显而易见的好处是保证了性能,因此有时他们甚至可能更喜欢 AMP-/Apple News/Instant Pages-链接,而不是“常规”和可能臃肿的页面。 对于处理大量第三方内容的内容密集型网站,这些选项可能有助于显着加快渲染时间。

    除非他们没有。 例如,根据 Tim Kadlec 的说法,“AMP 文档往往比其对应文档更快,但它们并不一定意味着页面是高性能的。从性能角度来看,AMP 并不是最大的区别。”

    网站所有者的好处是显而易见的:这些格式在各自平台上的可发现性以及在搜索引擎中的可见性增加。

    好吧,至少以前是这样的。 由于 AMP 不再是Top Stories的要求,出版商可能会从 AMP 转向传统堆栈(谢谢,Barry! )。

    不过,您也可以通过重用 AMP 作为 PWA 的数据源来构建渐进式 Web AMP。 缺点? 显然,在围墙花园中的存在使开发人员能够制作和维护其内容的单独版本,并且在没有实际 URL 的 Instant Articles 和 Apple News 的情况下(感谢 Addy,Jeremy!)

  2. 明智地选择您的 CDN。
    如上所述,根据您拥有多少动态数据,您可能能够将部分内容“外包”给静态站点生成器,将其推送到 CDN 并从中提供静态版本,从而避免向服务器。 事实上,其中一些生成器实际上是网站编译器,提供了许多开箱即用的自动优化。 随着编译器随着时间的推移添加优化,编译的输出随着时间的推移变得越来越小。

    请注意,CDN 也可以提供(和卸载)动态内容。 因此,没有必要将您的 CDN 限制为静态资产。 仔细检查您的 CDN 是否执行压缩和转换(例如,图像优化和边缘调整大小),它们是否为服务器工作者、A/B 测试以及边缘端包含提供支持,这些包含组合页面的静态和动态部分在 CDN 的边缘(即离用户最近的服务器)和其他任务。 另外,检查您的 CDN 是否支持 HTTP over QUIC (HTTP/3)。

    Katie Hempenius 撰写了一份精彩的 CDN 指南,其中提供了有关如何选择好的 CDN 、如何对其进行微调以及评估 CDN 时要记住的所有小事的见解。 通常,最好尽可能积极地缓存内容并启用 CDN 性能功能,例如 Brotli、TLS 1.3、HTTP/2 和 HTTP/3。

    注意:根据 Patrick Meenan 和 Andy Davies 的研究,HTTP/2 优先级在许多 CDN 上被有效破坏,因此在选择 CDN 时要小心。 Patrick 在他关于 HTTP/2 Prioritization 的演讲中有更多细节(谢谢,Barry! )。

    CDNPerf 预览 CDN 名称和查询速度(毫秒)
    CDNPerf 通过每天收集和分析 3 亿个测试来衡量 CDN 的查询速度。 (大预览)

    选择 CDN 时,您可以使用这些比较网站并详细了解其功能:

    • CDN 比较,用于 Cloudfront、Aazure、KeyCDN、Fastly、Verizon、Stackpach、Akamai 等的 CDN 比较矩阵。
    • CDN Perf 通过每天收集和分析 3 亿个测试来衡量 CDN 的查询速度,所有结果均基于来自全球用户的 RUM 数据。 另请检查 DNS 性能比较和云性能比较。
    • CDN Planet Guides 概述了特定主题的 CDN,例如 Serve Stale、Purge、Origin Shield、Prefetch 和 Compression。
    • Web Almanac:CDN 采用和使用提供有关顶级 CDN 提供商、他们的 RTT 和 TLS 管理、TLS 协商时间、HTTP/2 采用等的见解。 (不幸的是,数据仅来自 2019 年)。

资产优化

  1. 使用 Brotli 进行纯文本压缩。
    2015 年,谷歌推出了 Brotli,这是一种新的开源无损数据格式,现在所有现代浏览器都支持这种格式。 为 Brotli 实现编码器和解码器的开源 Brotli 库具有 11 个预定义的编码器质量级别,更高的质量级别需要更多的 CPU 以换取更好的压缩比。 较慢的压缩最终会导致更高的压缩率,但 Brotli 仍然可以快速解压缩。 值得注意的是,压缩级别为 4 的 Brotli 比 Gzip 更小且压缩速度更快。

    在实践中,Brotli 似乎比 Gzip 更有效。 意见和经验各不相同,但如果您的网站已经使用 Gzip 进行了优化,您可能会期望在大小缩减和 FCP 时间方面至少有一位数的改进,最多也有两位数的改进。 您还可以估计您的站点的 Brotli 压缩节省。

    只有当用户通过 HTTPS 访问网站时,浏览器才会接受 Brotli。 Brotli 受到广泛支持,许多 CDN 都支持它(Akamai、Netlify Edge、AWS、KeyCDN、Fastly(目前仅作为直通)、Cloudflare、CDN77),即使在尚不支持的 CDN 上也可以启用 Brotli (与服务人员)。

    问题是,由于使用 Brotli 以高压缩级别压缩所有资产的成本很高,因此许多托管服务提供商无法全面使用它,因为它会产生巨大的成本开销。 事实上,在最高级别的压缩下,Brotli非常慢,以至于服务器在等待动态压缩资产时开始发送响应所需的时间可能会抵消文件大小的任何潜在收益。 (但如果您在构建期间有时间使用静态压缩,当然,更高的压缩设置是首选。)

    显示为须图的比较,显示了三种不同后端时间的各种压缩方法:最小、平均和第 90 个百分位数
    各种压缩方法的后端时间比较。 不出所料,Brotli 比 gzip 慢(目前)。 (大预览)

    不过,这可能正在改变。 Brotli 文件格式包括一个内置的静态字典,除了包含多种语言的各种字符串外,它还支持对这些单词应用多种转换的选项,增加了它的多功能性。 在他的研究中,Felix Hanau 发现了一种改进 5 到 9 级压缩的方法,方法是使用“比默认字典更专业的子集”并依靠Content-Type标头告诉压缩器是否应该使用HTML、JavaScript 或 CSS 的字典。 结果是“在使用有限的字典使用方法以高压缩级别压缩 Web 内容时,性能影响可以忽略不计(CPU 比正常情况下的 12% 多 1% 到 3%)。”

    一个条形图,显示在第 5 级使用 Brotli 精简字典的压缩增益
    使用改进的字典方法,我们可以在更高的压缩级别上更快地压缩资产,同时只使用 1% 到 3% 的 CPU。 通常,压缩级别 6 比 5 最多会使 CPU 使用率增加 12%。 (大预览)

    最重要的是,通过 Elena Kirilenko 的研究,我们可以使用以前的压缩工件实现快速高效的 Brotli 再压缩。 根据 Elena 的说法,“一旦我们通过 Brotli 压缩了资产,并且我们尝试动态压缩动态内容,其中的内容类似于我们提前可用的内容,我们就可以显着缩短压缩时间。 "

    这种情况多久发生一次? 例如,交付JavaScript 捆绑包子集(例如,当部分代码已经缓存在客户端或使用 WebBundles 提供动态捆绑包时)。 或者使用基于预先知道的模板的动态 HTML,或者动态子集的 WOFF2 字体。 根据 Elena 的说法,当删除 10% 的内容时,我们可以获得 5.3% 的压缩改进和 39% 的压缩速度改进,当删除 50% 的内容时,压缩率提高了 3.2%,压缩速度提高了 26%。

    Brotli 压缩越来越好,所以如果你能绕过动态压缩静态资产的成本,那绝对是值得的。 不用说,Brotli 可以用于任何纯文本负载——HTML、CSS、SVG、JavaScript、JSON 等等。

    注意:截至 2021 年初,大约 60% 的 HTTP 响应是在没有基于文本的压缩的情况下交付的,其中 30.82% 使用 Gzip 压缩,9.1% 使用 Brotli 压缩(无论是在移动设备上还是在桌面上)。 例如,23.4% 的 Angular 页面没有被压缩(通过 gzip 或 Brotli)。 然而,通常打开压缩是通过简单的开关翻转来提高性能的最简单的方法之一。

    策略? 在最高级别使用 Brotli+Gzip 预压缩静态资产,并在级别 4-6 使用 Brotli 动态压缩(动态)HTML。 确保服务器正确处理 Brotli 或 Gzip 的内容协商。

根据 Web Almanax 2020 报告显示 HTTP 请求的压缩算法的条形图
在 2020 年压缩的资源中,有 22.59% 是使用 Brotli 压缩的。 大约 77.39% 是用 gzip 压缩的。 (图片来源:Web Almanac:压缩)(大预览)
  1. 我们是否使用自适应媒体加载和客户端提示?
    它来自旧新闻的土地,但它始终是一个很好的提醒,使用带有srcsetsizes<picture>元素的响应式图像。 特别是对于媒体占用量大的网站,我们可以通过自适应媒体加载(在本例中为 React + Next.js)更进一步,为慢速网络和低内存设备提供轻量体验,为快速网络和高内存设备提供完整体验-内存设备。 在 React 的上下文中,我们可以通过服务器上的客户端提示和客户端上的 react-adaptive-hooks 来实现它。

    随着客户提示的广泛采用,响应式图像的未来可能会发生巨大变化。 客户端提示是 HTTP 请求标头字段,例如DPRViewport-WidthWidthSave-DataAccept (指定图像格式首选项)等。 他们应该通知服务器有关用户浏览器、屏幕、连接等的细节。

    因此,服务器可以决定如何用适当大小的图像填充布局,并仅以所需格式提供这些图像。 通过客户端提示,我们将资源选择从 HTML 标记移到客户端和服务器之间的请求-响应协商中。

    显示如何通过根据用户的网络能力向用户发送不同的分辨率来使用自适应媒体服务的插图
    正在使用的自适应媒体服务。 我们向离线用户发送带文本的占位符,向 2G 用户发送低分辨率图像,向 3G 用户发送高分辨率图像,向 4G 用户发送高清视频。 通过在 20 美元的功能手机上快速加载网页。 (大预览)

    正如 Ilya Grigorik 不久前指出的那样,客户提示完成了图片 - 它们不是响应式图像的替代品。 “ <picture>元素在 HTML 标记中提供了必要的艺术方向控制。客户端提示为生成的图像请求提供注释,从而实现资源选择自动化。Service Worker 在客户端提供完整的请求和响应管理功能。”

    例如,服务工作者可以将新的客户端提示标头值附加到请求中,重写 URL 并将图像请求指向 CDN,根据连接性和用户偏好调整响应等。它不仅适用于图像资产,而且适用于几乎所有其他请求也是如此。

    对于支持客户端提示的客户端,可以测量到图像节省 42% 的字节和 70th+ 百分位数的 1MB+ 更少的字节。 在 Smashing Magazine 上,我们也可以测量到 19-32% 的改进。 基于 Chromium 的浏览器支持客户端提示,但在 Firefox 中仍在考虑中。

    但是,如果您同时为客户端提示提供正常的响应式图像标记和<meta>标记,则支持的浏览器将评估响应式图像标记并使用客户端提示 HTTP 标头请求适当的图像源。

  2. 我们是否使用响应式图像作为背景图像?
    我们当然应该! 使用image-set ,现在 Safari 14 和除 Firefox 之外的大多数现代浏览器都支持,我们也可以提供响应式背景图像:

    background-image: url("fallback.jpg"); background-image: image-set( "photo-small.jpg" 1x, "photo-large.jpg" 2x, "photo-print.jpg" 600dpi);

    基本上,我们可以有条件地提供具有1x描述符的低分辨率背景图像,以及具有2x描述符的高分辨率图像,甚至是具有600dpi描述符的打印质量图像。 但请注意:浏览器不会为辅助技术提供有关背景图像的任何特殊信息,因此理想情况下,这些照片只是装饰。

  3. 我们使用 WebP 吗?
    图像压缩通常被认为是一种快速的胜利,但在实践中它仍然没有得到充分利用。 当然,图像不会阻塞渲染,但它们会严重影响 LCP 分数,而且通常它们对于正在使用它们的设备来说太重太大了。

    所以至少,我们可以探索为我们的图像使用 WebP 格式。 事实上,随着 Apple 在 Safari 14 中增加对 WebP 的支持,WebP 传奇已接近尾声。因此,经过多年的讨论和辩论,截至今天,所有现代浏览器都支持 WebP。 因此,如果需要(请参阅 Andreas Bovens 的代码片段)或使用内容协商(使用Accept标头),我们可以使用<picture>元素和 JPEG 后备来提供 WebP 图像。

    不过,WebP 并非没有缺点。 虽然 WebP 图像文件的大小与等效的 Guetzli 和 Zopfli 相比,但该格式不支持像 JPEG 这样的渐进式渲染,这就是为什么用户可以使用良好的 JPEG 更快地看到完成的图像,尽管 WebP 图像可能会通过网络变得更快。 使用 JPEG,我们可以用一半甚至四分之一的数据提供“体面”的用户体验,然后再加载其余的数据,而不是像 WebP 那样使用半空图像。

    您的决定将取决于您追求的目标:使用 WebP,您将减少有效负载,使用 JPEG,您将提高感知性能。 您可以在 Google 的 Pascal Massimino 的 WebP Rewind 演讲中了解有关 WebP 的更多信息。

    要转换为 WebP,您可以使用 WebP Converter、cwebp 或 libwebp。 Ire Aderinokun 也有一个非常详细的关于将图像转换为 WebP 的教程——Josh Comeau 在他关于拥抱现代图像格式的文章中也是如此。

    用于 Pascal Massimino 演讲的幻灯片,标题为 Image ready: webp rewind
    关于 WebP 的彻底讨论:Pascal Massimino 的 WebP Rewind。 (大预览)

    Sketch 原生支持 WebP,并且可以使用 Photoshop 的 WebP 插件从 Photoshop 中导出 WebP 图像。 但也有其他选择。

    如果您使用的是 WordPress 或 Joomla,有一些扩展可以帮助您轻松实现对 WebP 的支持,例如 Optimus 和 WordPress 的 Cache Enabler 以及 Joomla 自己支持的扩展(通过 Cody Arsenault)。 您还可以使用 React、样式化组件或 gatsby-image 抽象出<picture>元素。

    啊——不要脸的插头! — Jeremy Wagner 甚至出版了一本关于 WebP 的 Smashing 书,您可能想看看您是否对 WebP 周围的一切感兴趣。

  4. 我们使用 AVIF 吗?
    您可能已经听说了一个重大消息:AVIF 已经登陆。 它是一种源自 AV1 视频关键帧的新图像格式。 它是一种开放、免版税的格式,支持有损和无损压缩、动画、有损 Alpha 通道,并且可以处理锐利的线条和纯色(这是 JPEG 的一个问题),同时提供更好的结果。

    事实上,与 WebP 和 JPEG 相比, AVIF 的性能要好得多,在相同的 DSSIM 下(使用近似人类视觉的算法,两个或多个图像之间的(不)相似性),文件大小中值节省高达 50%。 事实上,在他关于优化图像加载的详尽文章中,Malte Ubl 指出,AVIF“在一个非常重要的方面始终优于 JPEG。这与 WebP 不同,WebP 并不总是产生比 JPEG 更小的图像,实际上可能是一个网络 -由于缺乏对渐进式加载的支持而导致的损失。”

    将 AVIF 显示为渐进增强的代码片段
    我们可以将 AVIF 用作渐进增强,将 WebP 或 JPEG 或 PNG 传递给旧版浏览器。 (大预览)。 请参阅下面的纯文本视图。

    具有讽刺意味的是,AVIF 的性能甚至比大型 SVG 还要好,尽管它当然不应被视为 SVG 的替代品。 它也是最早支持 HDR 颜色支持的图像格式之一; 提供更高的亮度、色位深度和色域。 唯一的缺点是目前 AVIF 不支持渐进式图像解码(还没有?),与 Brotli 类似,高压缩率编码目前相当慢,尽管解码速度很快。

    AVIF 目前在 Chrome、Firefox 和 Opera 中得到支持,而对 Safari 的支持预计很快就会到来(因为 Apple 是创建 AV1 的小组的成员)。

    那么,如今提供图像的最佳方式是什么? 对于插图和矢量图,(压缩的)SVG 无疑是最佳选择。 对于照片,我们使用带有picture元素的内容协商方法。 如果支持 AVIF,我们发送 AVIF 图像; 如果不是这样,我们首先回退到 WebP,如果 WebP 也不支持,我们切换到 JPEG 或 PNG 作为后备(如果需要,应用@media条件):

    <picture> <source type="image/avif"> <source type="image/webp"> <img src="image.jpg" alt="Photo" width="450" height="350"> </picture>

    坦率地说,我们更有可能在picture元素中使用一些条件:

    <picture> <source type="image/avif" /> <source type="image/webp" /> <source type="image/jpeg" /> <img src="fallback-image.jpg" alt="Photo" width="450" height="350"> </picture>
    <picture> <source type="image/avif" /> <source type="image/webp" /> <source type="image/jpeg" /> <img src="fallback-image.jpg" alt="Photo" width="450" height="350"> </picture>

    对于选择使用prefers-reduced-motion运动的客户,您可以通过将动画图像与静态图像交换更进一步:

    <picture> <source media="(prefers-reduced-motion: reduce)" type="image/avif"></source> <source media="(prefers-reduced-motion: reduce)" type="image/jpeg"></source> <source type="image/avif"></source> <img src="motion.jpg" alt="Animated AVIF"> </picture>
    <picture> <source media="(prefers-reduced-motion: reduce)" type="image/avif"></source> <source media="(prefers-reduced-motion: reduce)" type="image/jpeg"></source> <source type="image/avif"></source> <img src="motion.jpg" alt="Animated AVIF"> </picture>

    在过去的几个月里,AVIF 获得了相当大的关注:

    • 我们可以在 DevTools 的 Rendering 面板中测试 WebP/AVIF 回退。
    • 我们可以使用 Squoosh、AVIF.io 和 libavif 对 AVIF 文件进行编码、解码、压缩和转换。
    • 我们可以使用 Jake Archibald 的 AVIF Preact 组件,它在 worker 中解码 AVIF 文件并将结果显示在画布上,
    • 为了只向支持的浏览器提供 AVIF,我们可以使用 PostCSS 插件和 315B 脚本在您的 CSS 声明中使用 AVIF。
    • 我们可以使用 CSS 和 Cloudlare Workers 逐步交付新的图像格式,以动态更改返回的 HTML 文档,从accept头推断信息,然后根据需要添加webp/avif等类。
    • AVIF 已经在 Cloudinary 中可用(有使用限制),Cloudflare 在 Image Resizing 中支持 AVIF,您可以在 Netlify 中启用带有自定义 AVIF 标头的 AVIF。
    • 在动画方面,AVIF 的表现与 Safari 的<img src=mp4>一样好,总体上优于 GIF 和 WebP,但 MP4 的表现仍然更好。
    • 一般来说,对于动画,AVC1 (h264) > HVC1 > WebP > AVIF > GIF,假设基于 Chromium 的浏览器将永远支持<img src=mp4>
    • 您可以在 Netflix 的 Aditya Mavlankar 的 AVIF for Next Generation Image Coding 演讲和 Cloudflare 的 Kornel Lesinski 的 AVIF 图像格式演讲中找到有关 AVIF 的更多详细信息。
    • AVIF 的所有内容的绝佳参考:Jake Archibald 关于 AVIF 的综合帖子已登陆。

    那么未来的AVIF呢? Jon Sneyers 不同意:AVIF 的性能比 JPEG XL 差 60%,JPEG XL 是另一种由 Google 和 Cloudinary 开发的免费开放格式。 事实上,JPEG XL 似乎全面表现得更好。 但是,JPEG XL 仍处于标准化的最后阶段,还不能在任何浏览器中工作。 (不要与来自优秀的 Internet Explorer 9 次的 Microsoft JPEG-XR 混淆)。

响应式图像断点生成器
响应式图像断点生成器自动生成图像和标记。
  1. JPEG/PNG/SVG 是否正确优化?
    当您在着陆页上工作时,英雄图像的加载速度至关重要编码器专注于感知性能,并利用 Zopfli 和 WebP 的学习成果。 唯一的缺点:处理时间慢(每百万像素 CPU 需要一分钟)。

    对于 PNG,我们可以使用 Pingo,对于 SVG,我们可以使用 SVGO 或 SVGOMG。 如果您需要从网站快速预览、复制或下载所有 SVG 资源,svg-grabber 也可以为您完成。

    每一篇图像优化文章都会说明这一点,但保持矢量资源的干净和紧凑总是值得一提的。 确保清理未使用的资产,删除不必要的元数据并减少艺术品中的路径点数量(以及 SVG 代码)。 (谢谢,杰里米!

    不过,也有一些有用的在线工具可用:

    • 使用 Squoosh 以最佳压缩级别(有损或无损)压缩、调整大小和处理图像,
    • 使用 Guetzli.it 通过 Guetzli 压缩和优化 JPEG 图像,它适用于具有锐利边缘和纯色的图像(但可能会慢一些)。
    • 使用响应式图像断点生成器或 Cloudinary 或 Imgix 等服务来自动优化图像。 此外,在许多情况下,单独使用srcsetsizes将获得显着的好处。
    • 要检查响应式标记的效率,您可以使用imaging-heap,这是一个命令行工具,可以测量视口大小和设备像素比的效率。
    • 您可以将自动图像压缩添加到您的 GitHub 工作流程中,因此没有图像可以在未压缩的情况下投入生产。 该操作使用可处理 PNG 和 JPG 的 mozjpeg 和 libvips。
    • 为了优化存储,您可以使用 Dropbox 的新 Lepton 格式将 JPEG 平均压缩 22%。
    • 如果您想尽早显示占位符图像,请使用 BlurHash。 BlurHash 拍摄一张图片,并为您提供一个短字符串(仅 20-30 个字符!),代表该图片的占位符。 该字符串足够短,可以很容易地作为字段添加到 JSON 对象中。
    左侧没有图像占位符和右侧显示占位符的界面的比较
    BlurHash 是图像占位符的小型紧凑表示。 (大预览)

    有时仅优化图像并不能解决问题。 为了缩短开始渲染关键图像所需的时间,延迟加载不太重要的图像并延迟任何脚本在关键图像已经渲染后加载。 最安全的方法是混合延迟加载,当我们使用本机延迟加载和延迟加载时,该库可以检测通过用户交互触发的任何可见性更改(使用我们稍后将探讨的 IntersectionObserver)。 此外:

    • 考虑预加载关键图像,这样浏览器就不会太晚发现它们。 对于背景图片,如果你想更加激进,可以使用<img src>将图片添加为普通图片,然后将其隐藏在屏幕之外。
    • 考虑通过根据媒体查询指定不同的图像显示尺寸来使用 Sizes 属性交换图像,例如,操纵sizes以交换放大镜组件中的源。
    • 查看图像下载不一致的情况,以防止前景和背景图像的意外下载。 注意默认加载但可能永远不会显示的图像——例如在轮播、手风琴和图像画廊中。
    • 确保始终在图像上设置widthheight 。 注意 CSS 中的aspect-ratio属性和intrinsicsize属性,这将允许我们设置图像的宽高比和尺寸,因此浏览器可以提前预留一个预定义的布局槽以避免页面加载期间的布局跳转。
    显示编辑器中使用的 padding-top 和 aspect-ratio 元素的代码截图
    现在应该只是几周或几个月的事情,纵横比登陆浏览器。 已经在 Safari Technical Preview 118 中。 目前在 Firefox 和 Chrome 中处于领先地位。 (大预览)

    如果您喜欢冒险,您可以使用 Edge 工作程序(基本上是位于 CDN 上的实时过滤器)来切分和重新排列 HTTP/2 流,以通过网络更快地发送图像。 边缘工作者使用 JavaScript 流,这些流使用您可以控制的块(基本上它们是在 CDN 边缘上运行的 JavaScript,可以修改流响应),因此您可以控制图像的交付。

    使用服务人员,为时已晚,因为您无法控制线路上的内容,但它确实适用于 Edge 工作者。 因此,您可以在为特定登录页面逐步保存的静态 JPEG 之上使用它们。

    显示具有各种视口大小和设备像素比的表格的图像堆命令行工具的屏幕截图
    成像堆的示例输出,这是一个命令行工具,用于测量视口大小和设备像素比的效率。 (图片来源)(大预览)

    还不够好? 好吧,您还可以使用多背景图像技术提高图像的感知性能。 请记住,使用对比度和模糊不必要的细节(或去除颜色)也可以减小文件大小。 啊,你需要放大一张小照片而不损失质量吗? 考虑使用 Letsenhance.io。

    到目前为止,这些优化只涵盖了基础知识。 Addy Osmani 发布了一份关于基本图像优化的非常详细的指南,该指南非常深入地介绍了图像压缩和色彩管理的细节。 例如,您可以模糊图像的不必要部分(通过对其应用高斯模糊滤镜)以减小文件大小,最终您甚至可能开始去除颜色或将图片变为黑白以进一步减小大小. 对于背景图像,从 Photoshop 中以 0 到 10% 的质量导出照片也是完全可以接受的。

    在 Smashing Magazine 上,我们使用后缀-opt作为图像名称 - 例如, brotli-compression-opt.png ; 每当图像包含该后缀时,团队中的每个人都知道该图像已被优化。

    啊,不要在网络上使用 JPEG-XR ——“在 CPU 上解码 JPEG-XRs 软件端的处理抵消甚至超过了字节大小节省的潜在积极影响,尤其是在 SPA 的上下文中”(不是与 Cloudinary/Google 的 JPEG XL 混合)。

用视频元素替换动画 GIF,节省 80% 以上
Addy Osmani 建议用循环内嵌视频替换动画 GIF。 文件大小差异很明显(节省 80%)。 (大预览)
  1. 视频是否正确优化?
    到目前为止,我们涵盖了图像,但我们避免了关于好的 ol' GIF 的对话。 尽管我们喜欢 GIF,但现在是时候彻底放弃它们了(至少在我们的网站和应用程序中)。 与其加载影响渲染性能和带宽的繁重的动画 GIF,不如切换到动画 WebP(GIF 作为后备)或完全用循环的 HTML5 视频替换它们。

    与图像不同,浏览器不会预加载<video>内容,但 HTML5 视频往往比 GIF 更轻更小。 不是一个选项? 好吧,至少我们可以使用有损 GIF、gifsicle 或 giflossy 为 GIF 添加有损压缩。

    Colin Bendell 的测试表明,Safari 技术预览中img标签内的内嵌视频显示速度至少比 GIF 快 20 倍,解码速度快 7 倍,而且文件大小只是一小部分。 但是,其他浏览器不支持它。

    在好消息的土地上,视频格式多年来一直在大规模发展。 很长一段时间以来,我们一直希望 WebM 能够成为统治一切的格式,而 WebP(基本上是 WebM 视频容器内的一张静止图像)将成为过时的图像格式的替代品。 的确,Safari 现在支持 WebP,但尽管 WebP 和 WebM 近来获得了支持,但这一突破并没有真正发生。

    尽管如此,我们仍然可以将 WebM 用于大多数现代浏览器:

    <!-- By Houssein Djirdeh. https://web.dev/replace-gifs-with-videos/ --> <!-- A common scenartio: MP4 with a WEBM fallback. --> <video autoplay loop muted playsinline> <source src="my-animation.webm" type="video/webm"> <source src="my-animation.mp4" type="video/mp4"> </video>

    但也许我们可以完全重新审视它。 2018 年,开放媒体联盟发布了一种新的有前途的视频格式,称为AV1 。 AV1 具有类似于 H.265 编解码器(H.264 的演变)的压缩,但与后者不同的是,AV1 是免费的。 H.265 许可证定价促使浏览器供应商改用性能相当的 AV1: AV1(就像 H.265)的压缩率是 WebM 的两倍

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

    事实上,苹果目前使用的是 HEIF 格式和 HEVC(H.265),而最新 iOS 上的所有照片和视频都是以这些格式保存的,而不是 JPEG。 虽然 HEIF 和 HEVC (H.265) 还没有正确地暴露在网络上(还没有?),但 AV1 是——而且它正在获得浏览器的支持。 因此,在您的<video>标签中添加AV1源代码是合理的,因为所有浏览器供应商似乎都参与其中。

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

    需要更快地开始渲染视频但视频文件仍然太大? 例如,每当您在登录页面上有大型背景视频时? 一种常用的技术是首先将第一帧显示为静止图像,或者显示一个经过高度优化的短循环片段,可以解释为视频的一部分,然后,只要视频缓冲足够,就开始播放实际的视频。 Doug Sillars 编写了一份详细的背景视频性能指南,在这种情况下可能会有所帮助。 (谢谢,Guy Podjarny! )。

    对于上述场景,您可能需要提供响应式海报图片。 默认情况下, video元素只允许一张图片作为海报,这不一定是最佳的。 我们可以使用 Responsive Video Poster,这是一个 JavaScript 库,允许您为不同的屏幕使用不同的海报图像,同时还添加过渡叠加和视频占位符的完整样式控制。

    研究表明,视频流质量会影响观看者的行为。 事实上,如果启动延迟超过 2 秒左右,观众就会开始放弃视频。 超过这一点,延迟增加 1 秒会导致放弃率增加大约 5.8%。 因此,视频开始时间的中位数为 12.8 秒也就不足为奇了,其中 40% 的视频至少有 1 个停顿,20% 的视频至少有 2 秒的停顿视频播放。 事实上,在 3G 上,视频停顿是不可避免的,因为视频播放速度快于网络提供的内容。

    那么,解决方案是什么? 通常小屏幕设备无法处理我们为桌面服务的 720p 和 1080p。 根据 Doug Sillars 的说法,我们可以创建更小的视频版本,并使用 Javascript 来检测小屏幕的来源,以确保在这些设备上快速流畅地播放。 或者,我们可以使用流式视频。 HLS 视频流将向设备提供适当大小的视频——抽象出为不同屏幕创建不同视频的需要。 它还将协商网络速度,并根据您使用的网络速度调整视频比特率。

    为了避免带宽浪费,我们只能为实际可以播放视频的设备添加视频源。 或者,我们可以完全从video标签中删除autoplay属性,并使用 JavaScript 为更大的屏幕插入autoplay 。 此外,我们需要在video上添加preload="none"来告诉浏览器在它真正需要文件之前不要下载任何视频文件:

    <!-- Based on Doug Sillars's post. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ --> <video preload="none" playsinline muted loop width="1920" height="1080" poster="poster.jpg"> <source src="video.webm" type="video/webm"> <source src="video.mp4" type="video/mp4"> </video>

    然后我们可以专门针对实际支持 AV1 的浏览器:

    <!-- Based on Doug Sillars's post. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ --> <video preload="none" playsinline muted loop width="1920" height="1080" poster="poster.jpg"> <source src="video.av1.mp4" type="video/mp4; codecs=av01.0.05M.08"> <source src="video.hevc.mp4" type="video/mp4; codecs=hevc"> <source src="video.webm" type="video/webm"> <source src="video.mp4" type="video/mp4"> </video>

    然后我们可以在某个阈值(例如 1000 像素)上重新添加autoplay

    /* By Doug Sillars. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ */ <script> window.onload = addAutoplay(); var videoLocation = document.getElementById("hero-video"); function addAutoplay() { if(window.innerWidth > 1000){ videoLocation.setAttribute("autoplay",""); }; } </script>
    条形图按设备和网络速度(包括 3G、Cable、LTE 和本机)显示 Alcatel 1X、Moto G、Moto G4、MotoE、Nexus 5 和 OnePlus 5 的小时间(毫秒)
    按设备和网络速度划分的档数。 速度更快的网络上的速度更快的设备几乎没有停顿。 根据道格·西拉斯的研究。 (大预览)

    视频播放性能本身就是一个故事,如果您想详细了解它,请查看 Doug Sillars 关于视频当前状态和视频交付最佳实践的另一个系列,其中包括有关视频交付指标的详细信息、视频预加载、压缩和流式传输。 最后,您可以使用 Stream 或 Not 检查视频流的速度或速度。

Zach Leatherman 的字体加载策略综合指南显示为思维导图
Zach Leatherman 的字体加载策略综合指南提供了十几种更好的 Web 字体交付选项。
  1. 网络字体交付是否优化?
    值得一问的第一个问题是,我们是否可以一开始就使用 UI 系统字体——我们只需要确保仔细检查它们在各种平台上的显示是否正确。 如果不是这种情况,我们提供的网络字体很可能包含字形以及未使用的额外功能和权重。 我们可以要求我们的字体铸造厂对Web 字体进行子集化,或者如果我们使用开源字体,则使用 Glyphhanger 或 Fontsquirrel 自行对它们进行子集化。 我们甚至可以使用 Peter Muller 的子字体自动化我们的整个工作流程,这是一个命令行工具,可以静态分析您的页面以生成最佳的网络字体子集,然后将它们注入我们的页面。

    WOFF2 支持很棒,我们可以使用 WOFF 作为不支持它的浏览器的后备——或者也许可以为旧版浏览器提供系统字体。 Web 字体加载有很多很多的选项,我们可以从 Zach Leatherman 的“字体加载策略综合指南”中选择一种策略(代码片段也可以作为 Web 字体加载食谱提供)。

    今天要考虑的更好的选择可能是带有preload的关键 FOFT 和“妥协”方法。 他们都使用两阶段渲染来分步交付网络字体——首先是一个小的超子集,需要使用网络字体快速准确地渲染页面,然后异步加载系列的其余部分。 不同之处在于“The Compromise”技术仅在不支持字体加载事件时才异步加载 polyfill,因此默认情况下不需要加载 polyfill。 需要快速获胜吗? Zach Leatherman 有一个 23 分钟的快速教程和案例研究,可以让您的字体井井有条。

    一般来说,使用preload资源提示来预加载字体可能是一个好主意,但在您的标记中包含指向关键 CSS 和 JavaScript 的链接之后的提示。 对于preload ,存在优先级难题,因此请考虑在外部阻塞脚本之前将rel="preload"元素注入 DOM。 根据 Andy Davies 的说法,“使用脚本注入的资源在脚本执行之前对浏览器是隐藏的,当浏览器发现preload提示时,我们可以使用这种行为来延迟。” 否则,字体加载将在第一次渲染时花费您。

    幻灯片 93 的屏幕截图显示了两个图像示例,它们旁边的标题是“指标优先级:预加载每个系列中的一个”
    当一切都很关键时,没有什么是关键的。 仅预加载每个系列的一种或最多两种字体。 (图片来源:Zach Leatherman - 幻灯片 93)(大预览)

    有选择性并选择最重要的文件是个好主意,例如那些对渲染至关重要的文件或有助于避免可见和破坏性文本重排的文件。 一般来说,Zach 建议预加载每个系列的一到两种字体——如果它们不太重要,延迟一些字体加载也是有意义的。

    @font-face规则中定义font-family时,使用local()值(按名称引用本地字体)已变得非常普遍:

     /* Warning! Not a good idea! */ @font-face { font-family: Open Sans; src: local('Open Sans Regular'), local('OpenSans-Regular'), url('opensans.woff2') format ('woff2'), url('opensans.woff') format('woff'); }

    这个想法是有道理的:一些流行的开源字体,例如Open Sans,会预装一些驱动程序或应用程序,所以如果字体在本地可用,浏览器不需要下载网络字体,可以显示本地立即字体。 正如 Bram Stein 所指出的,“虽然本地字体与网络字体的名称匹配,但它很可能不是同一种字体。许多网络字体与其“桌面”版本不同。文本可能呈现不同,某些字符可能会下降回到其他字体,OpenType 功能可能完全缺失,或者行高可能不同。”

    此外,随着字体随着时间的推移而演变,本地安装的版本可能与 Web 字体大不相同,字符看起来也大不相同。 因此,根据 Bram 的说法,最好不要在@font-face规则中混合本地安装的字体和 Web 字体。 除了 Android 对 Roboto 的请求外,Google 字体也对所有用户的 CSS 结果禁用local()

    没有人喜欢等待内容显示。 使用font-display CSS 描述符,我们可以控制字体加载行为并使内容能够立即(使用font-display: optional )或几乎立即(超时 3 秒,只要字体被成功下载——使用font-display: swap )。 (嗯,它比这更复杂一些。)

    但是,如果您想尽量减少文本重排的影响,我们可以使用字体加载 API (在所有现代浏览器中都支持)。 具体来说,这意味着对于每种字体,我们将创建一个FontFace对象,然后尝试获取所有字体,然后才将它们应用到页面上。 这样,我们通过异步加载所有字体来对所有重绘进行分组,然后恰好从备用字体切换到网络字体一次。 看看 Zach 的解释,从 32:15 开始,以及代码片段):

    /* Load two web fonts using JavaScript */ /* Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // Remove existing @font-face blocks // Create two let font = new FontFace("Noto Serif", /* ... */); let fontBold = new FontFace("Noto Serif, /* ... */); // Load two fonts let fonts = await Promise.all([ font.load(), fontBold.load() ]) // Group repaints and render both fonts at the same time! fonts.forEach(font => documents.fonts.add(font));
    /* Load two web fonts using JavaScript */ /* Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // Remove existing @font-face blocks // Create two let font = new FontFace("Noto Serif", /* ... */); let fontBold = new FontFace("Noto Serif, /* ... */); // Load two fonts let fonts = await Promise.all([ font.load(), fontBold.load() ]) // Group repaints and render both fonts at the same time! fonts.forEach(font => documents.fonts.add(font));

    为了在使用 Font Loading API 的情况下尽早开始获取字体,Adrian Bece 建议添加一个不间断空格nbsp;body的顶部,并使用aria-visibility: hidden.hidden类在视觉上隐藏它:

    <body class="no-js"> <!-- ... Website content ... --> <div aria-visibility="hidden" class="hidden"> <!-- There is a non-breaking space here --> </div> <script> document.getElementsByTagName("body")[0].classList.remove("no-js"); </script> </body>
    <body class="no-js"> <!-- ... Website content ... --> <div aria-visibility="hidden" class="hidden"> <!-- There is a non-breaking space here --> </div> <script> document.getElementsByTagName("body")[0].classList.remove("no-js"); </script> </body>

    这与针对不同加载状态声明不同字体系列的 CSS 一起使用,一旦字体成功加载,更改由 Font Loading API 触发:

    body:not(.wf-merriweather--loaded):not(.no-js) { font-family: [fallback-system-font]; /* Fallback font styles */ } .wf-merriweather--loaded, .no-js { font-family: "[web-font-name]"; /* Webfont styles */ } /* Accessible hiding */ .hidden { position: absolute; overflow: hidden; clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0; }
    body:not(.wf-merriweather--loaded):not(.no-js) { font-family: [fallback-system-font]; /* Fallback font styles */ } .wf-merriweather--loaded, .no-js { font-family: "[web-font-name]"; /* Webfont styles */ } /* Accessible hiding */ .hidden { position: absolute; overflow: hidden; clip: rect(0 0 0 0); height: 1px; width: 1px; margin: -1px; padding: 0; border: 0; }

    如果您想知道为什么尽管进行了所有优化,Lighthouse 仍然建议消除渲染阻塞资源(字体),在同一篇文章中,Adrian Bece 提供了一些让 Lighthouse 满意的技术,以及 Gatsby Omni Font Loader,一种高性能异步字体Gatsby 的加载和 Flash Of Unstyled Text (FOUT) 处理插件。

    现在,我们中的许多人可能正在使用 CDN 或第三方主机来加载 Web 字体。 一般来说,如果可以的话,自托管所有静态资产总是更好,因此请考虑使用 google-webfonts-helper,这是一种自托管 Google 字体的轻松方式。 如果不可能,您也许可以通过页面来源代理 Google 字体文件。

    值得注意的是,虽然 Google 做了很多开箱即用的工作,所以服务器可能需要一些调整以避免延迟(谢谢,巴里!

    这一点非常重要,尤其是自 Chrome v86(2020 年 10 月发布)以来,由于浏览器缓存分区,字体等跨站点资源无法再在同一个 CDN 上共享。 这种行为多年来一直是 Safari 的默认行为。

    但是,如果根本不可能,有一种方法可以使用 Harry Roberts 的代码段获得最快的 Google 字体:

    <!-- By Harry Roberts. https://csswizardry.com/2020/05/the-fastest-google-fonts/ - 1. Preemptively warm up the fonts' origin. - 2. Initiate a high-priority, asynchronous fetch for the CSS file. Works in - most modern browsers. - 3. Initiate a low-priority, asynchronous fetch that gets applied to the page - only after it's arrived. Works in all browsers with JavaScript enabled. - 4. In the unlikely event that a visitor has intentionally disabled - JavaScript, fall back to the original method. The good news is that, - although this is a render-blocking request, it can still make use of the - preconnect which makes it marginally faster than the default. --> <!-- [1] --> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <!-- [2] --> <link rel="preload" as="style" href="$CSS&display=swap" /> <!-- [3] --> <link rel="stylesheet" href="$CSS&display=swap" media="print" onload="this.media='all'" /> <!-- [4] --> <noscript> <link rel="stylesheet" href="$CSS&display=swap" /> </noscript>

    Harry 的策略是先发制人地预热字体的起源。 然后我们为 CSS 文件启动一个高优先级的异步获取。 之后,我们启动一个低优先级的异步获取,该获取仅在页面到达后才应用于页面(使用打印样式表技巧)。 最后,如果不支持 JavaScript,我们将退回到原始方法。

    啊,说到谷歌字体:你可以通过&text只声明你需要的字符来减少谷歌字体请求的90% 的大小。 另外,最近谷歌字体也增加了对字体显示的支持,所以我们可以直接使用它。

    不过要注意一点。 如果您使用font-display: optional ,那么使用preload可能不是最佳选择,因为它会提前触发该 Web 字体请求(如果您有其他需要获取的关键路径资源,则会导致网络拥塞)。 使用preconnect来加快跨域字体请求,但要小心preload ,因为从不同来源预加载字体会导致网络争用。 所有这些技术都包含在 Zach 的 Web 字体加载配方中。

    另一方面,如果用户在可访问性首选项中启用了减少运动或选择了数据保护模式(请参阅Save-Data标题),则选择退出网络字体(或至少第二阶段渲染)可能是个好主意,或者当用户连接速度较慢时(通过网络信息 API)。

    如果用户选择了数据保存模式(还有其他用例),我们还可以使用prefers-reduced-data CSS 媒体查询来定义字体声明。 如果来自客户端提示 HTTP 扩展的Save-Data请求标头打开/关闭以允许与 CSS 一起使用,则媒体查询基本上会公开。 目前仅支持在 Chrome 和 Edge 后面的标志。

    指标? 要衡量 Web 字体加载性能,请考虑All Text Visible指标(所有字体已加载且所有内容以 Web 字体显示的时刻)、Time to Real Italics 以及首次渲染后的Web Font Reflow Count 。 显然,这两个指标越低,性能越好。

    你可能会问,可变字体呢? 重要的是要注意可变字体可能需要重要的性能考虑。 它们为我们提供了更广泛的印刷选择设计空间,但它的代价是单个串行请求而不是多个单独的文件请求。

    虽然可变字体大大减少了字体文件的整体组合文件大小,但该单个请求可能会很慢,从而阻止页面上所有内容的呈现。 所以子集化和将字体分割成字符集仍然很重要。 不过好的一面是,使用可变字体,默认情况下我们会得到一个重排,因此不需要 JavaScript 来对重绘进行分组。

    现在,怎样才能制定防弹的网络字体加载策略呢? 子集字体并为 2-stage-render 准备它们,使用字体font-display描述符声明它们,使用 Font Loading API 对重绘进行分组并将字体存储在持久服务工作者的缓存中。 在第一次访问时,在阻塞外部脚本之前注入脚本的预加载。 如有必要,您可以使用 Bram Stein 的 Font Face Observer。 如果您对测量字体加载的性能感兴趣,Andreas Marschke 将探索使用 Font API 和 UserTiming API 进行的性能跟踪。

    最后,不要忘记包含unicode-range以将大字体分解为较小的特定语言字体,并使用 Monica Dinculescu 的字体样式匹配器来最小化布局中的不和谐变化,因为后备和网页字体。

    或者,要为备用字体模拟 Web 字体,我们可以使用 @font-face 描述符来覆盖字体指标(演示,在 Chrome 87 中启用)。 (请注意,调整会因复杂的字体堆栈而变得复杂。)

    未来看起来光明吗? 通过渐进式字体丰富,最终我们可能能够“在任何给定页面上仅下载所需的字体部分,并且对于该字体的后续请求,可以根据后续页面的要求使用额外的字形集动态地‘修补’原始下载意见”,正如 Jason Pamental 解释的那样。 增量传输演示已经可用,并且正在进行中。

构建优化

  1. 我们是否确定了我们的优先事项?
    最好先知道你在处理什么。 对您的所有资产(JavaScript、图像、字体、第三方脚本和页面上的“昂贵”模块,例如轮播、复杂的信息图表和多媒体内容)进行清点,并将它们分组分解。

    设置电子表格。 定义旧版浏览器的基本核心体验(即完全可访问的核心内容)、功能强大的浏览器的增强体验(即丰富、完整的体验)和附加功能(非绝对必需且可以延迟加载的资产,例如网络字体、不必要的样式、轮播脚本、视频播放器、社交媒体小部件、大图像)。 多年前,我们发表了一篇关于“提高 Smashing Magazine 的性能”的文章,详细描述了这种方法。

    在优化性能时,我们需要反映我们的优先事项。 立即加载核心体验,然后是增强功能,然后是附加功能

  2. 你在生产中使用原生 JavaScript 模块吗?
    还记得将核心体验发送到旧版浏览器并增强现代浏览器体验的出色技巧吗? 该技术的更新变体可以使用 ES2017+ <script type="module"> ,也称为模块/无模块模式(也由 Jeremy Wagner 作为差分服务引入)。

    这个想法是编译并提供两个独立的 JavaScript 包:“常规”构建,一个带有 Babel-transforms 和 polyfill,只为真正需要它们的旧版浏览器提供它们,另一个包(相同功能)没有转换或填充物。

    因此,我们通过减少浏览器需要处理的脚本数量来帮助减少主线程的阻塞。 Jeremy Wagner 发表了一篇关于差异服务以及如何在构建管道中设置它的综合文章,从设置 Babel 到您需要在 Webpack 中进行哪些调整,以及完成所有这些工作的好处。

    默认情况下,原生 JavaScript 模块脚本被延迟,因此在进行 HTML 解析时,浏览器将下载主模块。

    一个显示原生 JavaScript 模块默认情况下如何延迟的示例
    默认情况下,原生 JavaScript 模块被延迟。 几乎所有关于原生 JavaScript 模块的东西。 (大预览)

    不过需要注意的一点是: module/nomodule 模式在某些客户端上可能会适得其反,因此您可能需要考虑一种解决方法:Jeremy 风险较小的差异服务模式,然而,它避开了预加载扫描器,这可能会以一种可能不会影响性能的方式预料。 (谢谢,杰里米!

    实际上,Rollup 支持将模块作为输出格式,因此我们既可以捆绑代码,也可以在生产环境中部署模块。 Parcel 在 Parcel 2 中具有模块支持。对于 Webpack,module-nomodule-plugin 自动生成模块/nomodule 脚本。

    注意:值得一提的是,仅凭功能检测不足以对发送到该浏览器的有效负载做出明智的决定。 就其本身而言,我们无法从浏览器版本推断设备能力。 例如,发展中国家的廉价 Android 手机大多运行 Chrome,尽管内存和 CPU 功能有限,但仍会减少芥末味。

    最终,使用设备内存客户端提示头,我们将能够更可靠地定位低端设备。 在撰写本文时,仅在 Blink 中支持标头(通常用于客户端提示)。 由于设备内存也有一个在 Chrome 中可用的 JavaScript API,一个选项可能是基于 API 进行功能检测,如果不支持则回退到模块/无模块模式谢谢,Yoav! )。

  3. 您是否在使用摇树、范围提升和代码拆分?
    Tree-shaking 是一种清理构建过程的方法,它只包含生产中实际使用的代码,并消除 Webpack 中未使用的导入。 使用 Webpack 和 Rollup,我们还具有范围提升功能,这两种工具都可以检测import链在哪里可以展平并转换为一个内联函数,而不会影响代码。 使用 Webpack,我们也可以使用 JSON Tree Shaking。

    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 的代码覆盖来实现它。

    在处理单页应用程序时,我们需要一些时间来初始化应用程序,然后才能渲染页面。 您的设置将需要您的自定义解决方案,但您可以注意模块和技术以加快初始渲染时间。 例如,这里介绍了如何调试 React 性能并消除常见的 React 性能问题,以及如何提高 Angular 中的性能。 一般来说,大多数性能问题都来自启动应用程序的初始时间。

    那么,什么是积极但又不过分积极地进行代码拆分的最佳方式呢? 根据 Phil Walton 的说法,“除了通过动态导入进行代码拆分之外,[我们] 还可以在包级别使用代码拆分,其中每个导入的节点模块都会根据其包的名称放入一个块中。” Phil 还提供了有关如何构建它的教程。

  4. 我们可以改进 Webpack 的输出吗?
    由于 Webpack 通常被认为是神秘的,因此有很多 Webpack 插件可能会派上用场,以进一步减少 Webpack 的输出。 以下是一些可能需要更多关注的更晦涩难懂的内容。

    其中一个有趣的来自 Ivan Akulov 的主题。 想象一下,您有一个函数,您调用一次,将其结果存储在一个变量中,然后不使用该变量。 摇树将删除变量,但不会删除函数,因为它可能会被使用。 但是,如果该功能未在任何地方使用,您可能需要将其删除。 为此,请在函数调用前加上/*#__PURE__*/ ,这是 Uglify 和 Terser 支持的 - 完成!

    编辑器中的 JS 代码截图,展示了如何使用 PURE 函数
    要在未使用其结果时删除此类函数,请在函数调用前添加/*#__PURE__*/ 。 通过 Ivan Akulov。(大预览)

    以下是 Ivan 推荐的其他一些工具:

    • purgecss-webpack-plugin 删除未使用的类,尤其是在您使用 Bootstrap 或 Tailwind 时。
    • 使用 split-chunks-plugin 启用optimization.splitChunks: 'all' 。 这将使 webpack 自动对您的条目包进行代码拆分,以实现更好的缓存。
    • 设置optimization.runtimeChunk: true 。 这会将 webpack 的运行时移动到一个单独的块中——并且还会改进缓存。
    • google-fonts-webpack-plugin 下载字体文件,因此您可以从您的服务器提供它们。
    • workbox-webpack-plugin 允许您为所有 webpack 资产生成具有预缓存设置的服务工作者。 此外,请查看 Service Worker Packages,这是一份可以立即应用的模块综合指南。 或者使用 preload-webpack-plugin 为所有 JavaScript 块生成preload / prefetch
    • speed-measure-webpack-plugin 测量你的 webpack 构建速度,提供构建过程中哪些步骤最耗时的洞察。
    • 当你的包包含同一个包的多个版本时,duplicate-package-checker-webpack-plugin 会发出警告。
    • 使用范围隔离并在编译时动态缩短 CSS 类名。

一个终端屏幕截图,展示了如何使用名为 responsive-loader 的 webpack 加载程序来帮助您生成开箱即用的响应式图像
加速您的图像是在较小的屏幕上提供较小的图片。 使用响应式加载器。 通过伊万·阿库洛夫。 (大预览)
  1. 你可以将 JavaScript 卸载到 Web Worker 中吗?
    为了减少对 Time-to-Interactive 的负面影响,考虑将繁重的 JavaScript 卸载到 Web Worker 中可能是一个好主意。

    随着代码库的不断增长,UI 性能瓶颈会出现,从而降低用户体验。 这是因为 DOM 操作在主线程上与 JavaScript 一起运行。 使用网络工作者,我们可以将这些昂贵的操作转移到在不同线程上运行的后台进程。 Web Worker 的典型用例是预取数据和渐进式 Web 应用程序以提前加载和存储一些数据,以便您以后可以在需要时使用它。 您可以使用 Comlink 来简化主页和工作人员之间的通信。 还有一些工作要做,但我们正在到达那里。

    有一些关于 Web Worker 的有趣案例研究展示了将框架和应用程序逻辑移动到 Web Worker 的不同方法。 结论:总的来说,仍然存在一些挑战,但已经有一些很好的用例(感谢 Ivan Akulov! )。

    从 Chrome 80 开始,一种具有 JavaScript 模块性能优势的Web 工作者新模式已经发布,称为模块工作者。 我们可以更改脚本加载和执行以匹配script type="module" ,此外,我们还可以对延迟加载代码使用动态导入,而不会阻塞工作线程的执行。

    如何开始? 以下是一些值得研究的资源:

    • Surma 发布了一篇关于如何在浏览器的主线程中运行 JavaScript 以及何时应该使用 Web Workers 的优秀指南。
    • 另外,请查看 Surma 关于主线程架构的讨论。
    • Shubhie Panicker 和 Jason Miller 的 A Quest to Guarantee Responsiveness 提供了有关如何使用 Web 工作者以及何时避免使用它们的详细见解。
    • 摆脱用户的方式:使用 Web Worker 减少 Jank 重点介绍了使用 Web Worker 的有用模式、worker 之间通信的有效方法、处理主线程之外的复杂数据处理以及测试和调试它们。
    • Workerize 允许您将模块移动到 Web Worker 中,自动将导出的函数反映为异步代理。
    • 如果你使用 Webpack,你可以使用 workerize-loader。 或者,您也可以使用 worker-plugin。
    左侧显示的 DOM 中的代码作为使用 Web Worker 时使用和避免使用的示例
    当代码阻塞很长时间时使用 web worker,但是当你依赖 DOM、处理输入响应并且需要最小的延迟时避免使用它们。 (通过 Addy Osmani)(大预览)

    请注意,Web Worker 无权访问 DOM,因为 DOM 不是“线程安全的”,它们执行的代码需要包含在单独的文件中。

  2. 你可以将“热路径”卸载到 WebAssembly 吗?
    我们可以将繁重的计算任务转移到 WebAssembly ( WASM ),这是一种二进制指令格式,被设计为用于编译 C/C++/Rust 等高级语言的可移植目标。 它的浏览器支持非常出色,并且随着 JavaScript 和 WASM 之间的函数调用变得越来越快,它最近变得可行。 此外,它甚至还支持 Fastly 的边缘云。

    当然,WebAssembly 不应该取代 JavaScript,但它可以在你注意到 CPU 占用的情况下对其进行补充。 对于大多数 Web 应用程序,JavaScript 更适合,而 WebAssembly 最适合用于计算密集型 Web 应用程序,例如游戏。

    如果您想了解有关 WebAssembly 的更多信息:

    • Lin Clark 为 WebAssembly 编写了一个详尽的系列文章,Milica Mihajlija 提供了有关如何在浏览器中运行本机代码的总体概述,为什么要这样做,以及这对 JavaScript 和 Web 开发的未来意味着什么。
    • 我们如何使用 WebAssembly 将我们的 Web 应用程序加速 20 倍(案例研究)重点介绍了如何用编译的 WebAssembly 取代缓慢的 JavaScript 计算并带来显着的性能改进的案例研究。
    • Patrick Hamann 一直在谈论 WebAssembly 日益重要的角色,他揭穿了一些关于 WebAssembly 的神话,探索了它的挑战,我们可以在今天的应用程序中实际使用它。
    • Google Codelabs 提供了 WebAssembly 简介,这是一个 60 分钟的课程,您将在其中学习如何使用 C 语言获取本机代码并将其编译为 WebAssembly,然后直接从 JavaScript 调用它。
    • Alex Danilo 在他的 Google I/O 演讲中解释了 WebAssembly 及其工作原理。 此外,Benedek Gagyi 分享了一个关于 WebAssembly 的实际案例研究,特别是团队如何将其用作其 C++ 代码库到 iOS、Android 和网站的输出格式。

    仍然不确定何时使用 Web Workers、Web Assembly、流或 WebGL JavaScript API 来访问 GPU? 加速 JavaScript 是一个简短但有用的指南,它解释了何时使用什么以及为什么使用 - 还带有一个方便的流程图和大量有用的资源。

左侧显示 C++、C 或 Rust 的插图,箭头显示浏览器,其中包含添加到 JavaScript、CSS 和 HTML 的 WASM 二进制文件
Milica Mihajlija 概述了 WebAssembly 的工作原理以及它为何有用。 (大预览)
  1. 我们是否只为旧版浏览器提供旧版代码?
    由于 ES2017 在现代浏览器中得到了很好的支持,我们可以使用babelEsmPlugin仅转换您所针对的现代浏览器不支持的 ES2017+ 功能。

    Houssein Djirdeh 和 Jason Miller 最近发布了一份关于如何转译和提供现代和遗留 JavaScript 的综合指南,详细介绍了如何使其与 Webpack 和 Rollup 一起使用,以及所需的工具。 您还可以估计可以在您的网站或应用程序包上减少多少 JavaScript。

    所有主流浏览器都支持 JavaScript 模块,因此使用 use script type="module"让支持 ES 模块的浏览器加载文件,而旧版浏览器可以加载带有script nomodule的旧版本。

    如今,我们可以编写在浏览器中本地运行的基于模块的 JavaScript,无需转译器或捆绑器。 <link rel="modulepreload">标头提供了一种启动模块脚本的早期(和高优先级)加载的方法。 基本上,这是一种帮助最大化带宽使用的好方法,它告诉浏览器它需要获取什么,这样它就不会在那些漫长的往返过程中遇到任何事情。 此外,Jake Archibald 发表了一篇详细的文章,其中包含值得一读的 ES 模块的陷阱和注意事项。

内联脚本被推迟到阻塞外部脚本和内联脚本被执行
Jake Archibald 发表了一篇详细的文章,其中包含 ES 模块的陷阱和注意事项,例如,内联脚本被推迟到阻塞外部脚本和内联脚本被执行。 (大预览)
  1. 通过增量解耦识别和重写遗留代码
    长期存在的项目往往会积聚灰尘和过时的代码。 重新审视您的依赖关系并评估需要多少时间来重构或重写最近造成问题的遗留代码。 当然,这总是一项艰巨的任务,但是一旦您了解遗留代码的影响,您就可以从增量解耦开始。

    首先,设置指标来跟踪遗留代码调用的比率是保持不变还是下降,而不是上升。 公开劝阻团队不要使用该库,并确保您的 CI 会在拉取请求中使用它时提醒开发人员。 polyfill 可以帮助从遗留代码过渡到使用标准浏览器功能的重写代码库。

  2. 识别并删除未使用的 CSS/JS
    Chrome 中的 CSS 和 JavaScript 代码覆盖率可让您了解哪些代码已执行/应用,哪些尚未执行/应用。 您可以开始记录覆盖率,在页面上执行操作,然后探索代码覆盖率结果。 检测到未使用的代码后,使用import()找到这些模块并延迟加载(查看整个线程)。 然后重复覆盖率配置文件并验证它现在在初始加载时发送的代码更少。

    您可以使用 Puppeteer 以编程方式收集代码覆盖率。 Chrome 也允许您导出代码覆盖率结果。 正如 Andy Davies 所指出的,您可能希望收集现代和旧版浏览器的代码覆盖率。

    Puppetter 还有许多其他用例和工具可能需要更多介绍:

    • Puppeteer 的用例,例如,自动视觉差异或在每次构建时监控未使用的 CSS,
    • 使用 Puppeteer 的 Web 性能食谱,
    • 用于记录和生成 Pupeeteer 和 Playwright 脚本的有用工具,
    • 此外,您甚至可以直接在 DevTools 中记录测试,
    • Nitay Neeman 对 Puppeteer 的全面概述,包括示例和用例。
    左边是 Puppeteer Recorder 的截图,右边是 Puppeteer Sandbox 的截图
    我们可以使用 Puppeteer Recorder 和 Puppeteer Sandbox 来记录浏览器交互并生成 Puppeteer 和 Playwright 脚本。 (大预览)

    此外,purgecss、UnCSS 和 Helium 可以帮助您从 CSS 中删除未使用的样式。 如果您不确定某处是否使用了可疑代码,您可以遵循 Harry Roberts 的建议:为特定类创建一个 1×1px 透明 GIF 并将其放入dead/目录,例如/assets/img/dead/comments.gif

    之后,您将特定图像设置为 CSS 中相应选择器的背景,然后等待几个月,如果该文件将出现在您的日志中。 如果没有条目,则没有人会在他们的屏幕上呈现该遗留组件:您可能可以继续将其全部删除。

    对于I-feel-adventurous部门,您甚至可以通过使用 DevTools 监控 DevTools,通过一组页面自动收集未使用的 CSS。

Webpack 对比表
Benedikt Rotsch 在他的文章中表明,从 Moment.js 切换到 date-fns 可以为 3G 和低端手机上的 First Paint 节省大约 300 毫秒。 (大预览)
  1. 修剪 JavaScript 包的大小。
    正如 Addy Osmani 所指出的,当您只需要一小部分时,您很有可能会发布完整的 JavaScript 库,以及用于不需要它们的浏览器的过时 polyfill,或者只是重复代码。 为避免开销,请考虑使用 webpack-libs-optimizations 在构建过程中删除未使用的方法和 polyfill。

    检查并审查您发送到旧版浏览器和现代浏览器的polyfill ,并对它们更具战略性。 看看 polyfill.io,它是一个服务,它接受对一组浏览器功能的请求,并仅返回请求浏览器所需的 polyfill。

    捆绑审计也添加到您的常规工作流程中。 您多年前添加的重型库可能有一些轻量级替代品,例如 Moment.js(现已停产)可以替换为:

    • 原生国际化 API,
    • Day.js 具有熟悉的 Moment.js API 和模式,
    • 日期-fns 或
    • 卢克森。
    • 您还可以使用 Skypack Discover,它将人工审核的包裹推荐与注重质量的搜索相结合。

    Benedikt Rotsch 的研究表明,从 Moment.js 切换到 date-fns 可以为 3G 和低端手机上的 First Paint 节省大约 300 毫秒。

    对于包审计,Bundlephobia 可以帮助您找到将 npm 包添加到包中的成本。 size-limit 通过 JavaScript 执行时间的详细信息扩展了基本的包大小检查。 您甚至可以将这些成本与 Lighthouse 自定义审计相结合。 这也适用于框架。 通过删除或修剪 Vue MDC 适配器(Vue 的材料组件),样式从 194KB 下降到 10KB。

    还有许多其他工具可帮助您就依赖项的影响和可行的替代方案做出明智的决定:

    • webpack-bundle-analyzer
    • 源地图浏览器
    • 捆绑好友
    • 捆绑恐惧症
    • Webpack 分析显示了为什么将特定模块包含在包中。
    • bundle-wizard 还为整个页面构建依赖关系图。
    • Webpack 大小插件
    • 可视化代码的导入成本

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

size-limit 提供基本的包大小检查以及 JavaScript 执行时间的详细信息
size-limit 提供基本的包大小检查以及 JavaScript 执行时间的详细信息。 (大预览)
  1. 我们是否使用部分补水?
    随着应用程序中使用的 JavaScript 数量,我们需要想办法尽可能少地发送给客户端。 这样做的一种方法——我们已经简要介绍过——是部分补水。 这个想法很简单:不是执行 SSR 然后将整个应用程序发送到客户端,而是只将应用程序的一小部分 JavaScript 发送到客户端然后进行水合。 我们可以将其视为在一个静态网站上具有多个渲染根的多个微型 React 应用程序。

    在文章“部分水合的案例(使用 Next 和 Preact)”中,Lukas Bombach 解释了德国新闻媒体之一 Welt.de 背后的团队如何通过部分水合取得更好的性能。 您还可以查看具有解释和代码片段的 next-super-performance GitHub 存储库。

    您还可以考虑其他选择:

    • Preact 和 Eleventy 的部分水合作用,
    • React GitHub repo 中的渐进式水合作用,
    • Vue.js 中的惰性水合(GitHub 存储库),
    • 当用户与需要它的 UI 交互时,导入交互模式以延迟加载非关键资源(例如组件、嵌入)。

    Jason Miller 发布了关于如何使用 React 实现渐进式水合作用的工作演示,因此您可以立即使用它们:演示 1、演示 2、演示 3(也可在 GitHub 上获得)。 另外,您可以查看 react-prerendered-component 库。

    在 Google Docs 中 loadshare() 时 +485KB 的 JavaScript
    仅当您无法在交互之前预取资源时,才应该对第一方代码进行交互时导入。 (大预览)
  2. 我们是否优化了 React/SPA 的策略?
    在单页应用程序中苦苦挣扎? Jeremy Wagner 探讨了客户端框架性能对各种设备的影响,重点介绍了我们在使用某个设备时可能需要注意的一些含义和指导方针。

    因此,这是 Jeremy 建议用于 React 框架的 SPA 策略(但对于其他框架,它不应该有显着变化):

    • 尽可能将有状态组件重构为无状态组件
    • 尽可能预渲染无状态组件以最小化服务器响应时间。 仅在服务器上渲染。
    • 对于具有简单交互性的有状态组件,请考虑对该组件进行预渲染或服务器渲染,并将其交互性替换为与框架无关的事件侦听器
    • 如果您必须在客户端上对有状态的组件进行水合,请在可见性或交互上使用惰性水合。
    • 对于延迟水合的组件,请在主线程空闲时间使用requestIdleCallback安排它们的水合。

    您可能还想追求或回顾其他一些策略:

    • React 应用程序中 CSS-in-JS 的性能注意事项
    • 通过仅在必要时加载 polyfill、使用动态导入和延迟水合来减少 Next.js 包大小。
    • JavaScript 的秘密:关于 React、性能优化和多线程的故事,一个由 7 部分组成的冗长系列,关于使用 React 改进用户界面挑战,
    • 如何测量 React 性能以及如何分析 React 应用程序。
    • 在 React 中构建移动优先的网络动画,Alex Holachek 的精彩演讲,以及幻灯片和 GitHub 存储库(感谢您的提示,Addy! )。
    • webpack-libs-optimizations 是一个很棒的 GitHub 存储库,其中包含许多有用的特定于 Webpack 的与性能相关的优化。 由伊万·阿库洛夫维护。
    • Notion 中的 React 性能改进,Ivan Akulov 编写的关于如何提高 React 性能的指南,其中包含大量有用的指针,可以使应用程序的速度提高 30% 左右。
    • React Refresh Webpack Plugin(实验性)允许热重新加载,保留组件状态,并支持钩子和函数组件。
    • 注意零包大小的 React 服务器组件,这是一种新提议的组件,不会对包大小产生影响。 该项目目前正在开发中,但非常感谢来自社区的任何反馈(Sophie Alpert 的出色解释)。
  3. 您是否对 JavaScript 块使用预测预取?
    我们可以使用启发式方法来决定何时预加载 JavaScript 块。 Guess.js 是一组工具和库,它们使用 Google Analytics 数据来确定用户最有可能从给定页面访问的下一个页面。 根据从 Google Analytics 或其他来源收集的用户导航模式,Guess.js 构建了一个机器学习模型来预测和预取每个后续​​页面所需的 JavaScript。

    因此,每个交互元素都会收到参与度的概率分数,并且基于该分数,客户端脚本决定提前预取资源。 您可以将该技术集成到您的 Next.js 应用程序、Angular 和 React,并且还有一个 Webpack 插件可以自动执行设置过程。

    显然,您可能会提示浏览器使用不需要的数据并预取不需要的页面,因此在预取请求的数量上保持相当保守是个好主意。 一个很好的用例是预取结帐中所需的验证脚本,或者在关键的号召性用语进入视口时进行推测性预取。

    需要不那么复杂的东西吗? 当出站链接出现在视口中时,DNStradamus 会对其进行 DNS 预取。 Quicklink、InstantClick 和 Instant.page 是小型库,它们在空闲时间自动预取视口中的链接,以尝试使下一页导航加载更快。 Quicklink 允许预取 React Router 路由和 Javascript; 再加上它考虑到数据,所以它不会在 2G 或Data-Saver开启时预取。 如果模式设置为使用视口预取(这是默认设置),则 Instant.page 也是如此。

    如果您想详细了解预测预取的科学,Divya Tagtachian 有一个关于预测预取的艺术的精彩演讲,涵盖了从头到尾的所有选项。

  4. 利用目标 JavaScript 引擎的优化。
    研究哪些 JavaScript 引擎在您的用户群中占主导地位,然后探索针对它们进行优化的方法。 例如,在针对 Blink 浏览器、Node.js 运行时和 Electron 中使用的 V8 进行优化时,对单体脚本使用脚本流。

    脚本流允许在下载开始后在单独的后台线程上解析asyncdefer scripts ,因此在某些情况下可将页面加载时间提高 10%。 实际上,在<head>中使用<script defer> ,这样浏览器可以及早发现资源,然后在后台线程上解析它。

    警告Opera Mini 不支持脚本延迟,因此如果您正在为印度或非洲开发, defer将被忽略,导致在脚本评估之前阻塞渲染(感谢 Jeremy!)

    您还可以挂钩到 V8 的代码缓存,通过将库从使用它们的代码中分离出来,或者反过来,将库及其使用合并到一个脚本中,将小文件组合在一起并避免内联脚本。 或者甚至可以使用 v8-compile-cache。

    一般来说,当谈到 JavaScript 时,还有一些值得牢记的做法:

    • JavaScript 的清洁代码概念,用于编写可读、可重用和可重构代码的大量模式集合。
    • 您可以使用 CompressionStream API 压缩来自 JavaScript 的数据,例如在上传数据之前压缩到 gzip(Chrome 80+)。
    • 分离的窗口内存泄漏和修复 Web 应用程序中的内存泄漏是有关如何查找和修复棘手的 JavaScript 内存泄漏的详细指南。 另外,您可以使用 DevTools 控制台中的 queryObjects(SomeConstructor)(谢谢,Mathias! )。
    • 重新导出对加载和运行时性能不利,避免它们有助于显着减小包大小。
    • 我们可以通过在options参数中设置一个标志来提高被动事件侦听器的滚动性能。 因此浏览器可以立即滚动页面,而不是在侦听器完成之后。 (通过凯斯巴斯克)。
    • 如果您有任何scrolltouch*侦听器,请将passive: true传递给 addEventListener。 这告诉浏览器你不打算在里面调用event.preventDefault() ,所以它可以优化它处理这些事件的方式。 (通过伊万·阿库洛夫)
    • 我们可以使用 isInputPending() 实现更好的 JavaScript 调度,这是一个新的 API,它试图通过网络上用户输入的中断概念来弥合加载和响应之间的差距,并允许 JavaScript 能够检查输入而不屈服于浏览器。
    • 您还可以在事件侦听器执行后自动删除它。
    • Firefox 最近发布的 Warp,是 SpiderMonkey(在 Firefox 83 中提供)、基线解释器的重要更新,还有一些可用的 JIT 优化策略。
帮助您了解时间加载和响应能力的插图
一个蓝色横幅,显示正在运行的 JS,在常规间隙中带有白线,表示我们主动检查是否有用户输入而不会产生让浏览器执行和返回的开销的时间
isInputPending() 是一个新的浏览器 API,它试图弥合加载和响应之间的差距。(大预览)
显示每个请求到不同域的链的地图插图,一直到第八方脚本
CNN.com 的请求图显示了每个请求到不同域的链,一直到第八方脚本。 资源。 (大预览)
  1. 总是更喜欢自托管第三方资产。
    同样,默认情况下自托管您的静态资产。 通常假设如果许多站点使用相同的公共 CDN 和相同版本的 JavaScript 库或 Web 字体,那么访问者将使用已经缓存在浏览器中的脚本和字体登陆我们的站点,从而大大加快他们的体验. 但是,这不太可能发生。

    出于安全原因,为了避免指纹识别,浏览器一直在实施分区缓存,该缓存早在 2013 年在 Safari 中引入,去年在 Chrome 中引入。 因此,如果两个站点指向完全相同的第三方资源 URL,则每个域都会下载一次代码,并且由于隐私问题,缓存会被“沙盒化”到该域(感谢 David Calhoun! )。 因此,使用公共 CDN 不会自动带来更好的性能。

    此外,值得注意的是资源不会像我们预期的那样长期存在于浏览器的缓存中,并且第一方资产比第三方资产更有可能留在缓存中。 因此,自托管通常更可靠、更安全,性能也更好。

  2. 限制第三方脚本的影响。
    在所有性能优化到位后,我们通常无法控制来自业务需求的第三方脚本。 第三方脚本指标不受最终用户体验的影响,因此一个单独的脚本经常会调用一长串令人讨厌的第三方脚本,从而破坏了专门的性能工作。 为了控制和减轻这些脚本带来的性能损失,仅仅通过资源提示(即dns-prefetchpreconnect )推迟它们的加载和执行以及预热连接是不够的。

    目前,所有 JavaScript 代码执行时间的 57% 都花在了第三方代码上。 移动站点访问12 个第三方域的中位数,平均有 37 个不同的请求(或每个第三方大约 3 个请求)。

    此外,这些第三方经常邀请第四方脚本加入,最终导致巨大的性能瓶颈,有时甚至会影响页面上的第八方脚本。 因此,定期审核您的依赖项和标签管理器可能会带来代价高昂的意外。

    另一个问题,正如 Yoav Weiss 在他关于第三方脚本的演讲中所解释的那样,在许多情况下,这些脚本下载的资源是动态的。 资源在页面加载之间会发生变化,因此我们不一定知道将从哪些主机下载资源以及它们将是什么资源。

    如上所示,延迟可能只是一个开始,因为第三方脚本也会从您的应用程序中窃取带宽和 CPU 时间。 我们可以更积极一点,只在我们的应用程序初始化时才加载它们。

    /* Before */ const App = () => { return <div> <script> window.dataLayer = window.dataLayer || []; function gtag(){...} gtg('js', new Date()); </script> </div> } /* After */ const App = () => { const[isRendered, setRendered] = useState(false); useEffect(() => setRendered(true)); return <div> {isRendered ? <script> window.dataLayer = window.dataLayer || []; function gtag(){...} gtg('js', new Date()); </script> : null} </div> }

    在一篇关于“减少第三方标签对网站速度的影响”的精彩文章中,Andy Davies 探讨了一种最大限度地减少第三方足迹的策略——从确定他们的成本到减少他们的影响。

    根据 Andy 的说法,标签影响网站速度的方式有两种——它们在访问者的设备上争夺网络带宽和处理时间,并且根据它们的实施方式,它们也可以延迟 HTML 解析。 因此,第一步是通过使用 WebPageTest 测试带有带有脚本的站点来确定第三方的影响。 借助 Simon Hearne 的请求图,我们还可以在页面上可视化第三方以及有关其大小、类型和触发负载的详细信息。

    最好自托管并使用单个主机名,但也使用请求映射来公开第四方调用并检测脚本何时更改。 您可以使用 Harry Roberts 的方法来审核第三方并生成类似这样的电子表格(也可以查看 Harry 的审核工作流程)。

    之后,我们可以探索现有脚本的轻量级替代方案,并慢慢用更轻量级的选项替换重复项和主要罪魁祸首。 也许一些脚本可以用它们的后备跟踪像素代替完整的标签。

    左侧示例显示 3KB 的 JavaScript 使用 lite-youtube 自定义元素,中间和右侧示例显示 +540KB 的 JavaScript 使用 lite-youtube 自定义元素
    加载带有外观的 YouTube,例如 lite-youtube-embed,它比实际的 YouTube 播放器小得多。 (图片来源)(大预览)

    如果它不可行,我们至少可以延迟加载具有外观的第三方资源,即看起来类似于实际嵌入的第三方的静态元素,但没有功能,因此对页面加载的负担要少得多。 那么,诀窍是仅在交互上加载实际嵌入

    例如,我们可以使用:

    • lite-vimeo-embed 用于 Vimeo 播放器,
    • 用于 Vimeo 播放器的 lite-vimeo,
    • lite-youtube-embed 用于 YouTube 播放器,
    • react-live-chat-loader 用于实时聊天(案例研究和另一个案例研究),
    • iframe 的惰性框架。

    标签管理器通常规模很大的原因之一是因为同时运行的许多同时进行的实验,以及许多用户细分、页面 URL、站点等,因此根据 Andy 的说法,减少它们可以减少两者下载大小和在浏览器中执行脚本所需的时间。

    然后是防闪烁片段。 Google Optimize、Visual Web Optimizer (VWO) 等第三方都一致使用它们。 这些片段通常与运行A/B 测试一起注入:为了避免在不同的测试场景之间闪烁,它们使用opacity: 0隐藏文档的body ,然后添加一个在几秒钟后调用的函数以恢复opacity . 由于大量的客户端执行成本,这通常会导致渲染的大量延迟。

    从 0.0 秒到 6.0 秒显示七个预览,显示当访问者启动导航时,内容如何以及何时被防闪烁片段隐藏
    在使用 A/B 测试时,客户经常会看到像这样的闪烁。 Anti-Flicker 片段可以防止这种情况,但它们也会降低性能。 通过安迪戴维斯。 (大预览)

    因此,跟踪触发防闪烁超时的频率并减少超时。 默认阻止页面显示最多 4 秒,这将破坏转化率。 根据 Tim Kadlec 的说法,“朋友不要让朋友做客户端 A/B 测试”。 CDN 上的服务器端 A/B 测试(例如边缘计算或边缘切片重新渲染)始终是性能更高的选项。

    如果您必须与全能的Google Tag Manager打交道,Barry Pollard 提供了一些指南来遏制 Google Tag Manager 的影响。 此外,Christian Schaefer 还探索了加载广告的策略。

    注意:一些第三方小部件隐藏在审计工具之外,因此它们可能更难以发现和测量。 要对第三方进行压力测试,请在 DevTools 的性能配置文件页面中检查自下而上的摘要,测试如果请求被阻止或超时会发生什么——对于后者,您可以使用 WebPageTest 的 Blackhole 服务器blackhole.webpagetest.org可以在您的hosts文件中指向特定域。

    那我们有什么选择呢? 考虑使用服务工作者通过超时来加速资源下载,如果资源在特定超时内没有响应,则返回一个空响应来告诉浏览器继续解析页面。 您还可以记录或阻止不成功或不满足特定条件的第三方请求。 如果可以,请从您自己的服务器而不是从供应商的服务器加载 3rd-party-script 并延迟加载它们。

    另一种选择是建立内容安全策略 (CSP)以限制第三方脚本的影响,例如禁止下载音频或视频。 最好的选择是通过<iframe>嵌入脚本,以便脚本在 iframe 的上下文中运行,因此无法访问页面的 DOM,并且不能在您的域上运行任意代码。 使用sandbox属性可以进一步限制 iframe,因此您可以禁用 iframe 可能执行的任何功能,例如阻止脚本运行、阻止警报、表单提交、插件、访问顶部导航等等。

    您还可以通过使用功能策略的浏览器内性能检查来检查第三方,这是一项相对较新的功能,可让您选择加入或退出您网站上的某些浏览器功能。 (作为旁注,它还可用于避免过大和未优化的图像、未调整大小的媒体、同步脚本等)。 目前支持基于 Blink 的浏览器。

    /* Via Tim Kadlec. https://timkadlec.com/remembers/2020-02-20-in-browser-performance-linting-with-feature-policies/ */ /* Block the use of the Geolocation API with a Feature-Policy header. */ Feature-Policy: geolocation 'none'
    /* Via Tim Kadlec. https://timkadlec.com/remembers/2020-02-20-in-browser-performance-linting-with-feature-policies/ */ /* Block the use of the Geolocation API with a Feature-Policy header. */ Feature-Policy: geolocation 'none'

    由于许多第三方脚本在 iframe 中运行,您可能需要彻底限制它们的允许范围。 沙盒 iframe 总是一个好主意,每个限制都可以通过sandbox属性上的一些allow值来解除。 几乎所有地方都支持沙盒,因此将第三方脚本限制在应允许的最低限度。

    ThirdPartyWeb.Today 网站的屏幕截图,显示了实体的脚本平均执行所需的时间
    ThirdPartyWeb.Today 按类别(分析、社交、广告、托管、标签管理器等)对所有第三方脚本进行分组,并可视化实体脚本执行所需的时间(平均)。 (大预览)

    考虑使用交叉口观察器; 这将使广告能够被 iframe ,同时仍然调度事件或从 DOM 获取他们需要的信息(例如广告可见性)。 注意新策略,例如功能策略、资源大小限制和 CPU/带宽优先级,以限制会降低浏览器速度的有害 Web 功能和脚本,例如同步脚本、同步 XHR 请求、 document.write和过时的实现。

    最后,在选择第三方服务时,请考虑查看 Patrick Hulce 的 ThirdPartyWeb.Today,该服务按类别(分析、社交、广告、托管、标签管理器等)对所有第三方脚本进行分组,并可视化实体脚本的时长执行(平均)。 显然,最大的实体对其所在页面的性能影响最差。 只需浏览页面,您就会了解您应该期待的性能足迹。

    啊,不要忘记通常的嫌疑人:我们可以使用静态社交共享按钮(例如 SSBG)和交互式地图的静态链接而不是交互式地图,而不是使用第三方小部件进行共享。

比较第一方和第三方请求百分比的图表示例:399KB 占第一方请求的 27%,1.15MB 占第三方请求的 73%
Casper.com 发布了一份详细的案例研究,说明他们如何通过自托管 Optimizely 将网站缩短 1.7 秒。 这可能是值得的。 (图片来源)(大预览)
  1. 正确设置 HTTP 缓存标头。
    缓存似乎是一件显而易见的事情,但要做到正确可能相当困难。 我们需要仔细检查expiresmax-agecache-control和其他 HTTP 缓存标头是否已正确设置。 如果没有适当的 HTTP 缓存标头,浏览器将自动将它们设置为自last-modified以来经过时间的 10%,最终导致潜在的缓存不足和过度缓存。

    通常,资源应该可以在很短的时间内(如果它们可能会更改)或无限期(如果它们是静态的)可以缓存——您可以在需要时在 URL 中更改它们的版本。 您可以将其称为 Cache-Forever 策略,其中我们可以将Cache-ControlExpires标头中继到浏览器,以仅允许资产在一年内过期。 因此,如果资产在缓存中,浏览器甚至不会发出请求。

    例外是 API 响应(例如/api/user )。 为了防止缓存,我们可以使用private, no store ,而不是max-age=0, no-store

     Cache-Control: private, no-store

    使用Cache-control: immutable以避免在用户点击重新加载按钮时重新验证长显式缓存生命周期。 对于重新加载的情况, immutable的保存了 HTTP 请求并改善了动态 HTML 的加载时间,因为它们不再与大量 304 响应竞争。

    我们想要使用immutable的典型示例是名称中带有哈希的 CSS/JavaScript 资产。 对于他们,我们可能希望尽可能长时间地缓存,并确保他们永远不会被重新验证:

    Cache-Control: max-age: 31556952, immutable

    根据 Colin Bendell 的研究, immutable将 304 重定向减少了大约 50%,即使使用了max-age ,客户端仍然会在刷新时重新验证并阻止。 Firefox、Edge 和 Safari 都支持它,而 Chrome 仍在争论这个问题。

    据 Web Almanac 称,“它的使用率已经增长到 3.5%,并且被广泛用于 Facebook 和 Google 第三方响应中。”

    从 Android Chrome 和 iOS Safari 检索数据,跨大陆缓存控制的有效性
    根据 Colin Bendell 在 Cloudinary 的研究,Cache-Control: Immutable 将 304 减少了大约 50%。 (大预览)

    你还记得旧时重新验证的好方法吗? 当我们用Cache-Control响应头指定缓存时间时(例如Cache-Control: max-age=604800 ),在max-age过期后,浏览器会重新获取请求的内容,导致页面加载速度变慢。 stale-while-revalidate可以避免这种减速; 它基本上定义了一个额外的时间窗口,在此期间缓存可以使用陈旧的资产,只要它在后台重新验证它是异步的。 因此,它“隐藏”了客户端的延迟(在网络和服务器上)。

    在 2019 年 6 月至 2019 年 7 月, Chrome 和 Firefox 推出了对 HTTP Cache-Control 标头中的stale-while-revalidate的支持,因此它应该会改善后续页面加载延迟,因为过时的资产不再处于关键路径中。 结果:重复视图的 RTT 为零。

    警惕可变标头,尤其是与 CDN 相关的标头,并注意 HTTP 表示变体,这有助于避免在新请求与先前请求略有不同(但不显着)时进行额外的往返验证(感谢 Guy 和 Mark ! )。

    此外,请仔细检查您没有发送不必要的标头(例如x-powered-bypragmax-ua-compatibleexpiresX-XSS-Protection等),并且您包含有用的安全和性能标头(例如作为Content-Security-PolicyX-Content-Type-Options等)。 最后,请记住单页应用程序中 CORS 请求的性能成本。

    注意:我们经常假设缓存的资产会被立即检索,但研究表明从缓存中检索对象可能需要数百毫秒。 事实上,根据 Simon Hearne 的说法,“有时网络可能比缓存更快,并且从缓存中检索资产的成本可能很高,因为缓存的资产(不是文件大小)和用户的设备数量很大。例如:Chrome OS 平均缓存检索从具有 5 个缓存资源的 ~50ms 到具有 25 个资源的 ~100ms 翻倍”。

    此外,我们通常假设捆绑包大小不是一个大问题,用户会下载一次,然后使用缓存版本。 同时,通过 CI/CD,我们每天多次将代码推送到生产环境,缓存每次都会失效,因此对缓存有策略性很重要。

    谈到缓存,有很多资源值得一读:

    • 平民缓存控制,与 Harry Roberts 一起深入研究缓存的所有内容。
    • Heroku 的 HTTP 缓存标头入门,
    • Jake Archibald 的缓存最佳实践,
    • Ilya Grigorik 的 HTTP 缓存入门,
    • 通过 Jeff Posnick 的 stale-while-revalidate 保持新鲜。
    • CS 可视化:Lydia Hallie 的 CORS 是关于 CORS、它如何工作以及如何理解它的出色解释者。
    • 谈到 CORS,这里是 Eric Portis 对同源策略的一些复习。
图表显示缓存检索时间(按右侧命名的不同操作系统和浏览器的缓存资产计数)(从上到下):桌面 Chrome 操作系统、平板电脑 Android 操作系统、移动 Android 操作系统、桌面 Mac =S X、桌面 Windows、桌面 Linux
我们假设浏览器缓存几乎是即时的,但数据显示从缓存中检索对象可能需要数百毫秒! 来自 Simon Hearne 关于网络何时比缓存更快的研究。 (大预览)

交付优化

  1. 我们是否使用defer来异步加载关键的 JavaScript?
    当用户请求一个页面时,浏览器获取 HTML 并构造 DOM,然后获取 CSS 并构造 CSSOM,然后通过匹配 DOM 和 CSSOM 生成渲染树。 如果需要解析任何 JavaScript,浏览器在解析之前不会开始渲染页面,从而延迟渲染。 作为开发人员,我们必须明确告诉浏览器不要等待并开始渲染页面。 对脚本执行此操作的方法是使用 HTML 中的deferasync属性。

    在实践中,事实证明最好使用defer而不是async 。 啊,又有什么区别呢? 根据 Steve Souders 的说法,一旦async脚本到达,它们就会立即执行——只要脚本准备好。 如果这种情况发生得非常快,例如当脚本处于缓存中时,它实际上会阻塞 HTML 解析器。 使用defer ,浏览器在解析 HTML 之前不会执行脚本。 因此,除非您需要在开始渲染之前执行 JavaScript,否则最好使用defer 。 此外,多个异步文件将以不确定的顺序执行。

    值得注意的是,关于asyncdefer存在一些误解。 最重要的是, async并不意味着只要脚本准备好,代码就会运行。 这意味着只要脚本准备好并且所有先前的同步工作完成,它将运行。 用 Harry Roberts 的话来说,“如果你在同步脚本之后放置一个async脚本,那么你的async脚本只会和最慢的同步脚本一样快。”

    此外,不建议同时使用asyncdefer 。 现代浏览器同时支持这两种属性,但只要同时使用这两种属性, async总是会胜出的。

    如果您想深入了解更多细节,Milica Mihajlija 编写了一份关于更快构建 DOM 的非常详细的指南,详细介绍了推测解析、异步和延迟。

  2. 使用 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属性( highlow )。 事实上,这是一种在轮播中取消图像优先级以及重新确定脚本优先级的好方法。 但是,有时我们可能需要更精细的控制。

    <!-- 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%。

    一个简短的警告:值得注意的是延迟加载应该是一个例外而不是规则。 延迟加载您真正希望人们快速看到的任何内容可能是不合理的,例如产品页面图像、英雄图像或主导航变为交互所需的脚本。

一个示例显示了 3000 像素的旧阈值和 160 KB 的下载(左),而新阈值的数量为 1250 像素,只有 90 KB 的下载(右)显示了 img 加载延迟数据节省的改进
在快速连接(例如 4G)上,Chrome 的视口距离阈值最近从 3000 像素降低到 1250 像素,而在较慢的连接(例如 3G)上,阈值从 4000 像素更改为 2500 像素。 (大预览)
带有显示 Twitter UI 的手机周围的文字插图,解释了延迟加载翻译字符串的工具改进
通过延迟加载翻译字符串,Mobile Twitter 成功地将新国际化管道的 JavaScript 执行速度提高了 80%。 (图片来源:Addy Osmani)(大预览)
  1. 逐步加载图像。
    您甚至可以通过向页面添加渐进式图像加载来将延迟加载提升到一个新的水平。 与 Facebook、Pinterest、Medium 和 Wolt 类似,您可以先加载低质量甚至模糊的图像,然后随着页面继续加载,使用 BlurHash 技术或 LQIP(低质量图像占位符)将它们替换为完整质量版本技术。

    如果这些技术是否改善了用户体验,意见会有所不同,但它肯定会缩短首次内容绘制的时间。 我们甚至可以通过使用 SQIP 来自动化它,该 SQIP 创建一个低质量版本的图像作为 SVG 占位符,或带有 CSS 线性渐变的渐变图像占位符。

    这些占位符可以嵌入到 HTML 中,因为它们自然可以很好地使用文本压缩方法进行压缩。 在他的文章中,Dean Hume 描述了如何使用 Intersection Observer 来实现这种技术。

    倒退? 如果浏览器不支持交叉点观察器,我们仍然可以延迟加载 polyfill 或立即加载图像。 甚至还有一个图书馆。

    想要更高级? 您可以跟踪图像并使用原始形状和边缘来创建轻量级 SVG 占位符,首先加载它,然后从占位符矢量图像过渡到(加载的)位图图像。

  2. 三个不同的版本展示了 Jose M. Perez 的 SVG 延迟加载技术,左侧是类似于立体主义艺术的版本,中间是像素化模糊版本,右侧是 Jose 本人的正确照片
    Jose M. Perez 的 SVG 延迟加载技术。 (大预览)
  3. 您是否延迟渲染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-leftpadding-right而不是默认的margin-left: auto;margin-right: auto; 和声明的宽度。 填充基本上允许元素溢出内容框并进入填充框,而不会离开整个盒子模型并被切断。

    另外,请记住,当最终呈现新内容时,您可能会引入一些 CLS,因此最好将contain-intrinsic-size与适当大小的占位符一起使用(谢谢,Una! )。

    Thijs Terluin 有更多关于这两个属性以及浏览器如何计算 contains contain-intrinsic-size的详细信息,Malte Ubl 展示了如何计算它,Jake 和 Surma 的简短视频解释器解释了它是如何工作的。

    如果您需要更细化,使用 CSS 包含,如果您只需要其他元素的大小、对齐或计算样式,您可以手动跳过 DOM 节点后代的布局、样式和绘制工作——或者该元素当前是画布外。

初始加载时的渲染性能对于基线(左)为 2,288 毫秒,对于具有 content-visibility:auto 的块为 13,464 毫秒(右)
在演示中,将content-visibility: auto应用于分块内容区域可在初始加载时将渲染性能提升 7 倍。 (大预览)
  1. 您是否使用decoding="async"延迟解码?
    有时内容会出现在屏幕外,但我们希望确保它在客户需要时可用——理想情况下,不阻塞关键路径中的任何内容,而是异步解码和渲染。 我们可以使用decode decoding="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 中分享了更多见解。

  2. 您是否生成并提供关键的 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.csscritical-product-page.css等)并从您的根目录提供它们,而无需内联它们。 (谢谢,菲利普!

    需要注意的是:使用 HTTP/2,关键 CSS 可以存储在单独的 CSS 文件中,并通过服务器推送交付,而不会使 HTML 膨胀。 问题是服务器推送对于跨浏览器的许多陷阱和竞争条件很麻烦。 它从来没有得到一致的支持,并且存在一些缓存问题(参见 Hooman Beheshti 演示文稿的幻灯片 114)。

    事实上,这种影响可能是负面的,并且会使网络缓冲区膨胀,从而阻止传递文档中的真实帧。 因此,Chrome 计划暂时取消对 Server Push 的支持也就不足为奇了。

  3. 尝试重新组合 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 的性能成本的更多见解。

  4. 您是否流式传输响应?
    流经常被遗忘和忽视,流提供了一个用于读取或写入异步数据块的接口,在任何给定时间,内存中可能只有其中的一个子集可用。 基本上,它们允许发出原始请求的页面在第一块数据可用时立即开始处理响应,并使用针对流式传输优化的解析器来逐步显示内容。

    我们可以从多个来源创建一个流。 例如,您可以让 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 中可用。

一张图片总结了 Android Chrome 上的保存数据使用情况以及 Cloudinary 研究在 2019 年 11 月和 2020 年 4 月发现的平均 img 点击或会话
根据 Cloudinary 的研究,全球 18% 的 Android Chrome 用户启用了精简模式(又名 Save-Data)。 (大预览)
  1. 考虑使您的组件具有连接意识。
    数据可能很昂贵,并且随着负载的增加,我们需要尊重在访问我们的网站或应用程序时选择节省数据的用户。 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使用RTTdownlinkeffectiveType值(以及其他一些值)来提供用户可以处理的连接和数据的表示。

    在这种情况下,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。
  2. 考虑让您的组件具有设备内存感知能力。
    但是,网络连接只为我们提供了一种关于用户上下文的视角。 更进一步,您还可以使用设备内存 API 根据可用设备内存动态调整资源。 navigator.deviceMemory返回设备有多少 RAM(以 GB 为单位),向下舍入到最接近的 2 次幂。 该 API 还具有客户端提示标头Device-Memory ,它报告相同的值。

    奖励Umar Hansa 展示了如何通过动态导入推迟昂贵的脚本,以根据设备内存、网络连接和硬件并发性来改变体验。

从 Chrome 46 及更高版本开始,显示 Blink 中不同资源的优先级细分
从 Chrome 46 及更高版本开始,显示 Blink 中不同资源的优先级细分。 (图片来源:Addy Osmani)(大预览)
  1. 预热连接以加快交付速度。
    使用资源提示来节省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)中启用“优先级”列。

    这些天的大部分时间,我们至少会使用preconnectdns-prefetch ,并且我们会谨慎使用prefetchpreloadprerender 。 请注意,即使使用preconnectdns-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查询规则有选择地下载资源,如上所示。

    此外,我们可以使用imagesrcsetimagesizes属性更快地预加载后期发现的英雄图像,或者通过 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)很有用。

    一个使用汤姆汉克斯主演的灰狗电影封面的例子,显示预加载的图像加载得更早,因为不需要等待 JavaScript 来发现
    尽早预加载重要图像; 无需等待 JavaScript 来发现它们。 (图片来源:Addy Osmani 的“更快地预加载后期发现的英雄图像”)(大预览)

    只有在浏览器从服务器接收到 HTML 并且前瞻解析器找到了preload标签后, preload标签才能启动预加载。 通过 HTTP 标头预加载可能会更快一些,因为我们不需要等待浏览器解析 HTML 来启动请求(尽管有争议)。

    Early Hints 将进一步提供帮助,甚至可以在发送 HTML 的响应标头之前启动预加载(在 Chromium、Firefox 的路线图上)。 另外,优先级提示将帮助我们指示脚本的加载优先级。

    注意:如果您使用preload as必须定义或不加载任何内容,加上没有crossorigin属性的预加载字体将双重获取。 如果您使用的是prefetch ,请注意 Firefox 中的Age标头问题。

显示在给定时间段内(以毫秒为单位)计数从 0 到 150 的第一个内容绘制(按服务器工作人员状态)的图表
使用 service worker,我们可以只请求最少的数据,然后将这些数据转换为完整的 HTML 文档以改进 FCP。 (通过菲尔沃尔顿)(大预览)
  1. 使用服务工作者进行缓存和网络回退。
    网络上的任何性能优化都不会比用户机器上本地存储的缓存更快(但也有例外)。 如果您的网站通过 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 应用程序而构建的服务工作者库。
  2. 您是否在 CDN/Edge 上运行服务器工作者,例如用于 A/B 测试?
    在这一点上,我们已经习惯了在客户端运行服务工作者,但是通过在服务器上实现它们的 CDN,我们也可以使用它们来调整边缘的性能。

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

显示 2016 年 1 月至 2020 年 7 月期间桌面和移动设备上的服务工作者安装时间序列以及页面百分比的图表
Service Worker 安装的时间序列。 根据 Web Almanac 的数据,只有 0.87% 的桌面页面注册了 Service Worker。 (大预览)
  1. 优化渲染性能。
    每当应用程序运行缓慢时,都会立即引起注意。 因此,我们需要确保在滚动页面或动画元素时没有延迟,并且您始终保持每秒 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 动画的文章。

    高性能动画,包括位置、缩放、旋转和不透明度
    浏览器可以廉价地为变换和不透明度设置动画。 CSS 触发器对于检查 CSS 是否触发重新布局或重排很有用。 (图片来源:Addy Osmani)(大预览)

    注意:对 GPU 合成层的更改是最便宜的,因此如果您可以通过opacitytransform仅触发合成来摆脱困境,那么您将走在正确的轨道上。 Anna Migas 在她关于调试 UI 渲染性能的演讲中也提供了很多实用的建议。 要了解如何在 DevTools 中调试绘制性能,请查看 Umar 的绘制性能审核视频。

  2. 您是否针对感知性能进行了优化?
    虽然组件出现在页面上的顺序,以及我们如何为浏览器提供资源的策略很重要,但我们也不应该低估感知性能的作用。 这个概念涉及等待的心理方面,基本上是让客户在其他事情发生时保持忙碌或参与。 这就是感知管理、抢先开始、提前完成和容忍管理发挥作用的地方。

    这是什么意思呢? 在加载资产时,我们可以尝试始终领先于客户一步,因此在后台发生很多事情的同时,体验感觉很快。 为了保持客户的参与度,我们可以测试骨架屏幕(实现演示)而不是加载指示器,添加过渡/动画,并且基本上在没有什么可以优化的情况下欺骗用户体验。

    在他们关于 UI 骨架艺术的案例研究中,Kumar McMillan 分享了一些关于如何模拟动态列表、文本和最终屏幕的想法和技术,以及如何使用 React 考虑骨架思维

    但请注意:在部署之前应该测试骨架屏幕,因为一些测试表明骨架屏幕在所有指标上都表现最差。

  3. 您是否防止布局变化和重绘?
    在感知性能领域,可能更具破坏性的体验之一是布局转换重排,这是由重新缩放的图像和视频、网络字体、注入的广告或用实际内容填充组件的后期发现的脚本引起的。 结果,客户可能开始阅读一篇文章,只是被阅读区域上方的布局跳转打断。 这种体验通常是突然的并且非常令人迷惑:这可能是加载需要重新考虑的优先级的情况。

    社区已经开发了一些技术和解决方法来避免回流。 一般来说,避免在现有内容之上插入新内容是个好主意,除非它是为了响应用户交互而发生的。 始终在图像上设置宽度和高度属性,因此现代浏览器默认分配框并保留空间(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

  1. 是否启用了 OCSP 装订?
    通过在您的服务器上启用 OCSP 装订,您可以加快 TLS 握手。 在线证书状态协议 (OCSP) 是作为证书撤销列表 (CRL) 协议的替代方案而创建的。 这两种协议都用于检查 SSL 证书是否已被吊销。

    但是,OCSP 协议不需要浏览器花时间下载然后搜索证书信息列表,因此减少了握手所需的时间。

  2. 您是否减少了 SSL 证书吊销的影响?
    在他关于“EV 证书的性能成本”的文章中,Simon Hearne 对常见证书进行了很好的概述,以及证书的选择可能对整体性能产生的影响。

    正如 Simon 所写,在 HTTPS 的世界中,有几种类型的证书验证级别用于保护流量:

    • 域验证(DV) 验证证书请求者是否拥有该域,
    • 组织验证(OV) 验证组织是否拥有该域,
    • 扩展验证(EV) 通过严格的验证来验证组织是否拥有该域。

    需要注意的是,所有这些证书在技术方面都是相同的。 它们仅在这些证书中提供的信息和属性上有所不同。

    EV 证书既昂贵又耗时,因为它们需要人工审核证书并确保其有效性。 另一方面,DV 证书通常是免费提供的——例如由 Let's Encrypt——一个开放的、自动化的证书颁发机构,它很好地集成到许多托管服务提供商和 CDN 中。 事实上,在撰写本文时,它为超过 2.25 亿个网站 (PDF) 提供支持,尽管它仅占页面的 2.69%(在 Firefox 中打开)。

    那么有什么问题呢? 问题是EV 证书不完全支持上面提到的 OCSP 装订。 虽然装订允许服务器与证书颁发机构检查证书是否已被吊销,然后将此信息添加(“装订”)到证书中,而无需装订客户端必须完成所有工作,从而导致在 TLS 协商期间产生不必要的请求. 在连接不佳的情况下,这可能会导致显着的性能成本(1000 毫秒以上)。

    EV 证书对于 Web 性能来说并不是一个很好的选择,而且它们对性能的影响要比 DV 证书大得多。 为获得最佳 Web 性能,请始终提供 OCSP 装订的 DV 证书。 它们也比 EV 证书便宜得多,而且获得的麻烦更少。 好吧,至少在 CRLite 可用之前。

    显示在普通、压缩或两者的情况下沿站点 UDP 数据报的握手次数的图表
    压缩很重要:40-43% 的未压缩证书链太大而无法容纳 3 个 UDP 数据报的单个 QUIC 飞行。 (图片来源:)快速)(大预览)

    注意:使用 QUIC/HTTP/3,值得注意的是 TLS 证书链是一个可变大小的内容,它在 QUIC 握手中支配字节数。 大小在几百字节和超过 10 KB 之间变化。

    因此,在 QUIC/HTTP/3 上保持 TLS 证书很小很重要,因为大型证书会导致多次握手。 此外,我们需要确保证书被压缩,否则证书链将太大而无法容纳在单个 QUIC 飞行中。

    您可以在以下位置找到更多详细信息和指向问题和解决方案的方法:

    • EV 证书使 Web 变得缓慢且不可靠 作者 Aaron Peters,
    • SSL 证书吊销对 Web 性能的影响 by Matt Hobbs,
    • Simon Hearne 的 EV 证书的性能成本,
    • QUIC 握手是否需要快速压缩? 通过帕特里克麦克马纳斯。
  3. 您采用 IPv6 了吗?
    因为我们的 IPv4 空间已经用完,而且主要移动网络正在迅速采用 IPv6(美国几乎达到了 50% 的 IPv6 采用阈值),所以最好将您的 DNS 更新为 IPv6,以防万一。 只需确保跨网络提供双栈支持——它允许 IPv6 和 IPv4 同时运行。 毕竟,IPv6 不是向后兼容的。 此外,研究表明,由于邻居发现 (NDP) 和路由优化,IPv6 使这些网站的速度提高了 10% 到 15%。
  4. 确保所有资产都通过 HTTP/2(或 HTTP/3)运行。
    随着谷歌在过去几年中推动更安全的 HTTPS 网络,切换到 HTTP/2 环境绝对是一项不错的投资。 事实上,根据 Web Almanac,64% 的请求已经在 HTTP/2 上运行。

    重要的是要了解 HTTP/2 并不完美并且存在优先级问题,但它得到了很好的支持; 而且,在大多数情况下,你会更好。

    提醒一句:HTTP/2 Server Push 正在从 Chrome 中删除,因此如果您的实现依赖于 Server Push,您可能需要重新访问它。 相反,我们可能正在研究 Early Hints,它已经作为实验集成在 Fastly 中。

    如果您仍然在 HTTP 上运行,最耗时的任务将是首先迁移到 HTTPS,然后调整您的构建过程以适应 HTTP/2 多路复用和并行化。 将 HTTP/2 引入 Gov.uk 是一个非常棒的案例研究,可以在此过程中通过 CORS、SRI 和 WPT 找到方法。 对于本文的其余部分,我们假设您正在切换到或已经切换到 HTTP/2。

图表显示了从 2016 年 1 月 2 日到 2020 年 10 月 1 日桌面和移动设备中 HTTP/2 请求的时间序列
根据 Web Almanac 的数据,到 2020 年底,64% 的请求都通过 HTTP/2 提供服务——距离其正式标准化仅 4 年。 (图片来源:Web Almanac)(大预览)
  1. 正确部署 HTTP/2。
    同样,通过 HTTP/2 提供资产可以受益于对迄今为止提供资产的方式进行部分改革。 您需要在打包模块和并行加载许多小模块之间找到一个很好的平衡点。 归根结底,最好的请求仍然是没有请求,但是,目标是在快速首次交付资产和缓存之间找到一个很好的平衡点。

    一方面,您可能希望避免完全连接资产,而不是将整个界面分解为许多小模块,将它们作为构建过程的一部分进行压缩并并行加载。 一个文件的更改不需要重新下载整个样式表或 JavaScript。 它还最大限度地减少了解析时间,并使各个页面的有效负载保持在较低水平。

    另一方面,包装仍然很重要。 通过使用许多小脚本,整体压缩会受到影响,并且从缓存中检索对象的成本会增加。 大包的压缩将受益于字典重用,而单独的小包则不会。 有标准的工作来解决这个问题,但现在还很遥远。 其次,浏览器尚未针对此类工作流程进行优化。 例如,Chrome 会触发与资源数量成线性关系的进程间通信 (IPC),因此包含数百个资源将产生浏览器运行时成本。

    使用渐进式 CSS 加载的 HTML 代码
    要使用 HTTP/2 获得最佳结果,请考虑逐步加载 CSS,正如 Chrome 的 Jake Archibald 所建议的那样。

    不过,您可以尝试逐步加载 CSS。 事实上,in-body CSS 不再阻碍 Chrome 的渲染。 但是有一些优先级问题,所以它不是那么简单,但值得尝试。

    您可以使用 HTTP/2 连接合并,它允许您在受益于 HTTP/2 的同时使用域分片,但在实践中实现这一点很困难,而且通常不被认为是好的做法。 此外,HTTP/2 和子资源完整性并不总是有效。

    该怎么办? 好吧,如果你在 HTTP/2 上运行,发送大约6-10 个包似乎是一个不错的折衷方案(对于旧版浏览器来说还不错)。 试验和测量以找到适合您网站的平衡点。

  2. 我们是否通过单个 HTTP/2 连接发送所有资产?
    HTTP/2 的主要优点之一是它允许我们通过单个连接将资产发送到线路上。 然而,有时我们可能做错了什么——例如有一个 CORS 问题,或者错误配置了crossorigin属性,所以浏览器将被迫打开一个新连接。

    要检查所有请求是否使用单个 HTTP/2 连接,或者是否配置错误,请启用 DevTools → Network 中的“Connection ID”列。 例如,在这里,所有请求共享相同的连接 (286) — 除了 manifest.json,它打开一个单独的连接 (451)。

在 Chrome 浏览器中打开的 DevTools 的屏幕截图
所有请求共享相同的 HTTP/2 连接 (286) — 除了 manifest.json,它打开一个单独的 (451)。 通过iamakulov。 (大预览)
  1. 您的服务器和 CDN 是否支持 HTTP/2?
    不同的服务器和 CDN(仍然)以不同的方式支持 HTTP/2。 使用 CDN 比较来检查您的选项,或快速查看您的服务器的性能以及您期望支持的功能。

    请参阅 Pat Meenan 关于 HTTP/2 优先级(视频)的令人难以置信的研究,并测试服务器对 HTTP/2 优先级的支持。 根据 Pat 的说法,建议启用 BBR 拥塞控制并将tcp_notsent_lowat设置为 16KB,以便 HTTP/2 优先级在 Linux 4.9 内核及更高版本上可靠地工作(感谢 Yoav! )。 Andy Davies 对跨浏览器、CDN 和云托管服务的 HTTP/2 优先级进行了类似的研究。

    使用它时,请仔细检查您的内核是否支持 TCP BBR,并在可能的情况下启用它。 它目前用于 Google Cloud Platform、Amazon Cloudfront、Linux(例如 Ubuntu)。

  2. 是否正在使用 HPACK 压缩?
    如果您使用的是 HTTP/2,请仔细检查您的服务器是否为 HTTP 响应标头实施 HPACK 压缩,以减少不必要的开销。 一些 HTTP/2 服务器可能不完全支持该规范,HPACK 就是一个例子。 H2spec 是一个很棒的(如果技术上非常详细)工具来检查它。 HPACK 的压缩算法令人印象深刻,而且它确实有效。
  3. 确保服务器上的安全性是防弹的。
    HTTP/2 的所有浏览器实现都通过 TLS 运行,因此您可能希望避免出现安全警告或页面上的某些元素不起作用。 仔细检查您的安全标头是否设置正确,消除已知漏洞,并检查您的 HTTPS 设置。

    此外,确保所有外部插件和跟踪脚本都是通过 HTTPS 加载的,跨站点脚本是不可能的,并且 HTTP 严格传输安全标头和内容安全策略标头都已正确设置。

  4. 您的服务器和 CDN 是否支持 HTTP/3?
    虽然 HTTP/2 为 Web 带来了许多显着的性能改进,但它也留下了相当多的改进空间——尤其是 TCP 中的线头阻塞,这在具有严重数据包丢失的慢速网络上很明显。 HTTP/3 正在彻底解决这些问题(文章)。

    为了解决 HTTP/2 问题,IETF 与 Google、Akamai 和其他公司一起,一直在研究一种新协议,该协议最近被标准化为 HTTP/3。

    Robin Marx 已经很好地解释了 HTTP/3,下面的解释是基于他的解释。 就其核心而言,HTTP/3 在功能方面与 HTTP/2 非常相似,但在底层它的工作方式却大不相同。 HTTP/3 提供了许多改进:更快的握手、更好的加密、更可靠的独立流、更好的加密和流控制。 一个显着的区别是 HTTP/3 使用 QUIC 作为传输层,QUIC 数据包封装在 UDP 图而不是 TCP 之上。

    QUIC 将 TLS 1.3 完全集成到协议中,而在 TCP 中它是分层的。 在典型的 TCP 堆栈中,我们有一些往返时间的开销,因为 TCP 和 TLS 需要自己进行单独的握手,但是使用 QUIC 可以将它们组合在一起并在一次往返中完成。 由于 TLS 1.3 允许我们为后续连接设置加密密钥,从第二个连接开始,我们已经可以在第一次往返中发送和接收应用层数据,称为“0-RTT”。

    此外,HTTP/2 的标头压缩算法及其优先级系统也被完全重写。 此外,QUIC 通过每个 QUIC 数据包标头中的连接 ID 支持从 Wi-Fi 到蜂窝网络的连接迁移。 大多数实现都是在用户空间完成的,而不是内核空间(就像 TCP 一样),所以我们应该期待协议在未来不断发展。

    这一切都会有很大的不同吗? 可能是的,尤其是对移动设备的加载时间有影响,而且对我们如何向最终用户提供资产也有影响。 在 HTTP/2 中,多个请求共享一个连接,而在 HTTP/3 中,请求也共享一个连接但独立流式传输,因此丢弃的数据包不再影响所有请求,只会影响一个流。

    这意味着,虽然使用一个大型 JavaScript 包,当一个流暂停时资产的处理速度会减慢,但当多个文件并行流 (HTTP/3) 时,影响将不那么显着。 所以包装仍然很重要

    HTTP/3 仍在进行中。 Chrome、Firefox 和 Safari 已经有了实现。 一些 CDN 已经支持 QUIC 和 HTTP/3。 2020 年底,Chrome 开始部署 HTTP/3 和 IETF QUIC,实际上所有 Google 服务(Google Analytics、YouTube 等)都已经在 HTTP/3 上运行。 LiteSpeed Web Server 支持 HTTP/3,但 Apache、nginx 或 IIS 都不支持它,但它很可能在 2021 年迅速改变。

    底线:如果您可以选择在服务器和 CDN 上使用 HTTP/3,那么这样做可能是一个非常好的主意。 主要好处将来自同时获取多个对象,尤其是在高延迟连接上。 我们还不确定,因为在该领域没有太多研究,但初步结果非常有希望。

    如果您想更深入地了解协议的细节和优势,这里有一些很好的起点来检查:

    • HTTP/3 Explained,一个记录 HTTP/3 和 QUIC 协议的协作努力。 有多种语言版本,也有 PDF 格式。
    • Daniel Stenberg 使用 HTTP/3 提升 Web 性能。
    • 与 Robin Marx 合作的 QUIC 学术指南介绍了 QUIC 和 HTTP/3 协议的基本概念,解释了 HTTP/3 如何处理线头阻塞和连接迁移,以及 HTTP/3 如何被设计为常青树(感谢Simon !)。
    • 您可以在 HTTP3Check.net 上检查您的服务器是否在 HTTP/3 上运行。

测试和监控

  1. 您是否优化了审计工作流程?
    这听起来可能没什么大不了的,但是触手可及的正确设置可能会为您节省大量测试时间。 考虑使用 Tim Kadlec 的 Alfred Workflow for WebPageTest 向 WebPageTest 的公共实例提交测试。 事实上,WebPageTest 有许多晦涩难懂的功能,因此请花时间学习如何阅读 WebPageTest 瀑布视图图表以及如何阅读 WebPageTest 连接视图图表以更快地诊断和解决性能问题。

    您还可以从 Google 电子表格驱动 WebPageTest,并使用 Lighthouse CI 将可访问性、性能和 SEO 分数整合到您的 Travis 设置中,或者直接整合到 Webpack 中。

    看看最近发布的 AutoWebPerf,这是一个模块化工具,可以自动收集来自多个来源的性能数据。 例如,我们可以在您的关键页面上设置每日测试,以捕获来自 CrUX API 的现场数据和来自 PageSpeed Insights 的 Lighthouse 报告的实验室数据。

    如果你需要快速调试一些东西,但你的构建过程似乎非常慢,请记住“空白删除和符号修改占大多数 JavaScript 缩小代码大小减少的 95%——而不是复杂的代码转换。你可以只需禁用压缩即可将 Uglify 构建速度提高 3 到 4 倍。”

GitHub 的 Pull Request 通知的屏幕截图,说明需要审核并且在成功解决检查之前阻止合并
使用 Lighthouse CI 将可访问性、性能和 SEO 分数集成到 Travis 设置中,将突出新功能对所有贡献的开发人员的性能影响。 (图片来源)(大预览)
  1. 您是否在代理浏览器和旧版浏览器中进行了测试?
    在 Chrome 和 Firefox 中进行测试是不够的。 查看您的网站在代理浏览器和旧版浏览器中的工作方式。 例如,UC 浏览器和 Opera Mini 在亚洲拥有重要的市场份额(在亚洲高达 35%)。 测量您感兴趣的国家/地区的平均互联网速度,以避免未来出现重大意外。 使用网络限制进行测试,并模拟高 DPI 设备。 BrowserStack 非常适合在远程真实设备上进行测试,并且还可以在您的办公室中至少使用一些真实设备来补充它——这是值得的。
  1. 您是否测试过 404 页面的性能?
    通常,当涉及到 404 页时,我们不会三思而后行。 毕竟,当客户端请求服务器上不存在的页面时,服务器将响应 404 状态代码和相关的 404 页面。 它没有那么多,不是吗?

    404 响应的一个重要方面是发送到浏览器的实际响应正文大小。 根据 Matt Hobbs 对 404 页面的研究,绝大多数 404 响应来自丢失的网站图标、WordPress 上传请求、损坏的 JavaScript 请求、清单文件以及 CSS 和字体文件。 每次客户请求不存在的资产时,他们都会收到 404 响应——而且该响应通常是巨大的。

    确保检查和优化 404 页面的缓存策略。 我们的目标是仅在浏览器需要 HTML 响应时才向浏览器提供 HTML,并为所有其他响应返回一个小的错误负载。 根据 Matt 的说法,“如果我们在源之前放置一个 CDN,我们就有机会在 CDN 上缓存 404 页面响应。这很有用,因为没有它,点击 404 页面可能会被用作 DoS 攻击向量,通过强制源服务器响应每个 404 请求,而不是让 CDN 以缓存版本响应。”

    404 错误不仅会损害您的性能,而且还会影响流量,因此最好在您的 Lighthouse 测试套件中包含 404 错误页面,并随着时间的推移跟踪其分数。

  2. 您是否测试过 GDPR 同意提示的性能?
    在 GDPR 和 CCPA 时代,依靠第三方为欧盟客户提供选择加入或退出跟踪的选项已变得很普遍。 但是,与任何其他第三方脚本一样,它们的性能可能会对整个性能工作产生相当大的破坏性影响。

    当然,实际同意可能会改变脚本对整体性能的影响,因此,正如 Boris Schapira 所指出的,我们可能想要研究一些不同的 Web 性能配置文件:

    • 完全拒绝同意,
    • 同意被部分拒绝,
    • 完全同意。
    • 用户尚未对同意提示采取行动(或提示被内容阻止程序阻止),

    通常 cookie 同意提示不应该对 CLS 产生影响,但有时会产生影响,因此请考虑使用免费和开源选项 Osano 或 cookie-consent-box。

    一般来说,值得研究弹出窗口的性能,因为您需要确定鼠标事件的水平或垂直偏移量,并正确定位弹出窗口相对于锚点的位置。 Noam Rosenthal 在文章 Web 性能案例研究:维基百科页面预览(也可作为视频和会议纪要)中分享了 Wikimedia 团队的学习成果。

  3. 您是否保留了性能诊断 CSS?
    虽然我们可以包括各种检查以确保部署非性能代码,但通常快速了解一些可以轻松解决的容易实现的成果是有用的。 为此,我们可以使用 Tim Kadlec 出色的性能诊断 CSS(灵感来自 Harry Roberts 的片段,该片段突出显示延迟加载的图像、未调整大小的图像、旧格式图像和同步脚本。

    例如,您可能希望确保首屏上方的图像不会被延迟加载。 您可以根据需要自定义片段,例如突出显示未使用的网络字体,或​​检测图标字体。 一个很棒的小工具,可以确保在调试过程中可以看到错误,或者只是快速审核当前项目。

    /* Performance Diagnostics CSS */ /* via Harry Roberts. https://twitter.com/csswizardry/status/1346477682544951296 */ img[loading=lazy] { outline: 10px solid red; }
  1. 您是否测试过对可访问性的影响?
    当浏览器开始加载页面时,它会构建一个 DOM,如果有像屏幕阅读器这样的辅助技术在运行,它还会创建一个可访问性树。 然后,屏幕阅读器必须查询可访问性树以检索信息并将其提供给用户——有时是默认的,有时是按需提供的。 有时这需要时间。

    在谈论快速交互时间时,通常我们指的是用户可以通过单击或点击链接和按钮与页面交互的速度指标。 屏幕阅读器的上下文略有不同。 在这种情况下,快速交互时间意味着屏幕阅读器可以在给定页面上宣布导航并且屏幕阅读器用户可以实际敲击键盘进行交互之前经过了多少时间

    Leonie Watson 就可访问性性能发表了令人大开眼界的演讲,特别是缓慢加载对屏幕阅读器公告延迟的影响。 屏幕阅读器习惯于快节奏的公告和快速导航,因此可能比有视力的用户更没有耐心。

    使用 JavaScript 进行的大页面和 DOM 操作将导致屏幕阅读器公告延迟。 几乎每个平台(Jaws、NVDA、Voiceover、Narrator、Orca)上都有一个相当未开发的领域,可以使用一些注意力和测试作为屏幕阅读器。

  2. 是否设置了持续监控?
    拥有 WebPagetest 的私有实例总是有利于快速和无限制的测试。 但是,具有自动警报的持续监控工具(如 Sitespeed、Calibre 和 SpeedCurve)可以让您更详细地了解您的表现。 设置您自己的用户计时标记来衡量和监控特定于业务的指标。 此外,考虑添加自动性能回归警报以监控随时间的变化。

    研究使用 RUM 解决方案来监控性能随时间的变化。 对于自动化的类似单元测试的负载测试工具,您可以使用 k6 及其脚本 API。 另外,请查看 SpeedTracker、Lighthouse 和 Calibre。

快速获胜

这个列表非常全面,完成所有优化可能需要相当长的时间。 那么,如果你只有 1 小时的时间来获得显着的改进,你会怎么做? 让我们把它归结为17 种容易获得的果实。 显然,在开始之前和完成后,测量结果,包括最大内容绘制和 3G 和电缆连接上的交互时间。

  1. 衡量现实世界的体验并设定适当的目标。 目标是比你最快的竞争对手至少快 20%。 保持在最大内容绘制 < 2.5 秒内,首次输入延迟 < 100 毫秒,在慢速 3G 上交互时间 < 5 秒,重复访问,TTI < 2 秒。 至少优化首次内容绘制和交互时间。
  2. 使用 Squoosh、mozjpeg、guetzli、pingo 和 SVGOMG 优化图像,并使用图像 CDN 提供 AVIF/WebP。
  3. 为您的主模板准备关键的 CSS,并将它们内联在每个模板的<head>中。 对于 CSS/JS,在最大的关键文件大小预算内运行。 170KB gzipped(0.7MB 解压)。
  4. 修剪、优化、延迟和延迟加载脚本。 投资捆绑器的配置以消除冗余并检查轻量级替代方案。
  5. 始终自托管您的静态资产,并且始终更喜欢自托管第三方资产。 限制第三方脚本的影响。 使用外观,在交互中加载小部件并注意防闪烁片段。
  6. 选择框架时要有选择性。 对于单页面应用程序,识别关键页面并静态提供它们,或者至少预渲染它们,并在组件级别使用渐进式水合并在交互时导入模块。
  7. 单独的客户端渲染对于性能来说并不是一个好的选择。 如果您的页面没有太大变化,请预渲染,如果可以,请推迟启动框架。 如果可能,请使用流式服务器端渲染。
  8. 仅向具有<script type="module">和 module/nomodule 模式的旧版浏览器提供旧版代码。
  9. 尝试重新组合 CSS 规则并测试 in-body CSS。
  10. 添加资源提示以通过更快dns-lookuppreconnectprefetchpreloadprerender加快交付。
  11. 子集网络字体并异步加载它们,并利用 CSS 中的font-display进行快速首次渲染。
  12. 检查 HTTP 缓存标头和安全标头是否设置正确。
  13. 在服务器上启用 Brotli 压缩。 (如果这不可能,至少确保启用 Gzip 压缩。)
  14. 只要您的服务器在 Linux 内核版本 4.9+ 上运行,就启用 TCP BBR 拥塞。
  15. 如果可能,启用 OCSP 装订和 IPv6。 始终提供 OCSP 装订的 DV 证书。
  16. 为 HTTP/2 启用 HPACK 压缩,如果可用,请移至 HTTP/3。
  17. 在 Service Worker 缓存中缓存字体、样式、JavaScript 和图像等资产。

下载清单(PDF,Apple Pages)

记住这个清单,你应该为任何类型的前端性能项目做好准备。 随意下载清单的可打印 PDF 以及可编辑的 Apple Pages 文档,以根据您的需要自定义清单:

  • 下载清单 PDF (PDF, 166 KB)
  • 在 Apple Pages (.pages, 275 KB) 中下载清单
  • 下载 MS Word 中的清单 (.docx, 151 KB)

如果您需要替代方案,您还可以查看 Dan Rublic 的前端清单、Jon Yablonski 的“Designer's Web Performance Checklist”和 FrontendChecklist。

我们走吧!

一些优化可能超出了您的工作或预算范围,或者考虑到您必须处理的遗留代码可能只是过度杀伤力。 没关系! 将此清单用作一般(希望是全面的)指南,并创建您自己的适用于您的上下文的问题列表。 但最重要的是,在优化之前测试和测量您自己的项目以识别问题。 祝大家2021年业绩好!


非常感谢 Guy Podjarny、Yoav Weiss、Addy Osmani、Artem Denysov、Denys Mishunov、Ilya Pukhalski、Jeremy Wagner、Colin Bendell、Mark Zeman、Patrick Meenan、Leonardo Losoviz、Andy Davies、Rachel Andrew、Anselm Hannemann、Barry Pollard、Patrick哈曼、吉迪恩·派泽、安迪·戴维斯、玛丽亚·普罗斯维尼娜、蒂姆·卡德莱克、雷伊·班戈、马蒂亚斯·奥特、彼得·鲍耶、菲尔·沃尔顿、玛丽安娜·佩拉尔塔、佩皮恩·森德斯、马克·诺丁汉、让·皮埃尔·文森特、菲利普·泰利斯、瑞恩·汤森、英格丽·褒曼、穆罕默德·侯赛因SH、Jacob Groß、Tim Swalling、Bob Visser、Kev Adamson、Adir Amsalem、Aleksey Kulikov 和 Rodney Rehm 对本文的审阅,以及我们出色的社区分享了从性能优化工作中获得的技术和经验教训,供大家使用. 你真的很牛逼!