JS `Rollup` 打包优化:ESM 优先与 Tree Shaking 极致

各位观众,大家好!今天咱们聊聊前端圈里越来越重要的打包优化,特别聚焦在 Rollup 这把瑞士军刀上,说说如何通过 ESM 优先Tree Shaking 极致 来让你的项目瘦身成功、性能起飞。

准备好了吗?系好安全带,咱们发车!

一、 打包优化:别让你的代码“虚胖”!

首先,咱们得明白打包优化为啥这么重要。想象一下,你辛辛苦苦写的代码,结果用户打开你的网站,半天加载不出来,体验贼差!原因很可能就是你的打包产物太大了,里面塞满了各种没用的东西。

代码“虚胖”的常见原因:

  • 引入了没用的库: 比如你只想用 Lodash 里的 debounce 函数,结果直接引入了整个 Lodash 库,剩下 99% 的函数都白白浪费了。
  • 重复的代码: 同一个函数或模块,在不同的地方被重复引入,导致打包体积膨胀。
  • 代码冗余: 代码写得不够精简,有很多可以优化的空间。
  • Tree Shaking 不彻底: Tree Shaking 没能有效去除未使用的代码。

打包优化就是要把这些“虚胖”的部分给砍掉,让你的代码更加精炼,加载速度更快,用户体验更好。

二、 Rollup:打包界的“瘦身大师”

Rollup 是一个 JavaScript 模块打包器,它能将小块代码编译成更大、更复杂的代码,例如库或应用程序。与其他 JavaScript 打包器(如 Webpack)相比,Rollup 更加专注于 ES 模块(ESM) 的打包,并且在 Tree Shaking 方面表现出色。

简单来说,Rollup 更擅长于打包库和框架,而 Webpack 更擅长于打包应用。当然,这也不是绝对的,现在 Webpack 在 Tree Shaking 方面也做了很多优化。

Rollup 的优势:

  • ESM 优先: Rollup 原生支持 ESM,可以更好地利用 ESM 的特性进行 Tree Shaking。
  • Tree Shaking 极致: Rollup 的 Tree Shaking 算法更加精确,能够去除更多未使用的代码。
  • 配置简单: 相比 Webpack,Rollup 的配置相对简单,更容易上手。
  • 体积小: Rollup 打包出来的代码通常比 Webpack 更小。

三、 ESM 优先:拥抱未来,解放 Tree Shaking 的潜力

ESM (ECMAScript Modules) 是 JavaScript 的官方模块化标准,它使用 importexport 关键字来导入和导出模块。

为什么要 ESM 优先?

  • 静态分析: ESM 是静态的,这意味着模块的依赖关系可以在编译时确定。这使得 Tree Shaking 成为可能,因为打包器可以知道哪些模块被使用,哪些模块没有被使用。
  • 更好的 Tree Shaking: Rollup 可以更好地利用 ESM 的静态分析特性进行 Tree Shaking,去除更多未使用的代码。
  • 面向未来: ESM 是 JavaScript 的未来,拥抱 ESM 可以让你的代码更加现代化,更容易维护。

如何使用 ESM?

很简单,只需要使用 importexport 关键字就可以了。

例子:

// utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// main.js
import { add } from './utils.js';

console.log(add(1, 2)); // 输出 3

注意事项:

  • 你的代码必须使用 .js 后缀,并且需要在 package.json 中指定 type: "module"
  • 确保你的 Node.js 版本支持 ESM。

四、 Tree Shaking 极致:砍掉多余的代码,让你的项目瘦成一道闪电!

Tree Shaking,中文名叫“摇树”,顾名思义,就是摇晃你的代码树,把那些没用的“枯枝败叶”给摇下来。

原理:

Tree Shaking 依赖于 ESM 的静态分析特性。Rollup 会分析你的代码,找出哪些模块被使用,哪些模块没有被使用,然后把那些没有被使用的模块从打包产物中移除。

