仓库源文站点原文


title: 精读 The Cost of JavaScript In 2018 layout: post thread: 200 date: 2018-08-04 author: Joe Jiang categories: Document tags: [前端, Web, JavaScript, PWA, 页面交互成本] excerpt: 如今,JavaScript 仍然是我们向移动终端分发页面时成本最高的资源,因为它可以在很大程度上延迟页面的交互性。一个页面在开发时都要考虑哪些问题,用户实际访问页面的效果与感受又是如何,Google 开发 Lighthouse 的初衷以及其具体用途,JavaScript 的成本究竟有多高,如何降低 JavaScript 成本与优雅的持续集成实践等等。 header: image: ../assets/in-post/2018-08-04-the-cost-of-javascript-in-2018-teaser.jpeg

caption: "From medium.com"

如今,JavaScript 仍然是我们向移动终端分发页面时成本最高的资源,因为它可以在很大程度上延迟页面的交互性。一个页面在开发时都要考虑哪些问题,用户实际访问页面的效果与感受又是如何,Google 开发 Lighthouse 的初衷以及其具体用途,JavaScript 的成本究竟有多高,如何降低 JavaScript 成本与优雅的持续集成实践等等。

这周在完善师兄 PWA Demo 时查阅了不少资料,对页面性能优化也做了一些比较有意思的尝试。而如上这些问题 Addy 在 The Cost of JavaScript In 2018 一文中都给出了很详实的介绍,并分享了在保证用户友好交互体验的前提下如何高效分发 JavaScript 的开发经验。正巧 JavaScript Weekly 看到这篇文章,2天 Meidum 鼓掌17k+,内容非常丰富,便尝试结合自己的理解做一次导读。

作者首先将全文的内容压缩成几条观点总结出来,之后从用户体验为 Web 带来的变化开始说起,到 JavaScript 的成本有哪些、它们为何如此高昂、如何降低开销以及持续集成,全文形成一个非常完整的优化流程。我将原文拆分为如下几节进行叙述(由于拆分了原文结构,对此在意的同学可以直接阅读原文或观看 Addy 油管演讲):

  1. #0 写在开头的话
  2. #1 tl;dr:
  3. #2 膨胀的 JavaScript 与 Web 现状
  4. #3 JavaScript 的成本所在
  5. #4 页面交互性解释与建议
  6. #5 处理 JavaScript 成本为何如此昂贵
  7. #6 千差万别的移动用户与应对策略
  8. #7 分发更少 JavaScript 的常见技巧
  9. #8 持续集成四部曲

原文地址见 https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4,视频地址见 https://www.youtube.com/watch?v=63I-mEuSvGA,精读知乎阅读地址 https://zhuanlan.zhihu.com/p/41292532,以下开始正文。

#0 写在开头的话

上图为通过 WebPageTest (src) 测定的 CNN.com 中 JavaScript 处理时间。高端机型 (iPhone 8) 的脚本处理时长在4秒以内。与之相对应的是长达13秒处理时长的一般机型 (Moto G4) 和36秒之久的2018款低端机型 (Alcatel 1X)。

如今,可交互性已经成为构建网站时不可或缺的一个考虑点,而作为最重要的实现手段,你需要将 JavaScript 代码分发到用户的设备上。考虑到此,你是否曾经历过用手机打开一个网页,当你想点击其中的链接或者滑动屏幕时,页面却没有任何响应?

#1 tl;dr:

#2 膨胀的 JavaScript 与 Web 现状

当用户访问你的网站时,为了达到预期的交互体验,你需要向他分发各类资源文件,其中脚本就占据了很大一部分。即便我们都很喜欢 JavaScript,但它一直都是网站数据传输成本中最高的那一部分,作者举了几个数据来说明这个问题:

数据源自 HTTP Archive state of JavaScript report, July 2018

数据源自 Bringing Facebook.com and the web up to speed

#3 JavaScript 的成本所在

由此作者提出了一个疑问:我们真的可以负担起这么多 JavaScript 么?一般来说,庞杂的 JS bundle 中包括:

代码越多,你的页面加载时间就越长,JavaScript 的成本主要取决于三个因素。分别是 Is it happening? Is it useful? Is it usable?

#4 页面交互性解释与建议

作者反复在文中提到交互性,在他看来,一个页面具有交互性的条件是它必须具有快速响应用户输入的能力。即不论用户点击一个链接,或者滚动页面时,他们都需要获得一些反馈以响应他们的操作。一个解释交互性的示意图如下所示:

Chrome 中提供了 LightHouse 可以对页面的各项性能指标(比如 Time-to-Interactive)进行评估:

