【译】Small Bundles, Fast Pages: What To Do With Too Much JavaScript

398 阅读8分钟

小代码包,快页面:如何处理过多的 JavaScript

原文链接:calibreapp.com/blog/bundle…

保证快速用户体验的一个非常重要的步骤就是最小化页面中JavaScript代码的量。

本文将解释代码包(bundle)大小的重要性,并推荐可以遵循的工具和流程用来监控和可视化JS代码包,当然最重要的是减小它们的体积。

代码包的大小如何影响性能?

大量的 JavaScript 代码在两个不同的时期对网站的速度产生负面影响:

  1. 页面加载时期: 下载体积大的代码包需要更长的时间。
  2. 解析与编译时期: 转换体积大的代码包成为机器码需要更长的时间,这会延迟JS的初始化。

如果用户刚好使用缓慢或不稳定的网络、电池电量低或者只是动力不足的安卓设备,体积大的代码包可能会导致加载、渲染、用户交互甚至页面滚动过程中的延迟

当然,用户没必要使用旧设备或慢速网络来获得欠佳的体验。虽然通过缓存、压缩和缩小脚本资源可以 部分 减轻代码包体积大的影响,但是 减小代码包的体积才是保证快速页面的唯一方式

通过使页面尽可能轻量,可以让每个访问者都有最大的机会获得出色的体验。

代码包的大小会影响哪些性能指标?

简而言之,代码包的大小会影响大多数的性能指标!具有大量脚本的页面会延迟最大内容绘制(Largest Contentful Paint),导致累积布局偏移(Cumulative Layout Shift),增加首次输入延迟、累积阻塞时长(Total Blocking Time)可交互时间(Time to Interactive)

这些性能指标的缓慢读数量化了糟糕的用户体验,并可能导致 SEO 排名下降

多大的JavaScript是太大呢?

当我们谈论性能时,我们通常关注资源压缩之后的大小。但是,如果资源未经过压缩,它们将大 2-3 倍。

例如,一个包含 300kB 压缩脚本的页面在解压后可以增大到 900kB–1.3MB。

在 NPMJS.com 上,commons.js 在网络传输中的大小为 306kB,但解压后大小为 1.2MB。

对于一些 CPU 受限的设备,多兆字节的有效负载对性能尤其有害:

2019 年 Cost of JavaScript。经Addy Osmani许可使用。

建议将页面脚本大小(压缩后)限制在300kb以下。在可能的情况下,使用代码分割(code splitting)将代码分解为 50KB 或更少的代码包。这样,浏览器就可以并行下载 JS 资源,充分利用 HTTP 2 的多路复用。

新的全局基线(baseline)为大约 100KiB(gzipped)的 HTML/CSS/字体和 300-350KiB 的 JavaScript (压缩后)留出了空间。

Alex Russell

使用工具和自动化操作来快速编码

设置编辑器

使用SublimeVSCode 中的导入cost插件,这样可以在编码时显示第三方库的大小:

通过导入cost插件,可以为小型或中型的包(package)设置阈值。建议设置比默认值更激进的目标:

// 将一个包算作小包的大小上限,单位是KB
"importCost.smallPackageSize": 15,

// 将一个包算作中包的大小上限,单位是KB
"importCost.mediumPackageSize": 50,

提示

导入cost插件是无法计算打包后的代码中具有相同依赖的两个库所节省的成本。

可视化代码包的内容

使用Bundle Buddysource-map-explorerwebpack-bundle-analyzer 等工具生成交互式代码包树图(bundle treemaps)。

在树图中,块的大小与文件的大小正相关——这非常适合快速发现大型导入文件!

此代码包表示 SVG 图标包含在0.js 中

通过可视化的方式探索代码包,能够识别出比预期大的模块。

寻找更小的、可替代的第三方库

通常我们选择一个依赖库,然后就一直这样使用它。但是,可能还有更多你不知道的轻量级替代方案。

使用BundlePhobia.com,可以扫描项目的 package.json 文件或搜索给定的 npm 包。

在之前的 15 个版本中,Moment.js 的大小增加了 15%。

当一个库是 “tree shakable” 时,诸如 webpack、rollup 或 esbuild 之类的打包工具可以在构建期间消除执行中未使用的代码。尽可能选择使用 tree shakable 库!

Bundlephobia 建议使用luxondayjsdate-fns作为 moment.js 的替代品。

提示

有时库较小的原因是因为它们不支持旧版本的浏览器。所以一定要测试这种边缘情况!

阻止选定的包未被使用

跨团队或跨公司之间沟通为什么使用这个包而不是那个包可能会很困难。为了解决这个问题,可以使用 ESLint 的no-restricted-import,它会在包含受限的包时发出警告或错误。

