性能优化之剔除无用代码(Tree shaking 与 no-unused-vars)

283 阅读5分钟

性能优化之剔除无用代码(Tree shaking 与 no-unused-vars)

Tree Shaking 和 ESLint 的 no-unused-vars 规则都是常用的性能优化方式,两者的实现原理其实都是基于AST。本文将从性能优化的角度,通过具体示例详细分析两者的区别与原理。


ESLint no-unused-vars 规则

作用

ESLint 的 no-unused-vars 规则用于在代码编写阶段检测未使用的变量、函数和模块导入。它通过静态代码分析帮助开发者识别潜在的代码问题,提高代码质量和可维护性。

实现原理

  1. 构建作用域链:遍历 AST 时创建变量作用域,记录每个作用域内的变量声明
  2. 追踪变量引用:在 AST 遍历过程中记录变量被引用的位置
  3. 交叉验证声明与引用:将变量声明与其所有引用进行匹配,标记未被引用的声明

我们通过一个具体示例说明其 AST 处理过程:

示例代码

function calculate() {
  const a = 1;          // 被使用的变量
  const b = 2;          // 未使用的变量
  return a + 3;
}

AST 解析(简化后的关键节点)

{
  "type": "FunctionDeclaration",
  "body": {
    "type": "BlockStatement",
    "body": [
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {"type": "Identifier", "name": "a"},
            "init": {"type": "Literal", "value": 1}
          }
        ]
      },
      {
        "type": "VariableDeclaration",
        "declarations": [
          {
            "type": "VariableDeclarator",
            "id": {"type": "Identifier", "name": "b"},
            "init": {"type": "Literal", "value": 2}
          }
        ]
      },
      {
        "type": "ReturnStatement",
        "argument": {
          "type": "BinaryExpression",
          "left": {"type": "Identifier", "name": "a"},
          "right": {"type": "Literal", "value": 3}
        }
      }
    ]
  }
}

源码实现

class ScopeManager {
  constructor() {
    this.currentScope = new Scope();
    this.scopes = new Map(); // 存储AST节点与作用域的映射
  }

  // 变量声明处理
  addVariable(node) {
    if (node.type === 'VariableDeclarator') {
      this.currentScope.variables.set(node.id.name, {
        declared: true,
        referenced: false
      });
    }
  }

  // 变量引用处理
  referenceVariable(name) {
    let scope = this.currentScope;
    while (scope) {
      if (scope.variables.has(name)) {
        const variable = scope.variables.get(name);
        variable.referenced = true;
        return;
      }
      scope = scope.parent;
    }
  }
}

// AST遍历逻辑
function detectUnusedVars(ast) {
  const scopeManager = new ScopeManager();
  const unusedVars = new Set();

  // 深度优先遍历AST
  function traverse(node) {
    switch (node.type) {
      case 'FunctionDeclaration':
        enterNewScope(node);
        break;
      case 'VariableDeclarator':
        scopeManager.addVariable(node);
        break;
      case 'Identifier':
        if (!isDeclarationContext(node)) {
          scopeManager.referenceVariable(node.name);
        }
        break;
    }
    
    // 递归遍历子节点
    if (node.body) traverse(node.body);
  }

  // 作用域退出时检测未使用变量
  function leaveScope() {
    for (const [name, variable] of scopeManager.currentScope.variables) {
      if (!variable.referenced) {
        unusedVars.add(name);
      }
    }
    scopeManager.exitScope();
  }

  traverse(ast);
  return Array.from(unusedVars);
}

过程分析

  1. 作用域管理: • 遇到函数声明时创建新作用域 • 记录变量 ab 的声明位置

    Scope1: { variables: { a: { referenced: false }, b: { referenced: false } } }
    
  2. 引用追踪: • 在 return a + 3 中检测到 a 被引用

    Scope1.variables.get('a').referenced = true
    
  3. 结果验证: • 作用域退出时检查变量引用状态

    a.referenced = true  // 保留
    b.referenced = false // 标记为未使用
    

最终输出

Unused variables: [b]

总结

ESLint 的检测具有以下特点: • 实时反馈:在开发阶段即时提示 • 语义感知:能识别不同作用域的变量 • 配置灵活:可通过参数控制检测策略


Tree Shaking

作用

