最小化JavaScript包大小的7个实用技巧
为什么要这么做?
如今,这可能令人惊讶,但互联网流量在许多情况下仍然是一个问题。移动网络的数据套餐通常有限,设备电池不是无限的,最重要的是,用户在等待网站加载时的注意力是有限的。这就是为什么包大小仍然很重要。以下是七个供您考虑的提示。
1. 不要转换为 ES5
2020 年,我为当地一家社交网络维护一款促销应用。这是一个典型的 React + TypeScript + webpack 应用,以 ES5 为目标。当 webpack 5 发布时,我决定升级。一切进展顺利;我监控了错误分析和用户反馈,没有出现任何意外。直到一周后,我才意外发现我的包包含箭头函数——这是 webpack 的一项新功能。
这是一篇关于 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 元素级别不涉及大量交互,或者不是动态的,或者非常简单,请考虑其他选项:
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”。这些工具可帮助您识别打包器的常见问题:
除了这些工具之外,偶尔手动阅读软件包以发现异常也是个好主意。例如,由于 TypeScript 配置错误,您可能会在 ES6+ 软件包中发现 ES5 帮助程序,或在 ESM 项目中发现 CJS 帮助程序。这些问题可能无法被自动化工具发现,但仍会增加加载时间,并可能让您失去最宝贵的资产——用户的注意力。