最小化JavaScript包大小的7个实用技巧

为什么要这么做?

如今,这可能令人惊讶,但互联网流量在许多情况下仍然是一个问题。移动网络的数据套餐通常有限,设备电池不是无限的,最重要的是,用户在等待网站加载时的注意力是有限的。这就是为什么包大小仍然很重要。以下是七个供您考虑的提示。

1. 不要转换为 ES5

2020 年,我为当地一家社交网络维护一款促销应用。这是一个典型的 React + TypeScript + webpack 应用,以 ES5 为目标。当 webpack 5 发布时,我决定升级。一切进展顺利;我监控了错误分析和用户反馈,没有出现任何意外。直到一周后,我才意外发现我的包包含箭头函数——这是 webpack 的一项新功能。

这是一篇关于 ES5 现状的优秀文章。主要内容如下:

  • 许多库已经包含 ES6+ 代码,这意味着它们的捆绑包与 ES5 不兼容。
  • 世界上大多数流行网站都不兼容 ES5 — — 您的网站可能也不需要它。
  • 如果您确定仍然需要 ES5 兼容性,则必须在构建过程中包含库。
  • 2. 了解并使用现代 JavaScript 语言特性

    以下一些功能可让您编写更好、更紧凑的代码。

    2.1. 生成器

    生成器是遍历嵌套结构的有效方法:

    type TreeNode = {
        left?: TreeNode
        value: T
        right?: TreeNode
    };
    
    function* traverse(root: TreeNode): Generator {
        if (root.left) yield* traverse(root.left)
        yield root.value
        if (root.right) yield* traverse(root.right)
    }

    2.2. 私有类字段

    压缩器确信这些字段不能有外部用途,即使在导出的对象中也是如此,并且可以自由缩短其名称。

    **来源**

    export class A {
      #myFancyStateObject
    }

    **捆**

    export class A{#t}

    当然,这对于 TypeScript 的“私有”字段不起作用,因为一旦“tsc”完成其工作,它们是私有的这一事实就会消失。

    2.3. 现代 API

    您听说过 Promise.withResolvers() 或 Map.groupBy() 吗?在撰写本文时,这些 API 尚未广泛使用,但很快就会普及。花点时间熟悉它们,并准备在几年后采用它们。

    提示:如何发现新的 JavaScript API

    博客和播客不计其数,但我发现最好的“新闻通讯”是 TypeScript 存储库中的新 `.d.ts` 文件。只需打开例如 es2024.collection.d.ts 即可享受 🙂

    3.避免代码重复

    你注意到了重复的模式吗?

    const clamp = (min, val, max) =>
      Math.max(min, Math.min(val, max))
    const x = clamp(0, v1, a.length - 1)
    const y = clamp(0, v2, b.length - 1)
    const z = clamp(0, v3, c.length - 1)

    重复的代码不仅会增加软件包的大小,还会使人们更难理解每个部分的作用。这通常会导致开发人员编写新代码,而不是识别和重用现有的实用函数,从而进一步增加软件包的体积。

    关于这个主题已经有很多优秀的材料,因此我推荐一本经典书籍,而不是重述,作者是 Martin Fowler。它不仅涵盖了上述简单示例,还涵盖了耦合层次结构和重复设计等复杂情况。

    现在,让我们改进一下我们的小例子。似乎“clamp”通常用于将参数限制在数组索引范围内,因此我们可以创建一个快捷方式:

    const clampToRange = (n, {length}) =>
      clamp(0, n, length - 1)
    const x = clampToRange(v1, a)
    // ...

    这一变化明确表明“n”可能是一个整数,目前尚未检查。它还强调了一个未处理的边缘情况:空数组。通过进行这个小小的重复数据删除,我们还发现了两个潜在的错误 🙂

    4.避免过度设计

    我不记得这句话的具体出处,但我认为它是正确的:

    过度设计正在解决你不存在的问题。

    在 Web 开发领域,我观察到两种主要类型的过度设计。

    4.1. 过度概括

    考虑一下这段代码。内边距是 `4px` 的倍数,背景颜色是蓝色。这可能不是巧合,如果是这样,则可能表示存在重复。但我们真的有足够的信息来提取通用的 `Button` 组件吗,还是我们过度设计了?

    **CSS**

    .btn-a {
        background-color: skyblue;
        padding: 4px;
    }
    .btn-b {
        background-color: deepskyblue;
        padding: 8px;
    }

    **JSX**

    
    // ...
    

    这条建议确实与“避免重复”有些冲突。过度删除重复代码会导致过度设计。那么,你该如何划清界限呢?就我个人而言,我使用神奇的数字“3”:一旦我看到三个地方有类似的模式,就可能是时候提取通用组件了。

    就我们的蓝色按钮而言,我相信最好的解决方案是使用 CSS 变量,至少对于“填充”,而不是创建新的组件。

    4.2. 使用不适当的框架

    是的,我说的是我们喜欢的东西 — Next.js、React、Vue 等等。如果您的应用在 DOM 元素级别不涉及大量交互,或者不是动态的,或者非常简单,请考虑其他选项:

  • 静态网站生成器——你可以从一些精选列表开始。注意:其中一些使用 React 或其他框架。如果你的目标是最小化包,请尝试其他方法。
  • 内容管理系统,例如 WordPress。
  • Vanilla — 在两种情况下特别有用:该应用程序非常简单。该应用程序不会操作太多 DOM,而是在画布上绘制一些东西。我有一个完全一样的项目。
  • 5. 避免使用过时的 TypeScript 功能

    TypeScript 的当前目标主要是对 JavaScript 进行类型检查,但情况并非总是如此。在 ES6 之前,曾有过许多创建“更好的 JavaScript”的尝试,TypeScript 也不例外。一些功能可以追溯到早期。

    5.1. 枚举

    它们不仅难以正确使用,而且还会转换成相当冗长的结构:

    **TypeScript**

    enum A {
      x, y
    }

    **JavaScript**

    var A;
    (function (A) {
        A[A["x"] = 0] = "x";
        A[A["y"] = 1] = "y";
    })(A || (A = {}));

    官方的 TypeScript 手册建议使用简单对象代替枚举。您也可以考虑使用联合类型。

    5.2. 命名空间

    命名空间是 ESM 模块出现之前的解决方案。它们不仅增加了包的大小,而且由于命名空间旨在成为全局的,因此在大型项目中很难避免命名冲突。

    **TypeScript**

    namespace A {
      export let x = 1
    }

    **JavaScript**

    var A;
    (function (A) {
        A.x = 1;
    })(A || (A = {}));

    不要使用命名空间,而要使用 ES 模块。

    6. 不要忽视小的优化

    这些小技巧中的每一个都可以为您节省包中的几个到几十个字节。如果持续应用,它们可以带来明显的效果。

    6.1. 使用 Truthy / Falsy 属性

    例如,空字符串为假。要检查它是否已定义且非空,您可以简单地编写:

    if (str) {
      // str is non-empty
    }

    6.2. 有时允许非严格比较

    我相信使用 `==` 将 `null` 强制转换为 `undefined`,反之亦然,是完全合理的。

    if (value == null) {
      // value is null or undefined,
      // but not 0 or ''
    }

    6.3 使用空值合并、逻辑或和默认参数替换默认值

    // Replaces null, undefined and '' with 'e'
    const v1 = str || 'e'
    
    // Replaces null or undefined with ''
    const v2 = str ?? ''
    
    function doSth(str = '') {
      // str is '' if not passed
    }

    6.4. 使用箭头函数编写单行代码

    而不是这样:

    function randomInt(bound) {
      return Math.floor(Math.random() * bound))
    }

    写如下:

    const randomInt = bound =>
      Math.floor(Math.random() * bound)

    6.5 不要对非常简单的对象使用类

    而不是这样:

    class User() {
      #name: string
      constructor(name: string) {
        this.#name = name
      }
      getName() {
        return this.#name
      }
    }

    写如下:

    type User = { name: string }
    const user = {
      name: 'John Doe'
    } satisfies User

    您还可以冻结对象以保护其属性不受改变。

    7.定期检查捆包

    对于每个打包器,都有可视化其内容的工具,例如用于 webpack 的“webpack-bundle-analyzer”和用于 Vite 的“vite-bundle-analyzer”。这些工具可帮助您识别打包器的常见问题:

  • 图书馆占用了过多的空间——也许是时候迁移或升级了?
  • 您的项目的不同部分使用了两个类似的库 - 您能合并并只使用一个吗?
  • 您的捆绑包中有一个大文件,但只有 0.5% 的用户访问的页面访问它(例如,许可证文本)——也许您可以使用动态 import() 对捆绑包进行分区?
  • 除了这些工具之外,偶尔手动阅读软件包以发现异常也是个好主意。例如,由于 TypeScript 配置错误,您可能会在 ES6+ 软件包中发现 ES5 帮助程序,或在 ESM 项目中发现 CJS 帮助程序。这些问题可能无法被自动化工具发现,但仍会增加加载时间,并可能让您失去最宝贵的资产——用户的注意力。