而说到 JavaScript 的实际成本所在,则不得不说说的浏览器的线程。当浏览器在处理你在 JavaScript 中定义的各种事件时,它可能同时在该线程上还在处理用户的输入,而这就是我们所说的主线程。关于浏览器与线程的具体细节可以参考《聊聊 JavaScript 与浏览器的那些事 - 引擎与线程》,这里就不展开叙述了。

总之作者想让大家清楚的一点是,我们可以通过 Web Worker 来处理部分 JavaScript 逻辑或者通过 Service Worker 来缓存资源,以达到减轻 JavaScript 成本的目的。尽量避免阻塞主线程,了解更多这一方面的细节可以移步 Why web developers need to care about interactivity

一些 JavaScript 影响页面交互性的例子,比如 Google Search 中的各类 Tab 或者 Button

通过 WebPageTest 和 Lighthouse ()测得到移动端 Google News 的 Time-to-Interactive 数据显示,不同机型在完成交互性上存在巨大差异,高端机型需花费7秒才能让页面具备交互性,而针对同一场景低端机型则需要55秒之久。我们都希望页面的可交互性可以越快越好,但怎样为交互性定义一个好的目标呢?

作者提出一个评估基线,即我们应该让页面在慢速3G网络下也能达到五秒之内具备可交互性。而一些公司已经开始尝试分发更少的 JavaScript 并减少 Time-to-Interactive 耗时:

#5 处理 JavaScript 成本为何如此昂贵

当我们在浏览器中输入一串 URL,实际都发生了些什么?这是一个经典的面试题,作者借由这个问题尝试解释为什么 JavaScript 成本如此高昂。

当一个请求发送给了服务端,它会返回一些标记文件。之后,浏览器则解析这些标记(通常是 HTML),并从中找到必要的 CSS,JavaScript 与图片资源引用,然后向服务端再次获取这些额外资源并处理。如上描述正是 Chrome 的现有实现逻辑,我们希望浏览器快速绘制,然后使页面具备可交互性,而事实则为 JavaScript 会成为整个过程的瓶颈。那么如何避免 JavaScript 成为现代交互体验的瓶颈呢?

作为一名开发者,我们必须知道:如果我们想让 JavaScript “变快”,我们必须让下载、解析、编译和执行 JavaScript 的整个过程都变快。所以我们不仅要保证快速的网络传输,还要保证快速的脚本处理能力。

来看作者提供的一些数据,V8(Chrome 的 JavaScript 引擎)在处理包含脚本的页面时花费时间的细分统计图如下:

橙色代表的是解析 JavaScript 所用的时间,黄色代表的是编译耗时。两者加到一起占了大部分页面 JavaScript 执行的30%的时间。尽管从 Chrome 66 开始,V8 开始在后台线程编译代码,但依旧很少看到大型 JavaScript 代码能够在50ms内完成解析与编译过程。

还有一个老生常谈的话题,即作者提醒我们:执行一个200KB的脚本和一个200KB的图片成本会相差很大。它们可能占用相同的下载时长,但在执行上并不是所有的字节都占用相同的成本。

一张 JPEG 图片需要被解码、栅格化然后绘制在屏幕上,而一段 JavaScript bundle 需要被下载、解释、编译然后被执行 — 与此同时还有很多其他的环节。有关这部分可以参考[译] JavaScript 引擎基础:Shapes 和 Inline Caches

#6 千差万别的移动用户与应对策略

移动设备市场广阔,我们无法保证自己的用户都在使用平均水平以上的设备。而对于低端机型来说,缓存大小、CPU、GPU 规格都会成为限制处理诸如 JavaScript 资源速度的瓶颈。你的低端手机用户群甚至可能大部分都在美国

Android 手机正变得越来越便宜,但却没有越来越快这些设备的 CPU L2/L3 缓存依旧很小,请不要高估了你的用户群体。让我们再回到文章开头 那张 CNN.com 中 JavaScript 处理时间统计图上看看。

iPhone 8(采用 A11 芯片)在完成 JavaScript 上比中端机型快9秒。而通过对比三类机型的 filmstrips 片段,能看出低端机型甚至都不能用简单的慢来形容了,我们必须要摒弃曾经一度以为的“我们用户网络环境一直很好、很快”的天真想法。

既然如此,那么在实际开发中,我们便要想办法在真实机型和网络环境中进行测试。如果你不方便购买一堆中低端设备用于测试,类似 webpagetest.org/easy 这样的模拟配置可以为你提供便利。此外,不同网络环境的测试也同样重要,Chrome Devtools 就提供有多种模拟网络环境用于开发测试。

