查看原文
其他

【第1354期】精读 The Cost of JavaScript

hijiangtao 前端早读课 2019-05-31

前言

今日早读文章由阿里飞猪@hijiangtao分享。

正文从这开始~

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

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

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

  • 0 写在开头的话

  • tl;dr:

  • 膨胀的 JavaScript 与 Web 现状

  • JavaScript 的成本所在

  • 页面交互性解释与建议

  • 处理 JavaScript 成本为何如此昂贵

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

  • 分发更少 JavaScript 的常见技巧

  • 持续集成四部曲

写在开头的话

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

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

tl;dr:

想要保持页面的快速运行,你需要仅加载当前页面所需的 JavaScript 代码。优先考虑用户所需,之后运用代码分离懒加载其他内容。

拥抱性能估算并学会与他相处。比如为自己的网页下一个目标——压缩后的 JS 代码体积小于 170KB。

学会如何审查并修剪你的 JavaScript bundle。比如你只用到了一个函数,但最终却引入了一整个三方库;又或者你为了做老旧浏览器的兼容实现了 polyfill,但最终发现你的用户都在用现代浏览器。

请记住每次交互都是一次新的 ‘Time-to-Interactive’ 的开始,以此进行代码优化。

如果用户端的 JavaScript 并没有提升用户体验,你则需要问问自己这些代码是否多余。比如,服务端渲染 HTML 或许更适合你?

膨胀的 JavaScript 与 Web 现状

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

资源大小与耗时:当下网页传输的压缩过的 JavaScript 资源平均大小为 350KB,解压后的资源大小则会超过 1MB;而处理这么多 JavaScript 代码直至网页具备交互性会耗费移动设备超过14秒的时间。

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

移动网络现状:来自 OpenSignal 的全球4G网络可用性统计表明,很多国家依旧经历着比我们想象还要慢的连接速度,而这还不包括很多未列入统计范畴的国家与地区。

现实情况:一些著名网站例如 Google、Facebook、LinkedIn 等所需加载的脚本大小早已远超平均大小 350KB,桌面端的 Facebook 站点 JavaScript 解压后可以达到 7.1MB。

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

JavaScript 的成本所在

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

  • 运行于客户端的框架或者 UI 库;

  • 状态管理方案(例如 Redux);

  • Polyfills;

  • 完整的工具库或者分割过的其中一部分方法代码;

  • 一套 UI 组件,按钮、导航栏等等;

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

Is it happening - 在这个时期,你可以开始往屏幕上分发内容(页面是否开始跳转?服务端是否开始响应?)。

Is it useful - 在这个时期,你已经完成了文本或内容的绘制,并允许用户从其中获取价值与有用信息。

Is it usable - 在这个时期,用户可以与页面进行实际操作,并能产生一些有意义的交互。

页面交互性解释与建议

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

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 耗时:

Pinterest 将 JavaScript bundle 从 2.5MB 降低到小于 200KB,而 Time-to-Interactive 时间则从23秒降到5.6s. 收入增长44%,注册增长753%,移动互联网周活跃用户增长103%。

AutoTrader 将 JavaScript bundle 大小降低了 56% 并将达到 Time-to-Interactive 的时长缩短了一半。

Nikkei 将 JavaScript bundle 大小降低了 43% 并将 Time-to-Interactive 耗时缩短了13秒。

处理 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 需要被下载、解释、编译然后被执行 — 与此同时还有很多其他的环节。有关这部分可以参考【第1314期】JavaScript 引擎基础:Shapes 和 Inline Caches

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

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

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

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

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

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

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

分发更少 JavaScript 的常见技巧

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

代码分离可以是页面级别、路由级别或者组件级别的,很多现代框架或工具库也对他有很好的支持,比如 webpack, Parcel 以及 React, Vue.js 和 Angular。有关代码分离的更多细节也可以参考【第1255期】超大型 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 Explorer 和 Bundle Buddy。

常规审查方式

审查结果举例

持续集成四部曲

度量与优化

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

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

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

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

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

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

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

监控

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

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

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

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

  • Long Tasks — 利用这个 API 你可以收集那些耗时超过50毫秒、可能会阻塞主线程的任务(及其脚本),并将其数据记录用于后续分析

  • First Input Delay (FID) 是一个度量标准,用于衡量用户首次与你的网站互动(即点击按钮时)到浏览器实际能够响应该互动的时间。虽然它还是一个新标准,但已经有 polyfill 实现。


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

如此往复

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

参考

  • Can You Afford It?: Real-world Web Performance Budgets

  • Progressive Performance

  • Reducing JavaScript payloads with Tree-shaking

  • Ouch, your JavaScript hurts!

  • Fast & Resilient — Why carving out the “fast” path isn’t enough

  • Web performance optimization with Webpack

  • JavaScript Start-up Optimization

  • The Impact Of Page Weight On Load Time

  • Beyond The Bubble — Real-world Performance

  • How To Think About Speed Tools

  • Thinking PRPL

关于本文
作者:@hijiangtao
原文:
https://zhuanlan.zhihu.com/p/41292532
https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4

最后,为你推荐


【图书】Node.js:来一打 C++ 扩展


【第1346期】如何更好的编写CSS


【第1342期】图解 React Native

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存