2019 前端性能优化年度总结

2019 - 02 - 13

Posted by Vitaly Friedman

Translate by 掘金翻译计划

起步:计划与指标

对于持续跟踪性能,”微优化”(micro-optimization)是个不错的主意,但是在脑子里有个明晰的目标也是很必要的 — 量化的目标会影响过程中采取的所有决策。有许多不同的模型可以参考,以下讨论的都基于我个人主观偏好,请根据个人情况自行调整。

1. 建立性能评估规范

在很多团队里面,前端开发者都确切地知道哪有最有可能出现问题,以及应该使用何种模式来修正这些问题。然而,由于性能评估文化的缺失,每个决定都会成为部门间的战场,使团队分裂成孤岛。要想获得业务利益相关者的支持,你需要通过具体案例来说明:页面速度会如何影响业务指标和他们所关心的 KPI

没有开发、设计与业务、市场团队的通力合作,性能优化是走不远的。研究用户抱怨的常见问题,再看看如何通过性能优化来缓解这些问题。

同时在移动和桌面设备上运行性能基准测试,由公司真实数据得到定制化的案例研究(case study)。除此以外,你还可以参考 WPO Stats 上展示的性能优化案例研究及其实验数据来提升自己对性能优化的敏感性,了解为什么性能表现如此重要,它对用户体验和业务指标会产生哪些影响。光是明白性能表现很重要还不够,你还得设立量化的、可追溯的目标,时刻关注它们。

那么到底该怎么做呢?在 Allison McKnight 名为 Building Performance for the Long Term 的演讲中,她详细地分享了自己如何在 Etsy 建立性能评估文化的案例

Brad Frost and Jonathan Fielding’s Performance Budget Calculator

Brad Frost 的 Performance budget builder 和 Jonathan Fielding 的 Performance Budget Calculator 可以帮助你建立性能预算并将其可视化表示出来。(预览

2. 目标:比你最快的竞争对手快至少 20%

根据一项心理学研究,如果你希望你的用户感觉到你们的网站用起来比竞争对手快,那么你需要比他们快至少 20%。研究你的主要对手,收集他们的网站在移动和桌面设备上的性能指标,确定超越他们的最低要求。为了得到准确的结果和目标,首先去研究你们产品的用户行为,之后模仿 90% 用户的行为来进行测试。

为了更好地了解你的对手的性能表现,你可以使用 Chrome UX ReportCrUX,一组现成的 RUM 数据集,Ilya Grigorik 的视频介绍),Speed Scorecard(可同时估算性能优化将如何影响收入),真实用户体验测试比较(Real User Experience Test Comparison)或者 SiteSpeed CI(基于集成测试)。

注意:如果你使用 Page Speed Insights(是的,它还没被抛弃),你可以得到指定页面详细的 CrUX 性能数据,而不是只有一些粗略的综合数据。在为具体页面(如”首页”、”产品列表页面”)设立性能目标时,这些数据会非常有用。另外,如果你正在使用 CI 来监测性能预算,当使用 CrUX 来确立目标时,你需要确保测试环境与 CrUX 一致。

收集数据,建立一个表格,削减掉 20%,以此建立你的目标性能预算。那么现在你有了量化的对照组样本。事情正逐步走向正轨,只要你时刻把这份预算记在心里,并且每次都交付尽可能少的代码以缩短可交互时间。

需要些资料来上手?

一旦确立好合适的性能预算,你就可以借助 Webpack Performance Hints and BundlesizeLightouse CI, PWMetricsSitespeed CI 把它们整合进打包流程中,在请求合并时强制检测性能预算,并在 PR 备注中注明得分记录。如果你需要个性化定制,你可以使用 webpagetest-charts-api,它提供了一系列可以从 WebPagetest 的结果生成图表的 API。

举个例子,正如 Pinterest 一样,你可以创建一个自定义的 eslint 规则,禁止导入重依赖(dependency-heavy)的文件和目录,从而避免打包文件变得臃肿。设定一个团队内共享的”安全”依赖包列表。

除了性能预算外,仔细考虑那些对你们业务价值最大的关键用户操作。规定并讨论可接受的关键操作响应时间阈值,并就”UX 就绪”耗时评分在团队内达成共识。大多数情况下,用户的操作流程会涉及到许多不同公司部门的工作,因此,就”时间阈值”达成共识可以为今后关于性能的沟通提供支持,避免不必要的讨论。确保对新增资源和功能带来的资源开销了如指掌。

另外,正如 Patrick Meenan 提议的,在设计过程中,规划好加载的顺序和取舍是绝对值得的。如果你预先规划好哪部分更重要,并确定每部分出现的顺序,那么同时你也会知道哪些部分可以延迟加载。理想情况下,这个顺序也会反映出 CSS 和 JavaScript 文件的导入顺序,因此在打包阶段处理它们会变得更容易些。除此以外,还得考虑页面加载时中间态的视觉效果(比方说,当网络字体还没有加载完全时)。

规划,规划,规划。尽管在早期就投入那些能起到立竿见影效果的优化似乎相当有吸引力 — 这对需要快速决胜的项目而言可能是个不错的策略,但是如果没有务实的规划和因地制宜的性能指标,很难保证性能优先能一直受到重视。

首次绘制(First Paint)、首次有内容绘制(First Contentful Paint)、首次有意义绘制(First Meaningful Paint)、视觉完备(Visual Complete)、首次可交互时间(Time To Interactive)的区别。完整文档

3. 选择合适的指标

并不是所有的指标都同等重要。研究哪个指标对你的应用最重要,通常来说它应该与开始渲染你的产品中最重要的那些像素的速度以及提供输入响应所需的时间相关。这个要点将为你指明最佳的优化目标,提供努力的方向。

不管怎样,不要总是盯着页面完整载入的时间(比方说 onloadDOMContentLoaded),要站在用户的角度去看待页面加载。也就是说,需要关注一组稍微不同的指标。事实上,”选择正确的指标”是没有绝对完美方案的。

根据 Tim Kadlec 的研究和 Marcos Iglesias 在他的演讲中提到的,传统的指标可以归为几种类型。通常,我们需要所有的指标来构建完整的性能画像,但是在特定场景中,某些指标可能比其他的更重要些。

  • 基于数量的指标衡量请求数量、权重和性能评分等。对于告警和监控长期变化很有用,但对理解用户体验帮助不大。

  • 里程碑式指标使用加载过程中的各个状态来标记,比如:首位字节时间(Time To First Byte)首次可交互时间(Time To Interactive)。对于描述用户体验和指标很有用,但对了解加载过程中的情况帮助不大。

  • 渲染指标可以估计内容渲染的时间,例如渲染开始时间(Start Render)速度指数(Speed Index)。对于检测和调整渲染性能很有用,但对检测重要内容何时出现、何时可交互帮助不大。

  • 自定义指标衡量某个特定的、个性化的用户事件,比如 Twitter 的首次发推时间(Time To First Tweet),Pinterest 的 收藏等待时间(PinnerWaitTime)。对准确描述用户体验很有用,但不方便规模化以及与竞品比较。

为了使性能画像更加完整,我们通常会在所有类型中都选择一些有用的指标。一般来说,最重要的是以下几个:

  • 首次有效绘制(First Meaningful Paint,FMP)

    反映主要内容出现在页面上所需的时间,也侧面反映了服务器输出任意数据的速度。FMP 时间过长一般意味着 JavaScript 阻塞了主线程,也有可能是后端/服务器的问题。

  • 首次可交互时间(Time to Interactive,TTI)

    在此时间点,页面布局已经稳定,主要的网络字体已经可见,主线程已可以响应用户输入 — 基本上意味着只是用户可以与 UI 进行交互。是描述”网站可正常使用前,用户所需要等待的时长”的关键因素。

  • 首次输入延迟(First Input Delay,FID 或 Input responsiveness)

    从用户首次与页面交互,到网站能够响应该交互的时间。与 TTI 相辅相成,补全了画像中缺少的一块:在用户切实与网站交互后发生了什么。标准的 RUM 指标。有一个 JavaScript 库 可以在浏览器中测量 FID 耗时。

  • 速度指数(Speed Index)

    衡量视觉上页面被内容充满的速度,数值越低越好。速度指数由视觉上的加载速度计算而得,只是一个计算值。同时对视口尺寸也很敏感,因此你需要根据目标用户设定测试配置的范围。

  • CPU 耗时

    描述主线程处理有效负载时繁忙程度的指标,显示在绘制、渲染、运行脚本和加载时,主线程被阻塞的频次和时长。高的 CPU 耗时明显地意味着卡顿的用户体验。利用 WebPageTest,你可以在 “Chrome” 标签页上选择 “Capture Dev Tools Timeline” 选项来暴露出可能的主线程崩溃(得益于 WebPageTest 可以在任何设备上运行)。

  • 广告的影响(Ad Weight Impact)

    如果你的站点的利润主要来源于广告,那么追踪广告相关代码的体积就很有用了。Paddy Ganti 的脚本可以构筑两条 URL(一条有广告,一条没有),并且利用 WebPageTest 生成一个比较视频,并显示区别。

  • 偏离度指标(Deviation metrics)

    正如 Wikipedia 的工程师所指出的,你的结果中数据的变化在一定程度上可以反映出设施的可靠性,以及你该花多少精力来关注这些偏离度和极端值。过大的变化意味着你很可能需要对目前设施的配置做一些调整,它也能帮助我们了解有某些页面是难以可靠地用指标衡量的,例如因为第三方脚本而导致的明显变化。另外,追踪浏览器版本也是个不错的主意,它可能帮助你获悉新版浏览器可以带来的性能变化。

  • 自定义指标(Custom metrics)

    自定义指标可由具体业务和用户体验的需要专门设置。它需要你对重要像素、关键脚本、必要 CSS 样式和相关静态资源有个清晰的概念,并能够测算用户需要多长时间来下载它们。关于这点,你可以使用 Hero Rendering TimesPerformance API,为重要业务事件创建时间戳。另外,你也可以通过在 WebPageTest 测试完成后运行自定义的脚本来收集自定义的指标