并不是所有网站都需要在2G网络或者低端机型上表现良好,这取决于你的实际用户群,这也是当下大家一直都在说的“用数据说话”。但请记住,即便高端机型用户也可能会遇到弱网环境,所以 JavaScript 下载时间至关重要,请善用压缩技术(例如 gzip, Brotli, Zopfli)。

在用户重复访问时利用好缓存,低配 CPU 在解析上是非常耗时的。

#7 分发更少 JavaScript 的常见技巧

代码分离 技术是一个可选项。其思想是说,取而代之一次下发所有 JavaScript bundle,我们将代码分离开,针对每个页面只下发正好保证其运行的最小 JavaScript 代码。

代码分离可以是页面级别、路由级别或者组件级别的,很多现代框架或工具库也对他有很好的支持,比如 webpack, Parcel 以及 React, Vue.jsAngular。有关代码分离的更多细节也可以参考[译] 超大型 JavaScript 应用的设计哲学。来看一段示意代码:

// 优化前
import OtherComponent from './OtherComponent';

const MyComponent = () => (
  <OtherComponent/>
);

// 优化后
import Loadable from 'react-loadable';

const LoadableOtherComponent = Loadable({
  loader: () => import('./OtherComponent'),
  loading: () => <div>Loading...</div>,
});

const MyComponent = () => (
  <LoadableOtherComponent/>
);

很多团队在投入代码分离后都获得了不小的收益。

在这些团队的项目改造中,除了代码分离,代码审查也是它们关注的一点。由于 JavaScript 生态的繁荣,已经有很多工具可以帮助我们实现这一点,例如 Webpack Bundle Analyzer, Source Map ExplorerBundle Buddy

常规审查方式

审查结果举例

#8 持续集成四部曲

#8.1 度量与优化

如果您不确定自己的 JavaScript 消耗是否有任何问题,可以试试 Lighthouse:

Lighthouse 已经集成到 Chrome 开发者工具中。当然,你也可以使用 Chrome 插件。它为你提供了深入的性能分析,并给出了一些潜在可以提高性能的建议。

LightHouse 最近添加了一个功能,即对 “高启动时间 JavaScript” 的标记支持。你可以利用它分析出当前代码中有哪些 JavaScript 会导致解析/编译耗时过长并延迟交互性,并据此拆分和优化你的代码。

你可以做的另一件事是确保没有将未使用到的代码分发给用户:

同样,代码覆盖也是 DevTools 提供的一个新特性,你可以在 Chrome 中尽情使用。

如果你正在寻找一种为用户提供高效的 JavaScript 分发模式,可以试试 PRPL 模式

PRPL 即推送,渲染,预缓存和懒加载。结合 service worker 使用更加。例如这周给师兄完善 PWA Demo 的一个小功能时,就用到了 React 在服务端渲染时采用的 renderToString 方法,在这个过程中,就是利用 HTML 在未加载 JavaScript 等资源的情况 下使用 App Shell 优化页面首次访问时的白屏体验。还挺有意思,有时间可以细说一下这个事。

#8.2 监控

为了防止多人协作或持续集成时的合作混乱,作者建议大家采用 performance budget 来进行管理与度量。

在表现性能预估这方面也有相应的 CI 工具提供支持——Lighthouse CI

开发时的性能考虑是一方面,但实际运行时用户端的表现又是怎样的呢?所以,这要求网站必须同时具有理论数据和实际表现数据的支持

在真实的用户场景监控上,作者有两点建议:

  1. Long Tasks — 利用这个 API 你可以收集那些耗时超过50毫秒、可能会阻塞主线程的任务(及其脚本),并将其数据记录用于后续分析
  2. First Input Delay (FID) 是一个度量标准,用于衡量用户首次与你的网站互动(即点击按钮时)到浏览器实际能够响应该互动的时间。虽然它还是一个新标准,但已经有 polyfill 实现。

众所周知,第三方 JavaScript 代码也是影响页面加载性能的重要因素之一,如果这是你当前需要考虑的因素之一,Google 提供有一份优化指导,可以移步 Third-party JavaScript 查看更多。

#8.3 如此往复

性能是一段旅程。许多微小的变化却可以带来巨大的收益。确保用最少的 JavaScript 代码为用户提供真正的价值,减少他们在访问网站时的困惑。不断的重复如上步骤,精益求精。

后记:原文对如上所述的很多方面提供了详尽的介绍,感兴趣可以移步 The Cost of JavaScript In 2018 精读。

参考