如何让 Tree Shaking 更有效?

  1. 使用 ESM: 这是最重要的一点!只有使用 ESM,Rollup 才能进行 Tree Shaking。
  2. 避免副作用: 副作用是指模块在导入时会执行一些操作,例如修改全局变量。有副作用的模块很难进行 Tree Shaking,因为 Rollup 无法确定这些模块是否真的被使用。
  3. 使用纯函数: 纯函数是指没有副作用的函数,它们只依赖于输入参数,并且只返回输出结果。使用纯函数可以更容易进行 Tree Shaking。
  4. 避免动态导入: 动态导入是指在运行时导入模块,例如使用 import() 函数。动态导入会使 Rollup 难以进行静态分析,从而影响 Tree Shaking 的效果。
  5. 合理使用 sideEffects:package.json 中可以设置 sideEffects 属性,告诉 Rollup 哪些文件有副作用,哪些文件没有副作用。这可以帮助 Rollup 更精确地进行 Tree Shaking。

例子:

// package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module",
  "sideEffects": false // 告诉 Rollup 所有文件都没有副作用
}

// utils.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// main.js
import { add } from './utils.js';

console.log(add(1, 2)); // 输出 3

// 打包后,subtract 函数会被 Tree Shaking 移除,因为没有被使用。

常见的 Tree Shaking 问题和解决方案:

问题 解决方案
CommonJS 模块: 尽量使用 ESM。如果必须使用 CommonJS 模块,可以使用 Rollup 的 @rollup/plugin-commonjs 插件将其转换为 ESM。
副作用: 尽量避免副作用。如果必须有副作用,可以使用 sideEffects 属性来告诉 Rollup 哪些文件有副作用。
动态导入: 尽量避免动态导入。如果必须使用动态导入,可以使用 Rollup 的 @rollup/plugin-dynamic-import-vars 插件来处理动态导入。
引入整个库: 尽量只引入需要的模块。可以使用 ESM 的命名导入来只引入需要的模块。
代码库本身未使用 ESM: 联系代码库作者,敦促他们支持 ESM。如果代码库没有提供 ESM 版本,可以尝试使用 Rollup 的插件将其转换为 ESM。
package.json 没有 sideEffects 声明: 为你的库添加 sideEffects 声明。默认为 true,这意味着所有文件都可能有副作用。如果你确定你的库没有副作用,可以设置为 false。如果只有部分文件有副作用,可以指定一个文件数组,例如 ["./src/has-side-effects.js"]

五、 Rollup 配置:打造你的专属瘦身方案

Rollup 的配置文件通常是一个 rollup.config.js 文件,它导出一个配置对象,告诉 Rollup 如何打包你的代码。

一个简单的 rollup.config.js 例子:

import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';

export default {
  input: 'src/main.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm', // 输出 ESM 格式
    sourcemap: true
  },
  plugins: [
    nodeResolve(), // 解析 node_modules 中的模块
    commonjs(), // 将 CommonJS 模块转换为 ESM
    terser() // 压缩代码
  ]
};

常用的 Rollup 插件:

  • @rollup/plugin-node-resolve: 解析 node_modules 中的模块。
  • @rollup/plugin-commonjs: 将 CommonJS 模块转换为 ESM。
  • @rollup/plugin-terser: 压缩代码,减小打包体积。
  • @rollup/plugin-babel: 使用 Babel 转换代码,使其兼容旧版本的浏览器。
  • @rollup/plugin-json: 允许 Rollup 导入 JSON 文件。
  • rollup-plugin-postcss: 处理 CSS 文件。
  • rollup-plugin-vue: 处理 Vue 单文件组件。

配置项说明:

配置项 说明
input 入口文件。
output 输出配置。
output.file 输出文件路径。
output.format 输出格式。常用的格式有 esm (ES 模块), cjs (CommonJS), umd (通用模块定义), iife (立即执行函数)。
output.sourcemap 是否生成 sourcemap 文件。sourcemap 文件可以帮助你调试代码。
plugins 插件列表。Rollup 插件可以扩展 Rollup 的功能,例如解析模块、转换代码、压缩代码等。
external 外部依赖。如果你的代码依赖于一些外部库,你可以将这些库设置为外部依赖,这样 Rollup 就不会将这些库打包到你的代码中。这可以减小打包体积。
treeshake Tree Shaking 配置。可以控制 Tree Shaking 的行为,例如是否开启 Tree Shaking, 是否忽略某些模块的 Tree Shaking。

更复杂的 rollup.config.js 例子 (结合 TypeScript 和代码分割):

import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import { splitVendorChunk } from 'rollup';

export default {
  input: 'src/index.tsx', // TypeScript 入口
  output: [
    {
      dir: 'dist/esm',
      format: 'esm',
      sourcemap: true,
      chunkFileNames: '[name]-[hash].js' // 代码分割后的 chunk 文件名
    },
    {
      dir: 'dist/cjs',
      format: 'cjs',
      sourcemap: true,
      exports: 'named' // 导出方式,避免 default exports 带来的 Tree Shaking 问题
    }
  ],
  plugins: [
    nodeResolve(),
    commonjs(),
    typescript(), // TypeScript 支持
    terser(),
    splitVendorChunk() // 代码分割
  ],
  external: ['react', 'react-dom'] // 外部依赖
};

这个例子展示了如何使用 TypeScript、代码分割、以及如何处理外部依赖。

六、 实战案例:用 Rollup 优化一个 React 组件库

假设我们有一个 React 组件库,里面有很多组件,但是我们只想用其中的几个组件。

目录结构:

my-component-library/
├── src/
│   ├── components/
│   │   ├── Button.jsx
│   │   ├── Input.jsx
│   │   ├── Select.jsx
│   │   └── index.js
│   ├── index.js
├── package.json
├── rollup.config.js

src/components/index.js:

export { default as Button } from './Button.jsx';
export { default as Input } from './Input.jsx';
export { default as Select } from './Select.jsx';

src/index.js:

export * from './components';

rollup.config.js:

import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/index.esm.js',
      format: 'esm',
      sourcemap: true
    },
    {
      file: 'dist/index.cjs.js',
      format: 'cjs',
      sourcemap: true,
      exports: 'named'
    }
  ],
  plugins: [
    peerDepsExternal(), // 将 peerDependencies 设置为 external
    nodeResolve(),
    commonjs(),
    babel({
      babelHelpers: 'bundled', // 打包 Babel helpers
      exclude: '**/node_modules/**'
    }),
    terser()
  ]
};

package.json:

{
  "name": "my-component-library",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "files": [
    "dist"
  ],
  "peerDependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  },
  "devDependencies": {
    "@rollup/plugin-babel": "^6.0.3",
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "@rollup/plugin-terser": "^4.0.4",
    "rollup": "^4.9.1",
    "rollup-plugin-peer-deps-external": "^2.2.4"
  },
  "scripts": {
    "build": "rollup -c"
  }
}

使用示例:

// 使用方代码
import { Button } from 'my-component-library';

function App() {
  return (
    <Button onClick={() => alert('Clicked!')}>Click me</Button>
  );
}

在这个例子中,即使你的组件库有很多组件,Rollup 也会只打包你实际使用的 Button 组件,而不会打包 InputSelect 组件。

七、 总结:让你的代码“轻装上阵”

今天我们聊了 Rollup 打包优化的两个核心概念:ESM 优先和 Tree Shaking 极致。通过拥抱 ESM,我们可以更好地利用 Tree Shaking 的特性,去除更多未使用的代码。通过合理的配置 Rollup,我们可以打造出更加精炼、高效的打包产物。

记住,打包优化是一个持续的过程,需要不断地学习和实践。希望今天的分享能帮助大家更好地掌握 Rollup,让你的代码“轻装上阵”,飞得更高、更快!

感谢大家的观看!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注