在以下示例中,当我们使用moment包时,ESLint 将导致构建失败,建议使用date-fns作为经过审查的替代方案:

{
  "rules": {
    "no-restricted-imports": [
      "error",
      {
        "paths": [
          {
            "name": "moment",
            "message": "Use date-fns instead. See https://bundlephobia.com/package/moment"
          }
        ]
      }
    ]
  }
}

动态加载组件和依赖项

大多数流行的打包工具,如WebpackESBuildRollupParcel可以对代码和依赖项进行代码拆分。代码拆分允许根据需要延迟加载(lazy-loading)应用程序的某些部分,从而减小代码包大小并加快初始加载体验。

ReactNextAngularVue都提供了工具,使延迟加载组件更加简单。下面是一个 React 示例:

import React, { Fragment, Suspense } from 'react'
import Skeleton from './Skeleton'

// Lazy loading React import
const Dashboard = React.lazy(() => import('./Dashboard'))

function Page() {
  return (
    <Fragment>
      <Suspense fallback={<Skeleton message="Loading" />}>
        <Dashboard />
      </Suspense>
    </Fragment>
  )
}

延迟加载有很多好处:

  • 要加载的初始脚本较少
  • 并行加载更多较小的请求
  • 不定期更改的代码可以长期缓存

延迟加载适用于:

  • 基于路由/导航的延迟加载:拆分每个页面所需的脚本。
  • 基于交互的延迟加载:根据需要加载依赖项。例如:当查看器打开面板时。

首选服务器端渲染主要内容

无论是对于终端用户还是 SEO 爬虫,我们都必须尽快呈现主要内容。

对于内容驱动的页面,建议在单页应用程序 (Single Page Applications SPA) 上使用服务器端渲染 (Server-Side Rendered SSR)。单页应用程序适用于会话时间长的应用或接口无缝过渡的界面(例如购物车),但是同时,必须快速显示内容。所以应该尽可能的使用服务器端渲染。

使用 Facade 延迟加载第三方资源

业务需求通常会推动第三方资源的使用,但这并不意味着开发人员不能影响第三方的性能。

Calibre使用react-live-chat-loader将**交互时间缩短了 30%**,我们的 facade 库 被用于 Help Scout、Intercom、Facebook Messenger、Drift、Userlike 和 Chatwoot。

Facade 库的工作原理是通过临时显示“假”(非交互式)聊天小部件、视频面板或支持工具来延迟第三方的加载,直到页面完成加载关键内容。

作为一个团队,可以使用多种策略来解决第三方性能问题。以下是我们的一些收藏:

  • 延迟第三方加载,直到需要使用 facade。
  • 对第三方域使用 dns-prefetching,例如:<link rel="dns-prefetch" href="https://fonts.googleapis.com/" />
  • 自己打包第三方库,而不是使用他们的 CDN。
  • 比较使用和不使用给定第三方脚本的页面性能。与对第三方工具做出决策的人分享结果!
  • 在与第三方的合同协议中请求性能服务标准。

将 ES6 模块发布给最新的浏览器

支持旧浏览器可能会阻碍新技术(及其是性能优势!)的使用。尽管如此,我们需要小心避免突然放弃对遗留技术的支持,这可能会导致无法访问。

考虑将构建(build)分成两个:

  • ES5 构建,具有浏览器支持、polyfills 和 Babel 转码。
  • ES2015+ 构建,利用async/await、期约、箭头函数、MapSet类型以及用于延迟加载的动态导入。
<!-- 不支持模块化的浏览器发布ES5代码-->
<script nomodule src="legacy-support-bundle.js"></script>

<!-- 支持模块化的浏览器发布ES2015 +代码 -->
<script type="module" src="bundle.js"></script>

资源

有关使用 Webpack 创建 ES5 和 ES2015+ 构建的说明,请参阅 Phil Walton 的优秀博文,今天在生产中部署 ES2015+ 代码

持续监视JavaScript的大小

优化代码包的大小不会以一项内务工作结束(我们希望如此!)。随着代码库的增长和演变,我们需要保护措施来控制 JavaScript 的大小。前面提到的一些工具在这里会有所帮助。

另一个推荐的策略是使用性能预算。通过设置目标,可以对指标以及它们如何影响用户体验建立问责制。在 Calibre,我们为整体 JavaScript 大小和第三方 JavaScript 创建预算,以便在超出预算时收到警告:

还可以使用 Lighthouse设置性能预算并编写属于自己的解决方案!

通过结合使用上述提示和策略,可以改善用户和开发人员的体验。如果你有其他技巧或成功的工作流程,请告诉我们!