🌳Tree-shaking 踩坑指南:为什么你的代码死活摇不掉?

5 阅读7分钟

🤔 写在前面

各位打工人,你们有没有遇到过这种情况:明明只用了一个小小的工具函数,打包出来的文件却大得离谱?🤯

恭喜你,你可能踩到了 tree-shaking 的坑!

Tree-shaking 这个技术听起来很玄乎,其实就是帮你把没用的代码"摇掉",让打包文件瘦身。但是!很多时候我们自以为写得很优雅的代码,却让 tree-shaking 直接罢工 😤

今天就来扒一扒那些年我们踩过的 tree-shaking 大坑,保证看完之后你再也不会被这些"隐形杀手"坑到!

🔍 Tree-shaking 到底是个啥?

简单来说,tree-shaking 就像一个超级挑剔的断舍离专家 🧹,它会仔细检查你的代码,把那些"买了但从来不穿的衣服"(没用的代码)统统扔掉。

它的工作原理其实很简单:

  1. 静态分析:在打包的时候就把你的代码扫一遍,看看谁用了谁
  2. ES6 模块:只认 import/export 这套语法(老古董 CommonJS 它不认 🙄)
  3. 副作用检测:如果你的代码有"副作用"(比如偷偷修改全局变量),它就不敢动你

听起来很美好对吧?但现实总是很骨感... 😅

💀 Tree-shaking 失效的八大死法

1. 💀 死法一:还在用老古董 CommonJS

这是最经典的坑,没有之一!很多同学到现在还在用 requiremodule.exports,然后疑惑为什么 tree-shaking 不工作...

❌ 错误写法

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

function multiply(a, b) {
  return a * b;
}

// CommonJS 导出
module.exports = {
  add,
  multiply
};

// main.js
const { add } = require('./utils');
console.log(add(1, 2));

✅ 正确写法

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

export function multiply(a, b) {
  return a * b;
}

// main.js
import { add } from './utils';
console.log(add(1, 2));

为啥不行?:CommonJS 是个"渣男",说话不算话!它的 require() 是运行时才执行的,tree-shaking 工具在编译时根本不知道你要加载什么,只能选择"宁可错杀,不可放过",把所有代码都保留 🤷‍♂️

2. 💀 死法二:玩起了"动态加载"

❌ 错误写法

// 动态属性访问
import * as utils from './utils';
const funcName = 'add';
utils[funcName](1, 2);

// 条件导入
import * as utils from './utils';
if (someCondition) {
  utils.add(1, 2);
} else {
  utils.multiply(1, 2);
}

✅ 正确写法

// 明确的静态导入
import { add, multiply } from './utils';

if (someCondition) {
  add(1, 2);
} else {
  multiply(1, 2);
}

为啥不行?:你这样写,tree-shaking 工具就懵了:"这哥们要用哪个函数?我咋知道?算了算了,全留着吧!" 😵‍💫

3. 💀 死法三:代码有"副作用"

❌ 错误写法

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

// 模块级别的副作用
console.log('utils module loaded');
window.globalVar = 'some value';

// 或者在函数中有副作用
export function multiply(a, b) {
  console.log('multiply called'); // 副作用
  return a * b;
}

✅ 正确写法

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

export function multiply(a, b) {
  return a * b;
}

// 如果确实需要副作用,明确标记
// package.json
{
  "sideEffects": ["./src/utils.js"]
}

为啥不行?:副作用就像是代码界的"熊孩子",你永远不知道它会搞什么破坏。tree-shaking 工具为了保险起见,只能选择不动它们。毕竟删错了代码,程序崩了,锅还不是要我们背? 🤪

4. 💀 死法四:Class 重度患者

❌ 错误写法

// utils.js
export class Calculator {
  add(a, b) {
    return a + b;
  }
  
  multiply(a, b) {
    return a * b;
  }
  
  // 未使用的方法
  divide(a, b) {
    return a / b;
  }
}

// main.js
import { Calculator } from './utils';
const calc = new Calculator();
console.log(calc.add(1, 2));

✅ 正确写法

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

export function multiply(a, b) {
  return a * b;
}

export function divide(a, b) {
  return a / b;
}

// main.js
import { add } from './utils';
console.log(add(1, 2));

为啥不行?:Class 就像一个"全家桶套餐",你说你只要薯条,但人家必须给你汉堡、可乐一起打包。tree-shaking 没法把类拆开,只能整个保留 🍔

5. 💀 死法五:对象导出大法

❌ 错误写法

// utils.js
const mathUtils = {
  add(a, b) {
    return a + b;
  },
  multiply(a, b) {
    return a * b;
  },
  divide(a, b) {
    return a / b;
  }
};

export default mathUtils;

// main.js
import mathUtils from './utils';
console.log(mathUtils.add(1, 2));

✅ 正确写法

// utils.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

// main.js
import { add } from './utils';
console.log(add(1, 2));

为啥不行?:对象导出就像是"买一送一"活动,你本来只想要一个函数,结果整个对象的所有方法都被强制塞给你了 🎁

6. 💀 死法六:export * 梭哈流

❌ 错误写法

// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;

