构建交互式网站通常涉及向用户发送大量的 JavaScript 代码。JavaScript 是我们向手机端发送的最昂贵的资源,因为在很大程度上,它们会增加交互延迟。在这篇文章中,我们将介绍一些策略,用于在保持良好用户体验的前提下更高效地发送 JavaScript。
JavaScript 是有成本的
今天的网站通常会在他们的 JS 中发送以下内容:
- 客户端框架或 UI 库
- 状态管理解决方案(例如 Redux)
- Polyfill(现代浏览器通常并不需要它们)
- 完整库或按需加载的内容(例如 lodash、Moment 和本地化资源)
- 一组 UI 组件(按钮、标题、侧边栏等)
要加载的代码越多,页面加载时间就越长。
加载页面通常分为三个阶段,即加载是否在发生、加载的内容是否是有用的,以及加载的内容是否可用。其中最后一点涉及“交互性”问题。
交互式页面必须能够快速响应用户输入。当用户点击链接或滚动页面时,他们需要看到实际发生了什么。如果无法实现这一体验目标,你的用户就会感到沮丧。我们看到 JavaScript 影响到了很多类型网站的交互性。加载太多 JavaScript 会延迟可见元素的交互性,这对很多公司来说都是一个挑战。
可交互性会影响到很多东西。比如用户通过咖啡店 WiFi 或旅途中的间歇性网络连接来加载网页,而你的网页需要处理大量的 JavaScript,这个时候用户需要等待页面呈现内容。或者即使页面元素已经呈现出来,但他们仍然需要等待很长时间才能与这些元素进行交互。理想情况下,减少发送 JavaScript 可以缓解这些问题。
以下是一些知名网站通过减少发送 JavaScript 来提升可交互性。
- Pinterest 将他们的 JavaScript 从 2.5MB 减少到 200KB 以下,Time-to-Interactive(TTI)从 23 秒减少到 5.6 秒。收入增长了 44%,注册用户增长了 753%,移动网络上的每周活跃用户增长了 103%。
- AutoTrader 将他们的 JavaScript 大小减少了 56%,并将页面的 TTI 缩短了约 50%。
- Nikkei 将他们的 JavaScript 大小减少了 43%,TTI 缩短了 14 秒。
为什么 JavaScript 的成本如此之高
如果我们想要使用 JavaScript,必须下载它,解析它,编译它,并执行它。如果你花费很长时间在 JavaScript 引擎中解析和编译脚本,就会增加用户的交互时间。下图是 V8(Chrome 的 JavaScript 引擎)在处理包含脚本的页面时花费时间的细分:
橙色表示用于解析 JavaScript 所花费的时间,黄色是指编译的时间。把它们加起来,最多需要高达 30%的时间用于处理页面的 JavaScript——这些成本是真实存在的。
从 Chrome 66 开始,V8 使用后台线程编译代码,将编译时间缩短了 20%。但解析和编译仍然非常昂贵,并且很少看到大型脚本能够在 50 毫秒内执行完毕,即使是在线程外编译也是如此。
另外要注意,同样是 200KB 的脚本和 200KB 的图像所消耗的成本是不一样的。它们可能需要相同的时间来下载,但却具有不同的处理成本。
JPEG 图像需要进行解码、栅格化并绘制到屏幕上。JavaScript 在下载后进行解析、编译和执行,JavaScript 引擎还需要完成其他的很多步骤。这些成本是不一样的。
移动设备频谱
廉价 / 低端、中端和高端设备组成了移动设备频谱。
如果我们足够幸运,可能会拥有一部高端或中端手机,但现实情况是并非所有用户都拥有这样的设备。
他们可能使用低端或中端的手机,这些设备之间也可能存在明显的差异——散热、高速缓存大小、CPU、GPU——根据设备的不同,最终处理 JavaScript 等资源的时间也不同。
以下是 2018 年可用硬件解析 JavaScript 所需时间的细分:
随着时间的推移,Android 手机会越来越便宜,而不是更快。这些设备通常配备较差的 CPU,使用更小的 L2/L3 高速缓存。如果你希望他们都拥有高端硬件,那么普通用户该怎么办?
另外,如果你希望 JavaScript 变得更快,请注意在低端网络下的下载时间。你可以进行的改进包括:减少代码、缩小源代码、使用压缩(即 gzip、Brotli 和 Zopfli)。
使用缓存来应对重复访问的内容。对于 CPU 速度慢的手机来说,解析时间至关重要。
如何发送更少的 JavaScript
我们必须向用户发送最少量的脚本,同时仍然为他们提供良好的体验,而代码拆分是实现这种可能性的手段之一。
代码拆分就是不要向用户发送整个 JavaScript 包——有点像一块大型的披萨——如果一次只发送一小个片段会怎样?只要让当前页面的功能可用就可以了。
代码拆分可以是页面级别、路由级别或组件级别的。很多现代库和框架都通过 Webpack 和 Parcel 来实现代码拆分。React( https://reactjs.org/docs/code-splitting.html )、Vue.js( https://router.vuejs.org/guide/advanced/lazy-loading.html )和 Angular( https://angular.io/guide/lazy-loading-ngmodules )提供了这方面的指南。
很多大型团队最近在代码拆分方面取得了巨大成功。
为了确保用户能够尽早与网页进行交互,Twitter 和 Tinder 采用了激进的代码拆分,把 TTI 减少了 50%左右。
这些网站把 JavaScript 的审计分析作为工作流程的一部分。所幸的是,JavaScript 生态系统提供了很多很棒的工具可用于分析 JavaScript 包。
我们可以使用 Webpack Bundle Analyzer、Source Map Explorer 和 Bundle Buddy 等工具来分析 JavaScript 包,寻找进行代码拆分的可能性。
这些工具可以对 JavaScript 包的内容进行可视化:它们突出显示大型的库、重复的代码和你可能不需要的依赖项。
审计分析通常会突出显示重量级依赖项(如 Moment.js 及其语言环境),以便使用更轻量级的替代方案(例如 date-fns)。
度量、优化、监控和重复
如果你不确定你的 JavaScript 性能是否存在问题,可以借助 Lighthouse:
Lighthouse 是 Chrome 开发者工具中的一款工具,也可以作为 Chrome 扩展程序使用。它为你提供深入的性能分析,帮你找出性能问题。
你可以做的另一件事是确保不要将没有用的代码发送给你的用户:
Code coverage 是 DevTools 提供的一项功能,可用它发现页面中没有用到的 JavaScript(和 CSS)代码。在 DevTools 中加载页面,coverage 选项卡将显示被执行的代码数量与加载的代码数量。你只需为用户提供必要的代码,这样就可以提高页面性能。
这对于找出可以进行拆分的脚本以及延迟加载非关键脚本来说非常有用。
PRPL(Push、Render、Precache 和 Lazy-Load)是一种模式,用于对每个路由进行代码拆分,然后利用 Service Worker 预先缓存未来路由需要用到的 JavaScript 和逻辑,并按需延迟加载。
这意味着当用户导航到其他视图时,它很可能已经存在于浏览器缓存中,因此启动脚本和获取交互方面的成本降低了很多。
如果你很关心性能,或者曾经为网站开发过性能补丁,那么你就会知道,有时候你修复一个问题,但几周之后,却发现其他人添加的功能无意中破坏了先前的经验。
所幸的是,我们可以尝试解决这个问题,比如制定性能预算。
性能预算定义了可度量的约束,让团队去实现性能目标。因为你不能突破预算的限制,所以每走一步都要考虑好性能,而不是事后再来解决。
性能预算的指标包括:
- 里程碑时间——基于加载页面的用户体验的时间(例如 TTI)。
- 基于质量的指标——基于原始值(例如 JavaScript 的权重、HTTP 请求的数量)。
- 基于规则的指标——由 Lighthouse 或 WebPageTest 等工具生成的分数。
在性能预算工具方面,可以在 Lighthouse 中设置评分预算:
有很多性能监控服务支持设置性能预算和预算警报,如 Calibre、Treo、Webdash 和 SpeedCurve。
拥抱性能预算鼓励团队认真思考他们在设计阶段到里程碑结束所做出的任何决策将产生怎样的后果。
接下来……
每个网站都应该能够访问实验和生产环境的性能数据。
要想知道 JavaScript 可能对 RUM(真实用户监控)的用户体验产生怎样的影响,我建议你检查一下两件事情。
第一个是 Long Tasks( https://w3c.github.io/longtasks )——一组 API,帮助你收集真实世界中持续时间超过 50 毫秒并可能会阻塞主线程的任务。你可以将这些任务记录下来。
第二个是 First Input Delay( https://developers.google.com/web/updates/2018/05/first-input-delay ),它用于度量从用户首次向网站发起交互(如当他们点击按钮时)到浏览器能够响应该交互的时间。FID 是一个早期指标,不过现在已经有一个可用的 polyfill,你可以尝试下。
众所周知,第三方 JavaScript 可能会对页面加载性能产生严重影响,但我们也不要忽略了自家 JavaScript 对性能的影响。如果我们要加快加载速度,需要同时消除双方对用户体验产生的影响。
我们看到了几个常见的漏洞,包括在文档头部使用 JavaScript 来决定向用户显示那个版本的 A/B 测试。或者将 A/B 测试所有的 JS 都发送给用户,但实际上只用了其中一个。
如果这是你目前遇到的主要瓶颈,我们还提供了一个有关加载第三方 JavaScript 的指南( https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript )。
总结
提升性能是一段长途旅行。很多小的变化可以带来巨大的收益。
让用户能够以最少的阻力与你的网站发生交互。运行最少量的 JavaScript 以提供真正的价值。这可能需要采取渐进的步骤来实现。
最后,你的用户会感谢你。
重要链接:
PRPL 模式: https://developers.google.com/web/fundamentals/performance/prpl-pattern
Lighthouse: https://developers.google.com/web/tools/lighthouse/
Code coverage: https://developers.google.com/web/updates/2017/04/devtools-release-notes#coverage
Webpack Bundle Analyzer: https://www.npmjs.com/package/webpack-bundle-analyzer
Source Map Explorer: https://www.npmjs.com/package/source-map-explorer
Bundle Buddy: https://github.com/samccone/bundle-buddy
Calibre: https://calibreapp.com/
Treo: https://treo.sh/a/addyosmani/3
Webdash: https://webdash.xyz/
SpeedCurve: https://speedcurve.com/about/
First Input Delay: https://github.com/GoogleChromeLabs/first-input-delay
英文原文: https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4
感谢覃云对本文的审校。
评论 1 条评论