Tree Shaking 是一种在构建阶段优化代码的技术,通过分析代码的依赖关系,移除未使用的代码,从而减小最终打包文件的体积,提高性能。

实现原理

  1. 解析代码为 AST:构建工具(如 Webpack 或 Rollup)使用解析器将代码解析为 AST。
  2. 分析模块依赖关系:通过分析 AST 中的 importexport 语句,构建模块的依赖关系图。
  3. 标记已使用的导出:从入口模块开始,递归标记所有被引用的导出及其依赖。
  4. 移除未使用的代码:再次遍历 AST,移除所有未标记为已使用的节点。

示例代码

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

// main.js
import { add } from './moduleA.js';
let firstNum = 3, secondNum = 4;
add(firstNum, secondNum);

源码实现

class Module {
    constructor(name, dependencies, exports) {
        this.name = name;
        this.dependencies = dependencies;
        this.exports = exports;
        this.code = '';
    }
}

function buildDependencyGraph(modules) {
    const graph = new Map();
    for (const module of modules) {
        graph.set(module.name, {
            module,
            dependencies: module.dependencies,
            exports: module.exports.map(exp => new Export(exp, module.name))
        });
    }
    return graph;
}

function treeShake(entryModule, modules) {
    const graph = buildDependencyGraph(modules);
    const usedExports = new Set();
    const usedModules = new Set();

    function markUsedExports(moduleName) {
        if (usedModules.has(moduleName)) return;
        usedModules.add(moduleName);

        const node = graph.get(moduleName);
        if (!node) return;

        for (const dep of node.dependencies) {
            markUsedExports(dep);
        }

        for (const exp of node.exports) {
            usedExports.add(`${moduleName}:${exp.name}`);
        }
    }

    markUsedExports(entryModule);

    const result = modules.filter(module => usedModules.has(module.name)).map(module => {
        const usedExportsForModule = module.exports.filter(exp =>
            usedExports.has(`${module.name}:${exp}`)
        );
        return new Module(module.name, module.dependencies, usedExportsForModule);
    });

    return result;
}

过程分析

初始模块数据

const modules = [
    new Module("moduleA.js", [], ["add", "multiple"]),
    new Module("main.js", ["moduleA.js"], [])
];
  1. 构建依赖图

    Map {
      'moduleA.js' => {
        exports: [Export('add'), Export('multiple')],
        dependencies: []
      },
      'main.js' => {
        exports: [],
        dependencies: ['moduleA.js']
      }
    }
    
  2. 标记过程: • 入口模块 main.js 触发对 moduleA.js 的依赖分析 • add 函数被标记为已使用

    usedExports: Set { 'moduleA.js:add' }
    
  3. 结果优化

    [
      {
        name: "moduleA.js",
        exports: ["add"] // multiple 被移除
      },
      {
        name: "main.js",
        exports: []
      }
    ]
    

性能优化的角度

ESLint no-unused-vars

开发阶段优化:即时反馈未使用变量,平均每千行代码可减少3-5KB冗余 • 质量保障:在CI/CD流程中阻断问题代码合并 • 典型案例:未使用的React组件声明可被及时清理

Tree Shaking

生产环境优化:典型项目可减少20-30%的打包体积 • 第三方库优化:Lodash的按需引用可减少70%体积 • 典型案例:Angular应用通过Tree Shaking平均减少40%代码量


是否需要同时使用

维度ESLintTree Shaking
作用阶段开发/编译时构建时
检测粒度单文件级跨模块级
优化目标代码质量包体积优化
典型场景未使用的函数参数未引用的模块导出
处理方式开发者手动清理工具自动移除

必要组合方案

  1. 开发阶段使用ESLint保证代码清洁度
  2. 构建阶段通过Tree Shaking实现自动优化
  3. 结合webpack-bundle-analyzer进行包体积监控

总结

ESLint 的 no-unused-vars 和 Tree Shaking 形成了完整的代码优化链条: • ESLint 如同代码质检员,在源头把控代码质量 • Tree Shaking 如同工业压缩机,在打包环节实现自动化优化

两者的协同工作可在典型React项目中实现:开发阶段减少15%-20%冗余代码,构建阶段再削减30%-40%无用代码,最终使生产环境代码体积减少50%以上。这种组合策略已成为现代Web应用的性能优化标配。