Steve Souders 写了一篇文章详细地介绍了各个指标。需要注意的是:首次交互时间是在实验环境下通过自动化审查得到的,而首次输入延迟则表示真实用户在使用中感受到的实际延迟。总而言之,始终观测和追踪这两个指标会是个好主意。

不同的应用,偏好的指标可能会不同。举个例子,对于 Netflix TV 的 UI 界面而言,关键输入响应、内存使用和首次可交互时间会更重要些,而对于 Wikipedia,首末视觉变化和 CPU 耗时指标会显得更重要些。

注意:FID 和 TTI 都不关心滚动表现。滚动事件可以独立发生,因为它是主线程外的。因此,对于许多内容为主的站点而言,这些指标可能并不是很重要。

以用户为中心的性能指标可以帮助更好地了解真实用户体验。首次输入延迟(FID)是一个尝试去实现这一目标的新指标。(戳此了解详情

4. 在目标用户的典型设备上收集数据

为了得到准确的数据,我们需要选择合适的测试设备。Moto G4 会是一个不错的选择,或者是 Samsung 的一款中端产品,又或者是一款如 Nexus 5X 一样中庸的设备,以及 Alcatel 1X 这样的低端设备。你可以在 open device lab 找到这些。如果想在更慢的设备上测试,你可以花差不多 $100 买一台 Nexus 2。

如果你手上没有合适的设备,你可以通过网络限速(比如:150ms RTT,下行 1.5Mbps,上行 0.7Mbps)以及 CPU 限速(慢 5 倍)在电脑上模拟移动端体验。然后,再切换到普通 3G、4G 和 WIFI 网络进行测试。为了使性能影响更加明显,你甚至可以引入 2G 星期二,或者为了更方便测试,在办公室限制 3G 网络

时刻记着:在移动设备上,运行速度应该会比在桌面设备上慢 4-5 倍。移动设备具有不同的 GPU、CPU、内存、电池特性。如果说慢速网络制约了下载时间的话,那么手机较为慢速的 CPU 则制约了解析时间。事实上,移动设备上的解析时间通常要比桌面设备长 36%。因此,一定要在一部平均水准的设备上进行测试 — 一部你的用户中最具代表性的设备。

Introducing the slowest day of the week

在一周中选择一天让网速变慢。Facebook 就有 2G 星期二来提高对低速网络的关注。(图片来源

幸运的是,有很多工具可以帮你自动化完成数据收集、评估上述性能指标随时间变化趋势。记住,一个好的性能画像应该包括一套完整的性能指标、实验数据和实际数据

  • 集成测试工具可以在预先规定了设备和网络配置的可复制环境中收集实验数据。例如:LighthouseWebPageTest
  • 真实用户监测(RUM) 工具可以持续评估用户交互,收集实际数据。例如,SpeedCurveNew Relic,两者也都提供集成测试工具。

前者在开发阶段会非常有用,它可以帮助你在开发过程中发现、隔离、修复性能问题。后者在维护阶段会很有用,它可以帮助你了解性能瓶颈在哪儿,因为这都是真实用户产生的数据。

通过深入了解浏览器内置的 RUM API,如 Navigation TimingResource TimingPaint TimingLong Tasks 等,集成测试和 RUM 两者搭配构建出完整的性能画像。你可以使用 PWMetricsCalibre, SpeedCurvemPulseBoomerangSitespeed.io 来进行性能监测,它们都是不错的选择。另外,利用 Server Timing header,你甚至可以同时监测后端和前端性能。

注意: 建议使用浏览器外部的网络节流器,因为浏览器的 DevTools 可能会存在一些问题,比如:由于实现方法的原因,HTTP/2 push 可能会有问题。对于 Mac OS,我们可以用 Network Link Conditioner;对于 Windows,可以用 Windows Traffic Shaper;对于 Linux,可以用 netem;对于 FreeBSD,可以用dummynet

Lighthouse

Lighthouse — DevTools 自带的性能审查工具。

5. 为测试设立”纯净”、”接近真实用户”的浏览器配置(Profile)

使用被动监控工具进行测试时,一个常见的做法是:关闭反病毒软件和 CPU 后台任务,关闭后台网络连接,使用没有安装任何插件的”干净的”浏览器配置,以避免结果失真。(FirefoxChrome)。

然而,了解你的用户通常会使用哪些插件也是个不错的主意,然后使用精心设计的”接近真实用户的“浏览器配置进行测试。事实上,某些插件可能会给你的应用带来显著的性能影响。如果你有很多用户在使用这些插件,你可能需要考虑这些影响。”干净的”用户浏览器配置可能有些过于理想化了,可能会与实际情况大相径庭。

6. 与团队其他成员分享这份清单

设置切实可行的目标

7. 100 毫秒响应时间,60 fps

为了使用户感觉交互流畅,界面的响应时间不得超过 100ms。如果超过了这个时间,那么用户将会认为该应用程序是卡顿的。RAIL,一个以用户为中心的性能模型 为你提供了健康的目标:为了达到 <100 毫秒的响应,页面必须在每 50 毫秒内将控制权交还给主线程。预计输入延迟时间 可以告诉我们是否到达了这个阈值,理想情况下,它应该小于 50 毫秒。对于像动画这样的(性能)高压点,如果可以,最好不要做任何事情。

RAIL

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

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

对性能预期持悲观态度,但要 在界面设计上保持乐观明智地使用空闲时间。显然,这些目标适用于运行时性能,而不是加载性能。

8. 速度指数 < 1250,TTI(交互时间) < 5s(3G),关键文件大小 < 170KB(gzip 压缩后)

虽然很难实现,但最好将终级目标定为,首次绘制时间 1 秒以内,速度指数 的值限制在 1250 以下。由于基准是模拟在价值 200 美元的 Android 手机(如 Moto G4)上,网络为 slow 3G,400ms RTT 和 400kbps 的传输速度,目标是 交互时间低于 5 秒,对于重复访问,目标是低于 2 秒(只能通过 service worker 实现)。

请注意,当谈到互动指标时,最好区分 First CPU Idle 以及 Time to Interactive,以避免误解。前者是主要内容渲染后的最早点(其中页面至少有 5 秒的响应时间)。后者是页面可以始终响应输入的时间。

我们有两个主要限制因素,限制我们制定一个 合理的 目标来保证网络内容的快速传输。一方面,由于 TCP 慢启动,我们有着网络传输的限制。HTML 的前 14 KB是最关键的有效负载块——并且是第一次往返中唯一可以提供的预算(由于手机唤醒时间,这是在 400ms RTT 情况下 1 秒内获得的)。

另一方面,内存和 CPU 有 硬件限制(稍后我们将详细讨论它们),原因是 JavaScript 的解析时间。为了实现第一段中所述目标,我们必须考虑 JavaScript 关键文件大小的预算。关于预算应该是多少有很多不同的意见(这应该由你的项目的本身决定),但是 gzip 压缩后预算为 170KB 的 JavaScript 已经需要花费 1s 才能在普通手机上进行解析和编译。假设解压缩时 170KB 扩展到 3 倍大小,那么解压缩后(0.7MB)时,那已经可能是 Moto G4 或 Nexus 2 上”用户体验的丧钟”。

当然,你的数据可能显示你的客户没有使用这些设备,但是也许因为低下的性能导致你的服务无法访问,他们根本没有出现在你的分析中。事实上,Google 的 Alex Russels 建议将 gzip 压缩后大小为 130-170KB 作为一个合理的上限,当超出这个预算时,你应该进行慎重考虑。在现实世界中,大多数产品都不是很接近(这个标准);当今的 bundle 平均大小约为 400KB,与 2015年末相比增长了 35%。在中等水平的移动设备上,Time-To-Interactive 占 30-35 秒。

我们当然也可以超过 bundle 的大小预算。例如,我们可以根据浏览器主线程的活动设置性能预算,即在开始渲染之前进行绘制,或 跟踪前端 CPU 热点CalibreSpeedCurve 以及 Bundlesize 等工具能够帮你控制预算,并且可以集成到你的构建过程中。

此外,性能预算可能不应该是固定值。由于依赖网络连接,性能预算应该(对不同的网络条件)进行适配,但无论他们如何使用,慢速连接上的负载更加”昂贵”。

From 'Fast By Default: Modern Loading Best Practices' by Addy Osmani

From Fast By Default: Modern loading best practices by Addy Osmani(幻灯片 19)

性能预算应根据普通移动设备的网络条件进行调整。(图片来源:Katie Hempenius)(大图) 性能预算应根据普通移动设备的网络条件进行调整。

定义环境

9. 选择并设置你的构建工具

不要过分关注那些炫酷的东西。 坚持你自己的构建环境,无论是 Grunt、Gulp、Webpack、Parcel 还是工具组合。只要你获得了所需结果,并且构建过程中没有任何问题,这就可以了。

在构建工具中,Webpack 似乎是最成熟的工具,有数百个插件可用于优化构建大小。入门 Webpack 可能会很难。所以如果你想要入门,这里有一些很棒的资源:

10. 默认使用渐进增强

保持 渐进增强 作为前端架构和部署的指导原则是一个安全的选择。首先设计和构建核心体验,然后使用高级特性为支持的浏览器提升体验,创建 弹性 体验。如果你的网站在一台拥有着差劲网络、屏幕以及浏览器的慢速机器上运行的很快,那么它在一台拥有强力网络和浏览器的快速机器上只会运行地更快。

11. 选择一个高性能基准

有很多未知因素影响加载——网络,热量限制,第三方脚本,缓存替换,解析器阻塞模式,磁盘 I/O,IPC 延迟,已安装的扩展,杀毒软件和防火墙,后台 CPU 任务,硬件和内存限制,L2/L3 缓存的差异和 RTTS 等。JavaScript 的成本最高,此外默认情况下阻塞渲染的 web 字体以及图像也经常消耗过多内存。随着性能瓶颈从服务器转移到客户端,作为开发人员,我们必须更详细地考虑所有这些未知因素。

由于 170KB 的预算已经包含关键路径 HTML/CSS/JavaScript、路由、状态管理、实用程序、框架和应用程序逻辑,我们必须彻底审核我们选择不同框架的 网络传输成本,解析/编译时间和运行时成本

正如 Seb Markbåge 所指出的,衡量框架启动成本的一个好方法是首先渲染一个视图,然后将其删除后重新渲染,因为它能告诉你框架如何压缩。首次渲染趋向于唤醒一堆懒洋洋的编译代码,一个更大的树可以在压缩时收益。第二次渲染基本上模拟了随着页面复杂性的提升,页面代码是如何重用影响性能特征的。

'Fast By Default: Modern Loading Best Practices' by Addy Osmani

From Fast By Default: Modern Loading Best Practices by Addy Osmani(幻灯片 18, 19)

12. 评估每个框架以及它们的依赖项

现在,并非每个项目都需要框架,而且不是每个单页应用的页面都需要加载框架。在 Netflix 的案例中,”删除 React,几个库以及对应的客户端代码将 JavaScript 总量减少了 200KB 以上,导致 Netflix 登出主页的交互时间缩短了 50% 以上。”然后,团队利用用户在目标网页上花费的时间为用户可能使用的后续网页预读取 React(详情请继续阅读)。

这听起来很明显但是值得一提:一些项目也可以从完全删除现有框架中收益。一旦选择了一个框架,你将至少使用它好几年,所以如果你需要使用它,请确保你的选择得到了充分的考虑

Inian Parameshwaran 测量了排名前 50 的框架的性能足迹(针对首次内容渲染——从导航到浏览器从 DOM 渲染第一部分内容的时间)。Inian 发现,单独来说,Vue 和 Preact 是最快的——无论是桌面端还是移动端,其次是 React(幻灯片)。你可以检查你的候选框架和它建议的体系结构,并研究大多数解决方案如何执行,例如平均而言,使用服务端渲染或者客户端渲染。

基线性能成本很重要。根据 Ankur Sethi 的一项研究,”无论你对它的优化程度如何,你的 React 应用程序在印度的普通手机上的加载时间绝对不会低于 1.1 秒。你的 Angular 应用程序始终需要至少 2.7 秒才能启动。你的 Vue 应用程序的用户需要等待至少 1 秒才能开始使用它。”无论如何,你可能不会讲印度定位为主要市场,但是网络不佳的用户在访问你的网站是会获得类似的体验。作为交换,你的团队当然可以获得可维护性和开发人员效率。但这种考虑值得商榷。

你可以通过探索功能、可访问性、稳定性、性能、包生态系统、社区、学习曲线、文档、工具、跟踪记录和团队来评估 Sacha Greif 的12 点量表评分系统 中的框架(或者任何其他 JavaScript 库)。但是在艰难的时间表上,在选择一个选项之前,最好至少考虑大小 + 初始解析时间的总成本;轻量级选项,如 PreactInfernoVueSvelte 或者 Polymer,都可以很好地完成工作。基线的大小将定义应用程序代码的约束。

一个很好的起点是为你的应用程序选择一个好的默认堆栈。Gatsby.js(React)、Preact CLI 以及 PWA Starter Kit 为中等移动硬件上的快速加载提供了合理的默认值。

JavaScript processing times in 2018 by Addy Osmani

13. 考虑使用 PRPL 模式以及应用程序 shell 架构

不同的框架会对性能产生不同的影响,并且不需要不同的优化策略,因此你必须清楚地了解你将依赖的框架的所有细节。构建 Web 应用程序时,请查看 PRPL模式应用程序 shell 体系结构。这个想法非常简单:推送初始路由交互所需的最少代码,以便快速渲染,然后使用 service worker 进行缓存和预缓存资源,然后异步地延迟加载所需的路由。

PRPL Pattern in the application shell architecture

PRPL 代表按需推送关键资源,渲染初始路由,预缓存与按需求延迟加载剩余路由。

Application shell architecture

应用程序 shell 是驱动用户界面所需要的最少 HTML、CSS 和 JavaScript。

14. 你是否优化了各个 API 的性能?

API 是应用程序通过所谓的端点向内部和第三方应用程序公开数据的通信通道。在 设计和构建 API 时,我们需要一个合理的协议来启动服务器和第三方请求之间的通信。Representational State TransferREST)是一个合理的成熟选择:它定义了开发人员遵循的一组约束,以便以高性能,可靠和可扩展的方式访问内容。符合 REST 约束的 Web 服务称为 RESTful Web 服务

HTTP 请求成功时,当从 API 检索数据,服务器响应中的任何延迟都将传播给最终用户,从而延迟渲染。当资源想要从 API 检索某些数据时,它将需要从相应的端点请求数据。从多个资源渲染数据的组件(例如,在每个评论中包含评论和作者照片的文章)可能需要多次往返服务器以在渲染之前获取所有数据。此外,通过 REST 返回的数据量通常大于渲染该组件所需的数据量。

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

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

如果你想开始使用 GraphQL,Eric Bear 在 Smashing 杂志上发表了两篇精彩的文章:A GraphQL Primer: Why We Need A New Kind Of API 以及 A GraphQL Primer: The Evolution Of API Design

Hacker Noon

REST 和 GraphQL 之间的区别,就如左图 Redux + REST 之间的对话与右图 Apollo + GraphQL 的对话的区别(图片来源:Hacker Noon)(大图

15. 你会使用 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 作为 PWA 的数据源来构建渐进式 web APM。至于缺点?显然,因为各个平台的不同的要求和限制,开发人员需要对他们的内容,在不同平台制作和维护不同的版本,如果是 Instant Articles 和 Apple News 没有实际的URL

16. 明智地选择你的 CDN

根据你拥有的动态数据量,你可以将内容的某些部分”外包”到 静态站点生成器,将其推送到 CDN 并从中提供静态版本,从而避免数据库请求。你甚至可以选择基于 CDN 的静态托管平台,通过交互式组件丰富你的页面作为增强功能(JAMStack)。事实上,其中一些生成器(如 Reats 之上的 Gatsby)实际上是网站编译器,提供了许多自动优化功能。随着编译器随着时间的推移添加优化,编译后的输出随着时间的推移变得越来越小,越来越快。

请注意,CDN 也可以提供(和卸载)动态内容。因此,不必将CDN限制为只有静态文件。仔细检查你的 CDN 是否执行压缩和转换(例如,在格式,压缩和边缘大小调整方面的图像优化),对 服务器端工作者 的支持,包括边缘,在 CDN 边缘组装页面的静态和动态部分(即最接近用户的服务器)和其他任务。

注意:基于 Patrick Meenan 和 Andy Davies 的研究,HTTP/2 在许多 CDN 上被破坏,所以我们不应该对那里的性能提升过于乐观。

资源优化

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

2015 年,Google 推出了 Brotli,一种新开源的无损数据格式,现已被所有现代浏览器所支持。实际上,Brotli 比 Gzip 和 Deflate 有效得。因为它比较依赖配置,所以这种压缩可能会(非常)慢,但较慢的压缩意味着更高的压缩率。不过它解压速度很快。所以你可以考虑 Brotli 为你的网站所节省的成本

只有用户通过 HTTPS 访问站点时,浏览器才会接受这种格式。那代价是什么呢?Brotli 并没有预安装在一些服务器上,所以如果没有自编译 Nginx,那么配置就会相对困难。尽管如此,它也并非是不可攻破的难题,比如,Apache 自 2.4.26 版本起,开始逐步对它进行支持。得益于 Brotli 被众多厂商支持,许多 CDN 也开始支持它(AkamaiAWSKeyCDNFastlyCloudlareCDN77),你甚至(结合 service worker 一起使用)可以在不支持它的 CDN 上,启用 Brotli

在最高级别压缩时,Brotli 会非常缓慢,以至于服务器在开始发送响应前等待动态压缩资源所花费的时间,可能会抵消文件大小(被压缩后)的潜在增益。但对于静态压缩,应该首选更高级别的压缩

或者,你可以考虑使用将数据编码为 Deflate、Gzip 和 Zlib 格式的 Zopfli 的压缩算法。任何普通的 Gzip 压缩资源都可以通过 Zopfli 改进的 Deflate 编码达到比 Zlib 的最大压缩率小 3% 到 8%的文件大小。问题是压缩文件大约需要耗费 80 倍的时间。这就是为什么在资源上使用 Zopfli 是个好主意,因为这些资源不会发生太大的变化,它们被设计成只压缩一次但可以下载多次。

如果可以降低动态压缩静态资源的成本,那么这种付出是值得的。Brotli 和 Zopfli 都可以用于任意纯文本的有效负载 — HTML、CSS、SVG 和 JavaScript 等。

有何对策呢?使用最高级别的 Brotli + Gzip 来预压缩静态资源,使用 Brotli 在 1 — 4 级中动态压缩(动态)HTML。确保服务器正确处理 Brotli 或 Gzip 的协议内容。如果你在服务器上无法安装/维护 Brotli,请使用 Zopfli。

18. 使用响应图像和 WebP

尽量使用带有 srcsetsizes 属性的响应式图片和 <picture> 元素响应式图片。当然,你还可以通过在原生 <picture> 上使用 WebP 图片以及回退到 JPEG 的机制或者使用协议内容的方式中使用 WebP 格式(在 Chrome、Opera、Firefox 65、Edge 18 中都被支持的格式)(参见 Andreas Bovens 的代码片段),或者使用协议内容(Accept 头部)。Ire Aderinokun 也有关于将图像转换为 WebP 图像的超详细教程

Sketch 原生地支持 WebP 的,可以使用 Phtotshop 的 WebP 插件从 Photoshop 中导出 WebP 图像。当然也存在其他可用的选项。如果你正在使用 WordPress 或 Joomla,也可以使用一些扩展来帮助你自己轻松实现对 WebP 的支持,比如适用于 WordPress 的 OptimusCache EnablerJoomla 当然也存在对应可提供支持的扩展 (通过使用 Cody Arsenault)。

需要注意的是,尽管 WebP 图像文件大小等价于 Guetzli 和 Zopfli,但它并不支持像 JPEG 这样的渐进式渲染,这也是用户以前通过 JPEG 可以更快地看到实际图像的原因,尽管 WebP 图像在网络中的传输速度更快。使用 JPEG,我们可以将一半甚至四分之一的数据提供给用户,然后再加载剩余数据,而不是像 WebP 那样可能会导致有不完整的图像。你应该根据自己的需求来进行取舍:使用 WebP,你可以有效减少负载,使用 JPEG,你可以提高性能感知。

在 Smashing Magazine 中,我们使用 -opt 后缀来为图像命名 — 比如,brotli-compression-opt.png;这样,当我们发现图像包含该后缀时,团队成员就会明白这个图像已经被优化过了 — Jeremy Wagner 出了一本关于 WebP 的书,写的很好

Responsive Image Breakpoints Generator

响应式图片端点生成器会自动生成图像和标记。

19. 图像的优化是否得当?

当你在开发 landing page 时,特定图像的加载必须很快,要确保 JPEG 是渐进加载的并且经过了 [mozJPEG] 或者 Guetzli 的压缩(通过操作扫描级别来改进开始渲染的时间),Google 新开源的编码器专注于性能感知,并利用了从 Zopfli 和 WebP 中所学的优点。唯一的缺点是:处理时间慢(每百万像素需要一分钟的 CPU)。对于 PNG 来说,我们可以使用 Pingo,对于 SVG 来说,我们可以使用 SVGOSVGOMG。如果你需要快速预览、复制或下载网站上的所有 SVG 资源,那么你可以尝试使用 svg-grabber

虽然每一篇图像优化文章都会说,但是我还是要提醒应该保证矢量资源的干净和紧凑。要记得清理未使用的资源,删除不必要的元数据以及图稿中的路径点数量(比如 SVG 这类代码)

还有更高级的选项,比如:

  • 使用 Squoosh 以最佳压缩级别(有损或无损)压缩。调整和操作图像。

  • 使用响应式图像断点生成器CloudinaryImgix 这样的服务来实现自动化图像优化。此外,在许多情况下,使用 srcsetsizes 可以获得最佳效果。

  • 要检查响应标记的效率,你可以使用 imaging-heap(一个命令行工具)来检测不同视窗大小和设备像素比的效果。

  • 使用 lazysizes 来延迟加载图像和 iframes,这是一个通过检测用户交互(或之后我们将讨论的 IntersectionObserver)来触发任何可见性修改的库。

  • 注意默认加载的图像,它们可能永远也用不到 —— 例如,在 carousels、accordions 和 image galleries。

  • 考虑根据请求类型来指定的不同图像显示以通过 Sizes 属性切换图像,比如,操作 sizes 来交换 magnifier 组件中的数据源。

  • 为防止前景和背景图像的意外下载,请检查图像下载的不一致性

  • 为了从根本上优化存储,你可以使用 Dropbox 的新格式(Lepton)来对 JPEG 执行平均值可达到 22% 的无损压缩。

  • 注意 CSS 属性中的 aspect-ratio 属性intrinsicsize 属性,它们允许为图像设置宽高和尺寸,因此浏览器为了避免样式错乱,可以在页面加载期间提前预留一个预定义的布局槽。

  • 如果你喜欢冒险,为了更快地通过网络传输图像,可以使用基于 CDN 的实时过滤器 Edge workers 来终止并重排 HTTP/2 流。Edge workers 使用你可以控制的 JavaScript 流模块(它们是运行在 CDN 上的,可以修改响应流),这样你就可以控制图像的传输。相对于 service worker 来说,这个过程时间稍长,因为你无法控制传输过程,但它确实适用于 Edge workers。因此,你可以在针对特定登录页面逐步保存的静态 JPEG 上使用它们。

imaging-heap(一个用于检测跨视窗大小及设备像素比的加载效率的命令行工具)的输出样例

响应式图像的未来可能会随着采用客户端提示而发生巨变。客户端提示内容是 HTTP 的请求头字段,例如 DPRViewport-WidthWidthSave-DataAccept(指定图像格式首选项)等。它们应该告知服务器用户的浏览器、屏幕、连接等细节。因此,服务器可以决定如何用对应大小的图像来填充布局,而且只提供对应格式所需的图像。通过客户端提示,我们将资源从 HTML 标记中,迁移到客户端和服务器之间的请求响应协议中。

就像 Ilya Grigorik 说的那样,客户端提示使图像处理更加完整 —— 它们不是响应式图像的替代品。<picture> 在 HTML 标记中提供了必要艺术方向的控制。客户端提示为请求的图像提供注释来实现资源选择的自动化。Service Worker 为客户端提供完整的请求和响应管理功能。比如,Service Worker 可以在请求中附加新的客户端提示 header 值,重写 URL 并将图像请求指向 CDN,根据链接调整响应,用户偏好等。它不仅适用于图像资源,也适用于所有其他请求。

对于支持客户端提示的客户端,可以检测到在图像上已经节省了 42% 的字节和超过 70% 的 1MB+ 字节数。在 Smashing 杂志上,我们同样可以检测到已经提高了 19-32% 的性能。不幸的是,客户端提示仍然需要得到浏览器的支持才行FirefoxEdge 正在考虑对它的支持。但如果同时提供普通的响应图像标记和客户端提示的 <meta> 标记,浏览器将评估响应图像标记并使用客户端提示 HTTP header 请求相应的图像。

还不够?那么你可以使用多种背景图像技术来提高图像的感知性能。请记住,处理对比以及模糊不必要细节(或删除颜色)也可以减小文件大小。你想放大一张小照片而不至于损失质量的话,可以考虑使用 Letsenhance.io

到目前为止,这些优化只涉及基本内容。Addy Osmani 出版了一份非常详细的关于基本图像优化的指南,这份指南对于图像压缩和颜色管理的细节有很深入的讲解。例如,你可以模糊图像中不必要的部分(通过应用高斯模糊过滤器)来减小文件大小,甚至可以移除颜色或将图像转换为黑白来进一步缩小文件。对于背景图像,从 Photoshop 中导出的照片质量只有 0 到 10% 是完全可以接受的。不要在 web 上使用 JPEG-XR — “在 CPU 上解码 JPEG-XRs 软件端这个过程会让节省字节大小这个潜在地积极影响失效,甚至更糟,尤其是在 SPAs 情况下”。

20. 视频优化是否得当?

到目前为止,我们的已经讨论完了图像的相关内容,但我们避免了关于 GIF 优点的探讨。坦白说,与其加载影响渲染性能和带宽的重动画 GIF,不如选择动态 WebP(GIF 作为回退)或者用 HTML5 videos 循环来替换它们。是的,带有 <video> 的浏览器性能极差,而且与图像不同的是,浏览器不会预加载 <video> 内容,但它们往往比 GIF 更轻量级、更小。别无他法了么?那么至少我们可以通过 Lossy GIFgifsiclegiflossy 来有损压缩 GIF。

早期测试表明带有 img 标签的内联视频相较于等效的 GIF,除了文件大小问题外,前者的显示的速度要快 20 倍,解码要快 7 倍。虽然在 Safari 技术预览中声明了对 <img src=".mp4"> 的技术支持,但是这个特性还远未普及,因此它在近期内不会被采用

Addy Osmani 推荐用循环内联视频来取代 GIF 动画。文件大小差异明显(节省了 80%)。(预览)

前端是不停进步的领域,多年来,视频格式一直在不停改革。很长一段时间里,我们一直希望 WebM 可以成为格式的统治者,而 WebP(基本上是 WebM 视频容器中的一个静止图像)将取代过时的图像格式。尽管这些年来 WebP 和 WebM 获得了支持,但我们所希望看到的突破并未发生。

在 2018,Alliance of Open Media 发布了一种名为 AV1 的视频格式。AV1 具有和 H.265(H.264 的改进版本)编码器类似的压缩,但与后者不同的是,AV1 是免费的。H.265 的许可证价格迫使浏览器供应商采用性能相同的 AV1:AV1(与 H.265 一样)的压缩性能是 WebP 的两倍

AV1 Logo 2018

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

事实上,目前 Apple 使用的是 HEIF 格式和 HEVC(H.265),最新的 IOS 中,所有的照片和视频都以这些格式保存,而不是纯 JPEG 格式。尽管 HEIFHEVC(H.265) 并没有在网上被公开使用,但被浏览器已经开始对慢慢支持 AV1 了。因此在你的 <video> 标签中可以添加 AV1,因为所有的浏览器供应商都会慢慢加入对它的支持。

目前来说,使用最广泛的是 H.264,由 MP4 文件提供服务,因此在提供文件之前,请确保你的 MP4 文件用 multipass-encoding 处理过,用 frei0r iirblur 进行了模糊处理(如果适用),moov atom metadata 也被移动到文件头部,而你的服务器接受字节服务。Boris Schapira 提供了 FFmpeg 的确切说明来最大限度地优化视频。当然,提供 WebM 格式作为替代方案也会有所帮助。

视频回放性能本身就有很多内容可以研究,如果你想深入了解它的细节,可以参阅 Doug Sillar 关于当前视频现状视频传输最佳实践的系列视频。包括视频传输指标、视频预加载、压缩和流媒体等详细信息。

Zach Leatherman’s Comprehensive Guide to Font-Loading Strategies

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

21. Web 字体优化过了么?

值得提出的第一个问题就是,你是否可以首选 UI 系统字体。如果不是上述情况,那你所提供的 Web 字体很有可能包括系统字体没有使用的字形或额外的特性或者字体粗细。你可以要求字体提供方将字体分组,或者如果你使用的是开源字体,你可以使用 GlyphhangerFontsquirrel 自行对它们进行子集化。你甚至可以使用 Peter Müller 的 subfont,一个可以自动化你整个流程的命令行工具,它可以静态分析你的页面,生成最佳 Web 字体子集,然后注入页面中。

WOFF2 的支持性是最好的,你可以使用 WOFF 作为不支持 WOFF2 的浏览器的备用选项 — 毕竟,系统字体对遗留的浏览器版本会更友好。Web 字体的加载有很多,很多,很多的选项。你可以从 Zach Leatherman 的 “字体加载策略综合指南“中选择一种策略(代码片段也可以在 Web 字体加载中找到)。

现在,更好的选项应该是使用 Critical FOFT 结合 preload“The Compromise” 方法。它们都使用两阶段渲染来逐步提供 Web 字体 —— 首先是使用 Web 字体快速准确地渲染页面所需的小超集,然后再异步加载剩余部分,不同的是 “The Compromise” 技术只在字体加载事件不受支持的的情况下才异步加载 polyfill,所以默认情况下不需要加载 polyfill。需要快速入门?Zach Leatherman 有一个 快速入门的 23 分钟教程和案例研究来帮助你使用字体。

一般而言,使用 preload 资源提示来预加载字体是个好主意,但需要在你的标记中包含 CSS 和 JavaScript 的链接。否则,字体加载会在第一次渲染时消耗时间。尽管如此,有选择性地选择重要文件是个好主意。比如,渲染至关重要的文件会有助于你避免可视化和具有破坏性的文本刷新文件。总之,Zach 建议预加载每个系列的一到两个字体。如果这些字体不是很关键的话,延迟加载一些字体也是有意义的。

没有人喜欢等待内容的显示。使用 font-display CSS 描述符,我们可以控制字体加载行为并使内容可被立即读取(font-display: optional),或者几乎是立即被读(font-display: swap)。然而,如果你想避免文本被重排,我们仍然需要使用字体加载 API,尤其是 group repaints,或者当你使用第三方主机时。除非你可以 用 Cloudflare workers 的 Google 字体。讨论 Google 字体:考虑使用 google-webfonts-helper,这是一种轻松自我托管 Google 字体的方式。如果可以,那么自行托管你的字体会赋予你对字体最大程度的控制。

一般而言,如果你选择 font-display: optional,那么就需要放弃使用 preload,因为它会提前触发对 Web 字体的请求(如果你有其他需要获取的关键路径资源,就会导致网络阻塞)。preconnect 可以更快地获取跨域字体请求,但要谨慎使用 preload,因为来自不同域的预加载字体会导致网络竞争。所有这些技术都包含在 Zach 的 Web 字体加载

此外,如果用户在辅助功能首选项中启用了 Reduce Motion 或选择数据保护模式(详细内容可参阅 Save-Data header),那么最好是选择不使用 Web 字体(至少是第二阶段的渲染中)。或者当用户碰巧链接速度较慢时(通过 网络信息 API)。

要检测 Web 字体的加载性能,可以考虑使用所有文本可视化的度量标准(字体加载的时,所有内容立即以 Web 字体显示),以及首次渲染后的 Web 字体重排计数。显然,这两种指标越低,性能越好。重要的是考虑到变量字体对性能的需求。它们为设计师提供了更大的字体选择空间,代价是单个串行请求与许多单独的文件请求相反。这个单一的请求可能会缓慢地阻止页面上的整个排版外观。不过,好的一面是,在使用可变字体的情况下,默认情况下我们将得到一个重新的文件流,因此不需要 JavaScript 对重新绘制的内容进行分组。

有没有一种完美的 Web 字体加载策略? 子集字体为二阶段渲染做好准备,使用 font-display 描述符来声明它们,使用字体加载 API 对重新绘制的内容进行分组并将字体存储在持久化的 service worker 缓存中。如果有必要,你可以回到 Bram Stein 的 Font Face Observer。如果你有兴趣检测字体加载的性能,Andreas Marschke 研究了使用 字体 API 和 UserTiming API 的性能

最后,不要忘记加入 unicode-range,将一个大字体分解成更小的特定语言字体,使用 Monica Dinculescu 的 font-style-matcher 来最小化布局上的不和谐变化,这是因为回退和 Web 字体之间的大小会产生不一致。

构建优化

22. 确定优先级

要了解你首先要处理什么。列出你全部的静态资源清单(JavaScript、图片、字体、第三方脚本以及页面上的大模块:如轮播图、复杂的信息图表和多媒体内容),并将它们分组。

新建一个电子表格。定义旧版浏览器的基本核心体验(即完全可访问的核心内容)、现代浏览器的增强体验(即更加丰富的完整体验)以及额外功能(可以延迟加载的非必需的资源:例如网页字体、不必要的样式、轮播脚本、视频播放器、社交媒体按钮和大图片)。不久前,我们发表了一篇关于”提升 Smashing 杂志网站性能“的文章,文中详细描述了这种方法。

在优化性能时,我们需要确定我们的优先事项。立即加载核心体验,然后加载增强体验,最后加载额外功能

23. 重温优秀的”符合最低要求”技术

如今,我们仍然可以使用符合最低要求(cutting-the-mustard)技术 将核心体验发送到旧版浏览器,并为现代浏览器提供增强体验。(译者注:关于 cutting-the-mustard 出处可以参考这篇文章。)该技术的一个更新版本将使用 ES2015 + 语法 <script type="module">。现代浏览器会将脚本解释为 JavaScript 模块并按预期运行它,而旧版浏览器无法识别该属性并忽略它,因为它是未知的 HTML 语法。

现在我们需要谨记的是,单独的功能检测不足以做出该发送哪些资源到该浏览器的明智决定。就其本身而言,符合最低要求 从浏览器版本中推断出设备的能力,今天已经不再有效了。

例如,发展中国家的廉价 Android 手机主要使用 Chrome 浏览器,尽管设备的内存和 CPU 功能有限,但其仍然达到了使用符合最低要求技术的标准。最终,使用设备内存客户端提示报头,我们将能够更可靠地定位低端设备。在本文写作时,仅在 Blink 中支持该报头(通常用于客户端提示)。由于设备内存还有一个已在 Chrome 中提供的 JavaScript API,因此基于该 API 进行功能检测是一个选择,并且只有在不支持时才会再来使用符合最低要求技术

24. 解析 JavaScript 是耗时的,所以让它体积小

在处理单页面应用程序时,我们需要一些时间来初始化应用程序,然后才能渲染页面。你的设置需要你的自定义解决方案,但可以留意能够加快首次渲染的模块和技术。例如,如何调试 React 性能消除常见的 React 性能问题,以及如何提高 Angular 的性能。通常,大多数性能问题都来自启动应用程序的初始解析时间。

JavaScript 有一个解析的成本,但很少仅是由于文件大小一个因素影响性能。解析和执行时间根据设备的硬件的不同有很大差异。在普通电话(Moto G4)上,1MB(未压缩)JavaScript 的解析时间约为 1.3-1.4s,移动设备上有 15-20% 的时间用于解析。在游戏中编译,仅仅在准备 JavaScript 就平均耗时 4 秒,在移动设备上首次有效绘制(First Meaningful Paint )之前大约需要 11 秒。原因:在低端移动设备上,解析和执行时间很容易高出 2-5 倍

为了保证高性能,作为开发人员,我们需要找到编写和部署更少量 JavaScript 的方法。这就是为什么要详细检查每一个 JavaScript 依赖关系的原因。

有许多工具可以帮助你做出有关依赖关系和可行替代方案影响的明智决策:

有一种有趣方法可以用来避免解析成本,它使用了 Ember 在 2017 年推出的二进制模板。使用该模板,Ember 用 JSON 解析代替 JavaScript 解析,这可能更快。

衡量 JavaScript 解析和编译时间。我们可以使用综合测试工具和浏览器跟踪来跟踪解析时间,浏览器实现者正在谈论将来把基于 RUM 的处理时间暴露出来。也可以考虑使用 Etsy 的 DeviceTiming,这是一个小工具,它允许你使用 JavaScript 在任何设备或浏览器上测量解析和执行时间。

底线:虽然脚本的大小很重要,但它并不是一切。随着脚本大小的增长,解析和编译时间不一定会线性增加

25. 使用了摇树、作用域提升和代码分割吗

摇树(tree-shaking)是一种在 webpack 中清理构建过程的方法,它仅将实际生产环境使用的代码打包,并排除没有使用的导入模块。使用 webpack 和 rollup,还可以使用作用域提升(scope hoisting),作用域提升使得 webpack 和 rollup 可以检测 import 链可以展开的位置,并将其转换为一个内联函数,并且不会影响代码。使用 webpack,我们也可以使用 JSON Tree Shaking

此外,你可能需要考虑学习如何编写高效的 CSS 选择器,以及如何避免臃肿且耗时的样式。如果你希望更进一步,你还可以使用 webpack 来缩短 class 名,并使用作用域隔离在编译时动态重命名 CSS class 名

代码拆分(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 的代码覆盖率工具来实现它。

如果你没有使用 webpack,请注意 rollup 显示的结果明显优于 Browserify 导出。虽然我们参与其中,但你可能需要查看 rollup-plugin-closure-compilerrollupify,它将 ECMAScript 2015 模块转换为一个大型 CommonJS 模块 —— 因为根据你的包和模块系统的选择,小模块可能会有惊人高的成本

26. 可以将 JavaScript 切换到 Web Worker 中吗?

为了减少对首次可交互时间(Time-to-Interactive)的负面影响,考虑将高耗时的 JavaScript 放到 Web Worker 或通过 Service Worker 来缓存。

随着代码库的不断增长,UI 性能瓶颈将会出现,进而会降低用户的体验。主要原因是 DOM 操作与主线程上的 JavaScript 一起运行。通过 web worker,我们可以将这些高耗时的操作移动到后台进程的另一线程上。Web worker 的典型用例是预获取数据和渐进式 Web 应用程序,提前加载和存储一些数据,以便你在之后需要时使用它。而且你可以使用 Comlink 简化主页面和 worker 之间的通信。仍然还有一些工作要做,但我们已经做了很多了。

Workerize 让你能够将模块移动到 Web Worker 中,自动将导出的函数映射为异步代理。如果你正在使用 webpack,你可以使用 workerize-loader。或者,也可以试试 worker-plugin

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

27. 可以将 JavaScript 切换到 WebAssembly 中吗?

我们还可以将 JavaScript 转换为 WebAssembly,这是一种二进制指令格式,可以使用 C/C++/Rust 等高级语言进行编译。它的浏览器支持非常出色,最近它变得可行了,因为 JavaSript 和 WASM 之间的函数调用速度变得越来越快,至少在 Firefox 中是这样。

在实际场景中,JavaScript 似乎在较小的数组大小上比 WebAssembly 表现更好,而 WebAssembly 在更大的数组大小上比 JavaScript 表现更好。对于大多数 Web 应用程序,JavaScript 更适合,而 WebAssembly 最适合用于计算密集型 Web 应用程序,例如 Web 游戏。但是,如果切换到 WebAssembly 能否获得显着的性能改进,则可能值得研究。

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

WebAssembly 如何工作,以及它为什么有用的概述。

Milica Mihajlija 提供了 WebAssembly 的工作原理及其有用的原因的概述。 (预览大图

28. 是否使用了 AOT 编译?

使用 AOT(ahead-of-time)编译器将一些客户端渲染放到服务器,从而快速输出可用结果。最后,考虑使用 Optimize.js 来加速初始化加载时间,它包装了需要立即调用的函数(尽管现在这可能不是必需的了)。

"默认快速:现代加载最佳实践",作者 Addy Osmani

来自默认快速:现代加载最佳实践,作者是独一无二的 Addy Osmani。幻灯片第 76 页。

29. 仅将遗留代码提供给旧版浏览器

由于 ES2015 在现代浏览器中得到了非常好的支持,我们可以使用 babel-preset-env ,仅转义尚未被我们的目标浏览器支持的那些 ES2015 + 特性。然后设置两个构建,一个在 ES6 中,一个在 ES5 中。如上所述,现在所有主流浏览器都支持 JavaScript 模块,因此使用 script type ="module" 让支持 ES 模块的浏览器加载支持 ES6 的文件,而旧浏览器可以使用 script nomodule 加载支持 ES5 的文件。我们可以使用 Webpack ESNext Boilerplate 自动完成整个过程。

请注意,现在我们可以编写基于模块的 JavaScript,它可以原生地在浏览器里运行,无需编译器或打包工具。<link rel="modulepreload"> header 提供了一种提前(和高优先级)加载模块脚本的方法。基本上,它能够很好地最大化使用带宽,通过告诉浏览器它需要获取什么,以便在这些长的往返期间不会卡顿。此外,Jake Archibald 发布了一篇详细的文章,其中包含了需要牢记的 ES 模块相关内容,值得一读。

对于 lodash,使用 babel-plugin-lodash,通过它可以只加载你在源代码中使用的模块。你的其他依赖也可能依赖于其他版本的 lodash,因此将通用 lodash requires 转换为特定需要的功能,以避免代码重复。这可能会为你节省相当多的 JavaScript 负载。

Shubham Kanodia 撰写了一份详细的关于智能打包的低维护指南:如何在生产环境中实现仅仅将遗留代码推送到老版本浏览器上,里面还有一些你可以直接拿来用的代码片段。

正如 Jake Archibald 的文章中所解释的那样,内联脚本会被推迟,直到正在阻塞的外部脚本和内联脚本得到执行。

Jake Archibald 发布了一篇详细的文章,其中包含了 需要牢记的 ES 模块相关内容,例如:内联脚本会被推迟,直到正在阻塞的外部脚本和内联脚本得到执行。(预览大图

30. 是否使用了 JavaScript 差异化服务?

我们希望通过网络发送必要的 JavaScript,但这意味着需要更加集中精力并且细粒度地关注这些静态资源的传送。前一阵子 Philip Walton 介绍了差异化服务的想法。该想法是编译和提供两个独立的 JavaScript 包:”常规”构建,带有 Babel-transforms 和 polyfill 的构建,只提供给实际需要它们的旧浏览器,以及另一个没有转换和 polyfill 的包(具有相同功能)。

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

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

老项目充斥着陈旧和过时的代码。重新查看你的依赖项,评估重构或重写最近导致问题的遗留代码所需的时间。当然,它始终是一项重大任务,但是一旦你了解了遗留代码的影响,就可以从增量解耦开始。

首先,设置指标,跟踪遗留代码调用的比率是保持不变或是下降,而不是上升。公开阻止团队使用该库,并确保你的 CI 能够警告开发人员,如果它在拉取请求(pull request)中使用。Polyfill 可以帮助将遗留代码转换为使用标准浏览器功能的重写代码库。

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

Chrome 中的 CSS 和 JavaScript 代码覆盖率可以让你了解哪些代码已执行/已应用,哪些代码尚未执行。你可以开始记录覆盖范围,在页面上执行操作,然后浏览代码覆盖率结果。一旦你检测到未使用的代码,找到那些模块并使用 import() 延迟加载(参见整个线程)。然后重复覆盖配置文件并验证它现在在初始加载时发送的代码是否变少了。

你可以使用 Puppeteer 以编程方式收集代码覆盖率,Canary 也能够让你导出代码覆盖率结果。正如 Andy Davies 提到的那样,你可能希望同时收集现代和旧版浏览器的代码覆盖率。Puppeteer 还有许多其他用例,例如,自动视差监视每个构建的未使用的 CSS

此外,purgecssUnCSSHelium 可以帮助你从 CSS 中删除未使用的样式。如果你不确定是否在某处使用了可疑的代码,可以遵循 harryzjm@live.com Roberts 的建议:为该 class 创建 1×1px 透明 GIF 并将其放入 dead/ 目录,例如:/assets/img/dead/comments.gif。然后,将该特定图像设置为 CSS 中相应选择器的背景,然后静候几个月,查看该文件能否出现在你的日志中。如果日志里没出现该条目,则没有人使用该遗留组件:你可以继续将其全部删除。

对于爱冒险的人,你甚至可以通过使用 DevTools 监控 DevTools,通过一组页面自动收集未使用的 CSS。

33. 减小 JavaScript 包的大小

正如 Addy Osmani 指出的那样,当你只需要一小部分时,你很可能会发送完整的 JavaScript 库,以及提供给不需要它们的浏览器的过时 polyfill,或者只是重复代码。为避免额外开销,请考虑使用 webpack-libs-optimization,在构建过程中删除未使用的方法和 polyfill。

将打包审计添加到常规工作流程中。有一些你在几年前添加的重型库的轻量级替代品,例如:Moment.js 可以用 date-fnsLuxon 代替。Benedikt Rötsch 的研究表明,从 Moment.js 到 date-fns 的转换可能会使 3G 和低端手机上的首次绘制时间减少大约 300ms。

这就是 Bundlephobia 这样的工具可以帮助你找到在程序包中添加 npm 包的成本。你甚至可以将这些成本与 Lighthouse Custom Audit 相结合。这也适用于框架。通过删除或减小 Vue MDC 适配器(Vue 的 Material 组件),样式可以从 194KB 降至 10KB。

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

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

Webpack 比较

Benedikt Rötsch 的文章中,他表示,从 Moment.js 到 date-fns 的转换会使 3G 和低端手机上的首次绘制时间减少大约 300ms。(预览大图

34. 是否使用了 JavaScript 代码块的预测预获取?

我们可以使用启发式方法来决定何时预加载 JavaScript 代码块。Guess.js 是一组工具和库,它使用 Google Analytics 的数据来确定用户最有可能从给定页面访问哪个页面。根据从 Google Analytics 或其他来源收集的用户导航模式,Guess.js 构建了一个机器学习模型,用于预测和预获取每个后续页面中所需的 JavaScript。

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

显然,你可能会让浏览器预测到使用不需要的数据从而预获取到不需要的页面,因此最好在预获取请求的数量上保持绝对保守。一个好的用例是预获取结账中所需的验证脚本,或者当一个关键的 CTA(call-to-action)进入视口时的推测性预获取。

需要不太复杂的东西?Quicklink 是一个小型库,可在空闲时自动预获取视口中的链接,以便加快下一页导航的加载速度。但是,它也考虑了数据流量,因此它不会在 2G 网络或者 Data-Saver 打开时预获取数据。

35. 从针对你的目标 JavaScript 引擎进行优化中获得好处

研究哪些 JavaScript 引擎在你的用户群中占主导地位,然后探索针对这些引擎的优化方法。例如,在为 Blink 内核浏览器、Node.js 运行时和 Electron 中使用的 V8 进行优化时,使用脚本流来处理庞大的脚本。它允许在下载开始时在单独的后台线程上解析 asyncdefer scripts,因此在某些情况下可以将页面加载时间减少多达 10%。实际上,在 <head>使用 <script defer>,以便浏览器可以提前发现资源,然后在后台线程上解析它。

警告Opera Mini 不支持脚本延迟,所以如果你正在为印度或非洲开发defer 将被忽略,这会导致阻止渲染,直到脚本执行完为止

渐进式启动

渐进式启动意味着使用服务器端渲染来获得快速的首次有效绘制,但也包括一些最小的 JavaScript,以保持首次交互时间接近首次有效绘制时间。

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

在这两种情况下,我们的目标应该是设置渐进式启动:使用服务器端渲染来获得快速的首次有效绘制,但也包括一些最小的必要 JavaScript,以保持首次交互时间接近首次有效绘制时间。如果 JavaScript 在首次有效绘制之后来得太晚,浏览器可能会在解析、编译和执行后期发现的 JavaScript 时锁定主线程,从而给站点或应用程序的交互带来枷锁。

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

从本质上讲,首次可交互时间(TTI)告诉我们导航和交互之间的时间。通过查看初始内容渲染后的前五秒窗口来定义度量标准,其中任何 JavaScript 任务都不会超过 50 毫秒。如果发生超过 50 毫秒的任务,则重新开始搜索五秒钟窗口。因此,浏览器将首先假设它已到达交互状态,然后切换到冻结状态,最终切换回交互状态。

一旦我们到达交互状态,在按需或在时间允许的情况下,就可以启动应用程序的非必要部分。不幸的是,正如 Paul Lewis 所注意到的那样,框架通常没有提供给开发者优先级的概念,因此大多数库和框架都难以实现渐进式启动。如果你有时间和资源,请使用此策略最终提升性能。

那么,客户端还是服务器端?如果用户没有明显的好处,客户端渲染可能不是真正必要的 —— 实际上,服务器端渲染的 HTML 可能更快。也许你甚至可以使用静态站点生成器预渲染一些内容,并将它们直接推送到 CDN,并在顶部添加一些 JavaScript。

将客户端框架的使用限制为绝对需要它们的页面。如果做得不好,服务器渲染和客户端渲染是一场灾难。考虑在构建时预渲染动态 CSS 内联,以生成生产就绪的静态文件。Addy Osmani 就可能值得关注的 JavaScript 成本发表了精彩的演讲

37. 约束第三方脚本的影响

通过所有性能优化,我们通常无法控制来自业务需求的第三方脚本。第三方脚本指标不受最终用户体验的影响,因此通常一个脚本最终会调用令人讨厌的冗长的第三方脚本,从而破坏了专门的性能工作。为了控制和减轻这些脚本带来的性能损失,仅仅异步加载它们(可能是通过延迟)并通过资源提示(如 dns-prefetchpreconnect)加速它们是不够的。

正如 Yoav Weiss 在他关于第三方脚本的必读观点中所解释的那样,在许多情况下,这些脚本会下载动态的资源。资源在页面加载之间发生变化,因此我们没有必要知道从哪些主机下载资源以及这些资源是什么。

你有哪些选择方案?考虑使用 service worker,通过超时竞争资源下载,如果资源在特定超时内没有响应,则返回空响应以告知浏览器继续解析页面。你还可以记录或阻止未成功或不符合特定条件的第三方请求。如果可以,请从你自己的服务器而不是从供应商的服务器加载第三方脚本。

Casper.com 发布了一个详细的案例研究,说明他们如何通过自我托管的 Optimizely 将网站响应时间减少了 1.7 秒。它可能是值得的。

Casper.com 发布了一个详细的案例研究,说明他们如何通过自托管的 Optimizely 网站响应时间减少了 1.7 秒。这可能是值得的。(图片来源)(预览大图

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

比如,可能必须使用 <iframe sandbox="allow-scripts"> 来运行脚本。每个限制都可以通过 sandbox 属性上的各种 allow 值来解除(几乎所有的浏览器都受支持),因此将它们限制在应该允许的最低限度。

考虑使用 Intersection Observer;这将使广告仍然在 iframe 中,但是可以调度事件或从 DOM 获取所需信息(例如,广告可见性)。可以关注一些新的策略,例如功能策略,资源大小限制和 CPU/带宽优先级,以限制可能会降低浏览器速度的有害 Web 功能和脚本,例如:同步脚本、同步 XHR 请求、document.write 和过时的实现。

要对第三方进行压力测试,请检查 DevTools 中性能配置文件页面中的自下而上的摘要,测试如果请求被阻止或超时的情况会发生什么 —— 对于后者,你可以使用 WebPageTest 的 Blackhole 服务器 blackhole.webpagetest.org,它可以将特定域指向你的 hosts 文件。最好是自托管并使用单一主机名,但也可以生成一个请求映射,该映射公开第四方调用并检测脚本何时更改。你可以使用 harryzjm@live.com Roberts 的方法审核第三方,并生成类似这样的电子表格。harryzjm@live.com 还在他关于第三方性能和审计的讨论中解释了审计工作流程。

请求阻止

图片来源: harryzjm@live.com Roberts

38. 设置 HTTP 缓存标头

仔细检查是否已正确设置 expiresmax-agecache-control 和其他 HTTP 缓存头。通常,资源无论在短时间内(如果它们可能会更改)还是无限期(如果它们是静态的)情况下都是可缓存的 —— 你只需在需要时在 URL 中更改它们的版本。禁用 Last-Modified 标头,因为任何带有它的静态资源都将导致带有 If-Modified-Since 标头的条件请求,即使资源位于缓存中也是如此。Etag 也是如此。

使用使用专为指纹静态资源设计的 Cache-control:immutable,以避免重新验证(截至 2018 年 12 月,Firefox、Edge 和 Safari 都已经支持该功能; Firefox 仅支持 https:// 事务)。事实上,”在 HTTP 存档中的所有页面中,2% 的请求和 30% 的网站似乎包含至少 1 个不可变响应。此外,大多数使用它的网站都设置了具有较长新鲜生命周期的静态资源。”

还记得 stale-while-revalidate 吗?你可能知道,我们使用 Cache-Control 响应头指定缓存时间,例如:Cache-Control: max-age=604800。经过 604800 秒后,缓存将重新获取所请求的内容,从而导致页面加载速度变慢。通过使用 stale-while-revalidate 可以避免这种速度变慢的问题。它本质上定义了一个额外的时间窗口,在此期间缓存可以使用旧的静态资源,只要它在异步地在后台重新验证自己。因此,它”隐藏了”来自客户端的延迟(在网络和服务器上)。

在 2018 年 10 月,Chrome 发布了一个意图 在 HTTP Cache-Control 标头中对 stale-while-revalidate 的处理,因此,它应该会改善后续页面加载延迟,因为旧的静态文件不再位于关键路径中。结果:重复访问页面的 RTT 为零

你可以使用 Heroku 的 HTTP 缓存标头入门,Jake Archibald 的”缓存最佳实践“和Ilya Grigorik 的 HTTP 缓存入门作为指南。另外,要注意标头的变化,特别是与 CDN 相关的标头,并注意 Key 标头,这有助于避免当新请求与先前请求略有差异(但不显着)时,需要进行额外的往返验证。

另外,请仔细检查你是否发送了不必要的标头(例如 x-powered-bypragmax-ua-compatibleexpires 等),并且包含有用的安全性和性能标头(例如 Content-Security-Policy, X-XSS-Protection, X-Content-Type-Options 等)。最后,请记住单页应用程序中 CORS 请求的性能成本

交付优化

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

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

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

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

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

40. 使用 IntersectionObserver 加载大型组件

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

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

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

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

41. 渐进式加载图片

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

43. 尝试重组 CSS 规则

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

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

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

  • YouTube 视频链接:https://youtu.be/Cjo9iq8k-bc

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DevTools 中的"优先级"列

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

50. 优化渲染性能

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

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

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

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

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

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

HTTP/2

52. 迁移到 HTTPS,然后启用 HTTP/2

随着 Google 推进更安全的 web 并最终所有的 HTTP 页面都被 Chrome 视为”不安全”,向 HTTP/2 环境转变已经不可避免。HTTP/2 现在已经得到了很好的支持;它没有任何大的改变;并且在大多数情况下,使用它会让你得到出色的性能表现。一旦在已经 HTTPS 运行了,你可以使用 service workes 和 server push 得到巨大的性能提升(至少长期来看)。

HTTP/2

最终 Google 打算标记所有 HTTP 页面为非安全,并把 Chrome 标记失效 HTTPS 用的红色三角形作为 HTTP 的安全性指示器。(图像来源

最耗时的工作将会是迁移至 HTTPS,并且根据你的 HTTP/1.1 用户(使用过时操作系统和浏览器的用户)数量你不得不要考虑过时浏览器的性能优化而发送不同构建的版本,这需要你采纳不同的构建进程。注意:配置迁移和新的构建进程会很麻烦且耗时。在本文的余下内容中,我会假设你正在或已经迁移 HTTP/2。

53. 合适地部署 HTTP/2

为让资源通过 HTTP/2 传递需要对现在提供资源的方式进行部分修改。你需要在打包成一个大模块和并行加载许多小模块之间找到合适的平衡。最好的请求就是没有请求,然而目标是在首次快速分发资源和缓存之间找到一个好的平衡。

一方面,你可能想避免资源全都合并在一起,而是把全部的接口分割成许多小的模块,把它们压缩为构建进程的一部分,通过 “侦查”途径引用并并行加载它们。一个文件的改变不需要重新加载全部样式或 JavaScript 。它还压缩解析时间并使每个页面保持少量的资源负载。

另一方面,打包仍然是个问题。首先,压缩会受到影响。大模块压缩会受益于字典复用,而小的独立模块不会。是有一些标准来解决这个问题,但是目前还差得很远。第二,浏览器针对这种流程还没有做优化。例如,Chrome 会触发数量和资源数线性相关的进程间通讯(IPC),这样大量的资源会消耗浏览器运行时。

渐进式 CSS 加载

为了获得使用 HTTP/2 的最佳效果,请考虑渐进式加载 CSS,这是来自 Chrome 成员 Jake Archibald 的建议。

你可以尝试渐进加载式 CSS。实际上,自从 Chrome 69 开始,body 内的 CSS 已经不再阻塞 Chrome 的渲染。显然,这样做不利于使用 HTTP/1.1 的用户,所以你可能需要为不同的浏览器生成并提供不同的构建,来作为你的调度进程一部分,事情会稍微更复杂一些。你可能会使用 HTTP/2 连接聚合来避免,它允许你利用 HTTP/2 使用域切分,但实际上并不容易做到,总之,它被不认为是最佳实践。

该怎么做呢?如果你正在运行 HTTP/2,那么发送大约 6-10 个包 会是一个不错的折中方案(并且对于老旧浏览器也不会太糟糕)。需要试验和测试来为你的网站找到最佳的平衡。

54. 你的服务器和 CDN 支持 HTTP/2 吗?

不同的服务器和 CDN 可能可能对 HTTP/2 的支持不一样。使用 TLS 速度快吗?来检查你的配置,或快速查找服务器的运行情况以及可以支持的功能。

我参考了 Pat Meenan 非常棒的 HTTP/2 优先级的研究测试服务器的支持程度以确定 HTTP/2 优先级。依据 Pat 的研究,为了让 HTTP/2 优先级能可靠地工作在 Linux 4.9 以及更新的内核上,推荐开启 BBR 堵塞控制和设置 tcp_notsent_lowat 为 16 KB。Andy Davies 在多个浏览器上做了类似的 HTTP/2 优先级研究,CDN 和云托管服务

TLS 速度快吗?

TLS 速度快吗?允许你在切换到 HTTP/2 时检查你的服务器和 CDN 的配置 (大预览图)

55. OCSP Stapling 是否启用?

通过在你的服务器上启用 OCSP Stapling,可以加速 TLS 握手。创建在线证书状态协议(Online Certificate Status Protocol)(OCSP)是作为证书撤销列表(Certificate Revocation List)(CRL)协议的代替。两种协议都是用来检查 SSL 证书是否被撤销。然而,OCSP 协议不需要浏览器花费时间下载然后在列表中搜寻证书信息,因此能减少握手需要的时间。

56. 你采用 IPv6 了吗?

因为 IPv4 地址正在消耗殆尽并且主要的手机网络正在迅速接受 IPv6(美国已经达到 50% IPv6 采纳率),更新你的 DNS 为 IPv6 是一个不错的想法,这样在将来可以保持服务器安全稳固。只需要确认网络是否支持双栈 —— 它允许 IPv6 和 IPv4 同时工作。别忘了,IPv6 并不向后兼容。并且,研究表明 得益于邻居发现(NDP)和路由优化, IPv6 使这些网站提速了 10 到 15%。

57. 是否使用 HPACK 压缩?

如果你在使用 HTTP/2,请确保检查你的服务器为 HTTP 响应头实现了 HPACK 压缩来减少不必要的载荷。因为 HTTP/2 服务器都比较新,它们也许没有完全支持设计规范,HPACK 就是一个例子,H2spec 是一个出色的(从技术上讲很详尽)检查工具。HPACK 的压缩算法确实令人印象深刻,并且运行效果不错

58. 确保你的服务器安全稳固

所有浏览器的 HTTP/2 实现都是运行在 TLS 之上,所以你可能想避免安全性警告或页面中的某些元素出错。请确保 HTTP 头在安全方面得到合适配置消除已知的风险,并且检查你的证书。还有确保通过 HTTPS 加载所有的外部插件和跟踪脚本,没有跨站脚本并且已经合适地配置了 HTTP 严格传输安全头内容安全策略头

测试和监控

59. 你优化过你的审计流程吗?

可能听起来没什么大不了的,但是如果设置合适可能会减少你很多测试上的时间。请考虑使用 Tim Kadlec 的针对 WebPageTest 的 Alfred 工作流向 WebPageTest 公共实例来提交测试用例。

你也可以用 Google Spreadsheet 来驱动 WebPageTest 并且 Travis 使用 Lighthouse CI 安装了包含辅助工具,性能和 SEO 评分的测试或直接打包进 Webpack

并且如果你需要快速调试东西但你的构建进程似乎奇慢,记住”对于大部分 JavaScript 来说移除空白符和 symbol mangling 可以使被压缩代码大小减少 95% —— 并不是精巧的代码转换。你可以简单的通过压缩使 Uglify 构建速度快 3 到 4 倍。”

拉取请求(pull request)检查非常有必要

通过使用 Lighthouse CI 在 Travis 中集成辅助性工具,性能和 SEO 评分测试对所有的合作开发者来说都能显著提升开发新功能的效率。(图像来源)(大预览图

60. 你测试过代理和过时的浏览器吗?

光测试 Chrome 和 Firefox 还不够。看看你的网站在代理浏览器和过时浏览器中的表现。例如在亚洲有着巨大的市场占有率(在亚洲多达 35%)的 UC 浏览器和 Opera Mini。评估平均网络速度以避免在你的国家出现加载非常慢的情况。使用网络节流和模拟高分辨率设备测试。BrowserStack 非常不错,不过还是要在真机上测试。

k6 允许你写类似单元测试的性能测试用例。

61. 你测试过辅助工具的性能吗?

当浏览器开始加载页面,它创建 DOM,如果此时有例如屏幕阅读器的辅助技术在运行,它也会创建辅助树。屏幕阅读器必须查询辅助树来获取信息并让读者可用 —— 有时默认直接查询,有时是按需,并且它可能会消耗一些时间。

当讨论到快速到达可交互状态,通常我们指用户能尽快通过点击链接或按钮来与页面交互的指标。这个概念与屏幕阅读器的有细微不同。对于屏幕阅读器来说,最快可交互时间是指当屏幕阅读器可以读出给定页面的导航并且使用者可以实际敲击键盘来交互时的时间过去了多少。

Léonie Watson 有一个在辅助性工具的性能方面令人眼界大开的讨论并且特别指出加载慢会导致屏幕阅读器阅读延迟。屏幕阅读器本是用来快速阅读并导航的,因此可能那些视力不好的用户会比视力好的用户缺少耐心。

加载大页面和使用 JavaScript 操作 DOM 会导致屏幕阅读器语音延迟。请关注这些以前没注意到的地方,并测试所有可用的平台(Jaws,NVDA,Voiceover,Narrator,Orca)。

62. 是否建立持续监控?

对于快速无限制测试来说持有一个 WebPagetest 实例总是非常受益的。一个类似 SitespeedCalibreSpeedCurve 的可持续监控工具能自动报警,给你更详尽的性能画像。设置你自己的用户时间记录来测试和监控特殊业务指标。并请考虑加入自动性能回归警报来监控变化。

了解使用 RUM-solutions 来监控性能随时间的变化。对于像加载测试工具的自动化测试,你可以使用 k6 和它的脚本 API。并了解 SpeedTrackerLighthouseCalibre

速效方案

本文的清单相当全面,并且完成所有的优化需要相当一段时间。所以,如果你只有一小时但想获得巨大性能提升,你要怎么做?让我们总结为 12 条易于实现的目标。显然,在你开始之前和完成之后,评估结果,包括在 3G 和有线网络连接下的渲染时间和 Speed Index。

  1. 评估实际经验和设置合适的目标。一个很好的目标是追求首次有意义的渲染时间 < 1 秒,同时 Speed Index < 1250 秒,慢速 3G 网络下首次可交互时间 < 5秒,TTI < 2 秒。针对渲染时间和首次可交互时间做优化。
  2. 为你的主要模板准备关键 CSS,并在放在页面的 head 标签内(预算应小于 14 KB)。对于 CSS/JS,使它们小于关键文件大小最大预算 gzipped 压缩后为 170 KB(未压缩为 0.7 MB)。
  3. 尽可能地让更多的脚本分割,优化,defer 加载或者懒加载,检查轻量级的可选包并限制第三方包的大小。
  4. 使用 <script type="module"> 来让代码只对旧浏览器工作。
  5. 试着整个 CSS 规则并测试 in-body CSS。
  6. 使用更快的 dns-lookuppreconnectprefetchpreload 来添加资源提示来加速分发。
  7. 给网络字体分组并异步加载,在 CSS 中利用 font-display 来加速首次渲染。
  8. 优化图片,并考虑为重要的页面(例如首页)使用 WebP。
  9. 检查 HTTP 头设置的缓存并确保已经被合适地设置。
  10. 在服务器上启用 Brotli 和 Zopfli 压缩。(如果不能,别忘了启用 Gzip 压缩。)
  11. 如果 HTTP/2 可用,启用 HPACK 压缩并开始监控 mixed-content 警告。开启 OSCP 压缩。
  12. 在 service worker 中缓存字体,样式,JavaScript 和图片等资源文件。

下载清单 (PDF,Apple Pages)

记住这条清单,你应该就能应对各种前端性能方面的项目。请自由下载可打印版的 PDF 清单,同时为了供您按需定制清单还准备了可编辑的 Apple Pages 文档

如果你需要更多选择,你也可以查看 Dan Rublic 总结的前端清单,Jon Yablonski 总结的设计者的 Web 性能清单FrontendChecklist

出发!

一些优化可能超出你的工作或计划,或者对于你要处理的老旧代码可能造出更多麻烦。这都不是问题!请把这个清单作为一个(希望够全面)大纲,创建适合你的专属的问题清单。不过重中之重的是优化前测试和权衡你的项目来定位问题。希望大家在 2019 年都能得到不错的优化成绩!


非常感谢

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,Patrick Hamann,Andy Davies,Tim Kadlec,Rey Bango,Matthias Ott,Peter Bowyer,Phil Walton,Mariana Peralta,Philipp Tellis,Ryan Townsend,Ingrid Bergman,Mohamed Hussain S. H.,Jacob Groß,Tim Swalling,Bob Visser,Kev Adamson,Adir Amsalem,Aleksey Kulikov 和 Rodney Rehm 对这篇文章的审阅,同时也感谢我们无与伦比的社区,大家会分享从工作学到的,对每个人都有用的优化技术和课程。你们真的是太棒了!

Table of Contents