// string.js
export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
export const reverse = (str) => str.split('').reverse().join('');

// index.js
export * from './math';
export * from './string';

// main.js
import { add } from './index';
console.log(add(1, 2));

✅ 正确写法

// index.js
export { add, multiply } from './math';
export { capitalize, reverse } from './string';

// 或者更好的方式
// main.js
import { add } from './math';
console.log(add(1, 2));

为啥不行?export * 就像是一个"无脑转发"的微博博主,不管三七二十一,把所有东西都转发一遍。tree-shaking 工具看到这种操作直接就放弃治疗了 😮‍💨

7. 💀 死法七:第三方库使用姿势不对

❌ 错误写法

// 导入整个库
import _ from 'lodash';
import * as utils from 'my-utils-lib';

console.log(_.get(obj, 'path'));
console.log(utils.someFunction());

✅ 正确写法

// 按需导入
import get from 'lodash/get';
import { someFunction } from 'my-utils-lib';

console.log(get(obj, 'path'));
console.log(someFunction());

为啥不行?:这就像是去超市买个苹果,结果收银员说:"不好意思,苹果不单卖,你得把整个水果区都买走!" 🍎🍌🍇

8. 💀 死法八:循环依赖死循环

❌ 错误写法

// a.js
import { funcB } from './b';
export const funcA = () => {
  console.log('A');
  funcB();
};

// b.js
import { funcA } from './a';
export const funcB = () => {
  console.log('B');
  funcA();
};

✅ 正确写法

// shared.js
export const sharedLogic = () => {
  console.log('shared');
};

// a.js
import { sharedLogic } from './shared';
export const funcA = () => {
  console.log('A');
  sharedLogic();
};

// b.js
import { sharedLogic } from './shared';
export const funcB = () => {
  console.log('B');
  sharedLogic();
};

为啥不行?:循环依赖就像是"先有鸡还是先有蛋"的哲学问题,tree-shaking 工具分析到这里直接就卡死了:"我TM到底该从哪开始分析???" 🐔🥚

🔍 怎么知道你的 Tree-shaking 有没有生效?

1. 📊 用工具可视化分析(推荐!)

别瞎猜了,数据说话最靠谱:

# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer

# Rollup Bundle Analyzer
npm install --save-dev rollup-plugin-analyzer

# Vite Bundle Analyzer
npm install --save-dev rollup-plugin-visualizer

2. 👀 直接看打包后的代码(硬核方法)

// 创建测试文件验证
// test-tree-shaking.js
import { add } from './utils';

console.log(add(1, 2));

// 构建后检查是否包含未使用的 multiply 函数

3. ⚙️ 正确配置构建工具(别忘了这步!)

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false
  }
};

// package.json
{
  "sideEffects": false
}

🏆 Tree-shaking 最佳实践(收藏级)

1. 🎯 拥抱 ES6 模块,告别石器时代

  • 无脑使用 import/export,把 require 扔进历史垃圾桶
  • 千万别 CommonJS 和 ES6 混着用,会出人命的! ⚠️

2. 🏷️ 给副作用贴个标签

{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

3. 🎯 第三方库按需导入(省钱大法)

// 使用支持 tree-shaking 的库
import { debounce } from 'lodash-es';

// 或使用 babel-plugin-import
// .babelrc
{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

4. 🏗️ 代码结构要讲究(强迫症福音)

src/
  utils/
    math.js      // 只导出数学相关函数
    string.js    // 只导出字符串相关函数
    index.js     // 明确的重新导出

5. 💪 TypeScript 加持(类型安全 + Tree-shaking 双重保险)

// 类型信息有助于更好的 tree-shaking
export interface MathUtils {
  add: (a: number, b: number) => number;
  multiply: (a: number, b: number) => number;
}

export const add: MathUtils['add'] = (a, b) => a + b;
export const multiply: MathUtils['multiply'] = (a, b) => a * b;

🎉 写在最后

Tree-shaking 这个技术说复杂也复杂,说简单也简单。关键是要理解它的"脾气"——它就像一个有洁癖的室友,喜欢干净整洁的代码,讨厌那些乱七八糟的写法。

划重点!敲黑板! 📝

  1. ES6 模块是基础 - 不用这个,其他都白搭
  2. 副作用要标明 - 别让工具瞎猜
  3. 静态导入是王道 - 动态的东西它搞不定
  4. 代码结构要清晰 - 乱糟糟的代码谁都不爱
  5. 配置要正确 - 工具再智能也需要你告诉它怎么做

记住这些,你的打包文件就能瘦成一道闪电!⚡

最后的最后,如果你觉得这篇文章对你有帮助,别忘了点赞收藏哦!毕竟,好文章就应该被更多人看到 😉


🔥 热门评论预告

  • "卧槽,我踩了8个坑里的7个!"
  • "终于知道为什么我的包这么大了..."
  • "作者写得太生动了,忍不住一口气看完!"
  • "建议加精!新手必看!"

本文所有示例都经过实际测试,保证每一个坑都是真实存在的。如果你还有其他 tree-shaking 相关的问题,欢迎在评论区交流!我们一起把前端优化这件事做到极致! 💪