Tree Shaking实现方法指南

目录
  • 正文
  • 方式一:JavaScript模拟
  • 方式二:利用AST实现

正文

当使用JavaScript框架或库时,代码中可能会存在许多未使用的函数和变量,这些未使用的代码会使应用程序的文件大小变大,从而影响应用程序的性能。Tree shaking可以解决这个问题,它可以通过检测和删除未使用的代码来减小文件大小并提高应用程序性能。

接下来我们将通过两种方式实现Tree shaking

方式一:JavaScript模拟

1、首先,你需要使用ES6模块来导出和导入代码。ES6模块可以静态分析,从而使Tree shaking技术成为可能。例如,在一个名为“math.js”的文件中,你可以使用以下代码来导出函数:

export function add(a, b) {
  return a + b;
}
export function subtract(a, b) {
  return a - b;
}
export function multiply(a, b) {
  return a * b;
}

2、在应用程序入口处标记使用的代码: 在应用程序的入口处,你需要标记使用的代码。这可以通过创建一个名为"usedExports"的集合来实现,其中包含你在入口文件中使用的导出。

import { add } from './math.js';
const usedExports = new Set([add]);

在这个示例中,我们创建了一个名为"usedExports"的集合,并将我们在应用程序入口文件中使用的add函数添加到集合中。

3、遍历并检测未使用的代码: 在应用程序的所有模块中遍历导出并检查它们是否被使用。你可以使用JavaScript的反射API来实现这一点。以下是代码示例:

function isUsedExport(exportName) {
  return usedExports.has(eval(exportName));
}
for (const exportName of Object.keys(exports)) {
  if (!isUsedExport(exportName)) {
    delete exports[exportName];
  }
}

在这个示例中,我们定义了一个isUsedExport函数来检查是否使用了给定的导出名称。然后,我们遍历应用程序中的所有导出,并将每个导出的名称作为参数传递给isUsedExport函数。如果导出没有被使用,则从exports对象中删除该导出。

4、最后,我们需要在控制台中调用一些函数以确保它们仍然可以正常工作。由于我们只在"usedExports"集合中添加了add函数,因此subtract()和multiply()函数已经被删除了。

方式二:利用AST实现

假设我们有以下的 source 代码:

import { sum } from './utils';
export function add(a, b) {
  return sum(a, b);
}
export const PI = 3.14;

我们首先需要使用 @babel/parser 将源代码解析成 AST:

const parser = require("@babel/parser");
const fs = require("fs");
const sourceCode = fs.readFileSync("source.js", "utf8");
const ast = parser.parse(sourceCode, {
  sourceType: "module",
});

接着,我们需要遍历 AST 并找到所有被使用的导出变量和函数:

// 创建一个 Set 来保存被使用的导出
const usedExports = new Set();
// 标记被使用的导出
function markUsedExports(node) {
  if (node.type === "Identifier") {
    usedExports.add(node.name);
  } else if (node.type === "ExportSpecifier") {
    usedExports.add(node.exported.name);
  }
}
// 遍历 AST 树并标记被使用的导出
function traverse(node) {
  if (node.type === "CallExpression") {
    markUsedExports(node.callee);
    node.arguments.forEach(markUsedExports);
  } else if (node.type === "MemberExpression") {
    markUsedExports(node.property);
    markUsedExports(node.object);
  } else if (node.type === "Identifier") {
    usedExports.add(node.name);
  } else if (node.type === "ExportNamedDeclaration") {
    if (node.declaration) {
      if (node.declaration.type === "FunctionDeclaration") {
        usedExports.add(node.declaration.id.name);
      } else if (node.declaration.type === "VariableDeclaration") {
        node.declaration.declarations.forEach((decl) => {
          usedExports.add(decl.id.name);
        });
      }
    } else {
      node.specifiers.forEach((specifier) => {
        usedExports.add(specifier.exported.name);
      });
    }
  } else if (node.type === "ImportDeclaration") {
    node.specifiers.forEach((specifier) => {
      usedExports.add(specifier.local.name);
    });
  } else {
    for (const key of Object.keys(node)) {
      // 遍历对象的属性,如果属性的值也是对象,则递归调用 traverse 函数
      if (key !== "loc" && node[key] && typeof node[key] === "object") {
        traverse(node[key]);
      }
    }
  }
}
// 遍历整个 AST 树
traverse(ast);

在这里,我们创建了一个 Set 来保存被使用的导出,然后遍历 AST 树并标记被使用的导出。具体来说,我们会:

  • 标记被调用的函数;
  • 标记被访问的对象属性;
  • 标记被直接引用的变量和函数;
  • 标记被导出的变量和函数;
  • 标记被导入的变量和函数。

我们通过遍历 AST 树并调用 markUsedExports 函数来标记被使用的导出,最终将这些导出保存在 usedExports Set 中。

接下来,我们需要遍历 AST 并删除未被使用的代码:

// 移除未使用的代码
function removeUnusedCode(node) {
  // 处理函数声明
  if (node.type === "FunctionDeclaration") {
    if (!usedExports.has(node.id.name)) { // 如果该函数未被使用
      node.body.body = []; // 将该函数体清空
    }
  }
  // 处理变量声明
  else if (node.type === "VariableDeclaration") {
    node.declarations = node.declarations.filter((decl) => {
      return usedExports.has(decl.id.name); // 过滤出被使用的声明
    });
    if (node.declarations.length === 0) { // 如果没有被使用的声明
      node.type = "EmptyStatement"; // 将该声明置为 EmptyStatement
    }
  }
  // 处理导出声明
  else if (node.type === "ExportNamedDeclaration") {
    if (node.declaration) {
      // 处理函数导出声明
      if (node.declaration.type === "FunctionDeclaration") {
        if (!usedExports.has(node.declaration.id.name)) { // 如果该函数未被使用
          node.declaration.body.body = []; // 将该函数体清空
        }
      }
      // 处理变量导出声明
      else if (node.declaration.type === "VariableDeclaration") {
        node.declaration.declarations = node.declarations.filter((decl) =>
        return usedExports.has(decl.id.name); // 过滤出被使用的声明
      });
      if (node.declaration.declarations.length === 0) { // 如果没有被使用的声明
        node.type = "EmptyStatement"; // 将该声明置为 EmptyStatement
      }
    } else {
      // 处理导出的具体内容
      node.specifiers = node.specifiers.filter((specifier) => {
        return usedExports.has(specifier.exported.name); // 过滤出被使用的内容
      });
      if (node.specifiers.length === 0) { // 如果没有被使用的内容
        node.type = "EmptyStatement"; // 将该声明置为 EmptyStatement
      }
    }
  }
  // 处理导入声明
  else if (node.type === "ImportDeclaration") {
    node.specifiers = node.specifiers.filter((specifier) => {
      return usedExports.has(specifier.local.name); // 过滤出被使用的声明
    });
    if (node.specifiers.length === 0) { // 如果没有被使用的声明
      node.type = "EmptyStatement"; // 将该声明置为 EmptyStatement
    }
  }
  // 处理表达式语句
  else if (node.type === "ExpressionStatement") {
    if (node.expression.type === "AssignmentExpression") {
      if (!usedExports.has(node.expression.left.name)) { // 如果该表达式未被使用
        node.type = "EmptyStatement"; // 将该语句置为 EmptyStatement
      }
    }
  }
  // 处理其他情况
  else {
    for (const key of Object.keys(node)) {
      if (key !== "loc" && node[key] && typeof node[key] === "object") {
        removeUnusedCode(node[key]); // 递归处理子节点
      }
    }
  }
}
removeUnusedCode(ast); // 执行移除未使用代码的

在这里,我们遍历 AST 并删除所有未被使用的代码。具体地,我们会:

  • 删除未被使用的函数和变量的函数体和声明语句;
  • 删除未被使用的导出和导入声明;
  • 删除未被使用的赋值语句。

最后,我们将修改后的 AST 重新转换回 JavaScript 代码:

const { transformFromAstSync } = require("@babel/core");
const { code } = transformFromAstSync(ast, null, {
  presets: ["@babel/preset-env"],
});
console.log(code);

这里我们使用了 @babel/core 将 AST 转换回 JavaScript 代码。由于我们使用了 @babel/preset-env,它会自动将我们的代码转换成 ES5 语法,以便于在各种浏览器上运行。

这只是一个简单的例子,实际上还有很多细节需要处理,比如处理 ES modules、CommonJS 模块和 UMD 模块等。不过,这个例子可以帮助我们理解 Tree Shaking 的工作原理,以及如何手动实现它。

以上就是Tree Shaking实现方法指南的详细内容,更多关于Tree Shaking实现的资料请关注我们其它相关文章!

(0)

相关推荐

  • JS 加载性能Tree Shaking优化详解

    目录 正文 什么是 Tree Shaking 寻找 Tree Shaking 的机会 防止 Babel 将 ES6 模块转换为 CommonJS 模块 留意 side effects 只导入你需要的 更复杂的情况 总结 正文 随着 web 应用复杂性增加,JS 代码文件的大小也在不断的攀升,截住 2021年9月,在 httparchive 上有统计显示——在移动设备上 JS 传输大小大约为 447 KB,桌面端 JS 传输大小大约为 495 KB,注意这仅仅是在网络中传输的 JS 文件大小,JS

  • tree shaking对打包体积优化及作用

    目录 背景 有啥用? 实践 前置准备 打包 sideEffects 副作用 sideEffects的使用 优化体积 背景 大家平时在查 webpack构建体积优化 ,可能都会查到 tree-shaking 这个东西,很多人看到这个东西,就会把它背下来,用来应付以后面试官可能会问到的情况. 但是,又有多少人去真的了解一下 tree-shaking 呢?自己去实践一下看 tree-shaking 到底起了哪些作用?对于我们的打包体积的优化又有多少呢? 有啥用? Tree Shaking中文含义是摇树

  • Tree-Shaking 机制快速掌握

    目录 写在前面 直奔主题 最后 写在前面 最近在读霍老师的<Vue.js设计与实现>,感觉收获很多,由于霍老师是官方Vue维护成员,会从很通俗易懂的角度去讲Vue的实现细节.而不是按照源码死讲解,很不错,推荐给大伙! 直奔主题 Tree-Shaking 的本质其实就是消除无用代码也就是dead code,减小打包后文件,不太清楚dead code概念的不用担心,下面会讲到.Tree-Shaking是打包构建工具常用的优化手段.在我们日常的开发最常使用的,可能就是ESM的使用,会触发默认的Tre

  • tree shaking功能及使用原理详细解析

    目录 前言 准备工作 生产环境配置tree-shaking 开发环境观察tree-shaking tree-shaking的步骤 sideEffects副作用 commonjs能不能tree-shaking 写在最后 前言 前端在做性能优化的时候,其中一种有效的方式就是减少包体积的大小.而减少包体积大小的其中一种有效的方式就是 tree-shaking,翻译成中文就是摇树.这个词非常形象,当果树结果后,如果用力摇树,那些成熟了但是还挂在树上的果子就会掉下来,减轻树的负担,因为果子已经成熟了,没有

  • 浅谈Webpack4 Tree Shaking 终极优化指南

    几个月前,我的任务是将我们组的 Vue.js 项目构建配置升级到 Webpack 4.我们的主要目标之一是利用 tree-shaking 的优势,即 Webpack 去掉了实际上并没有使用的代码来减少包的大小.现在,tree-shaking 的好处将根据你的代码库而有所不同.由于我们的几个架构决策,我们从公司内部的其他库中提取了大量代码,而我们只使用了其中的一小部分. 我写这篇文章是因为恰当地优化 Webpack 并不简单.一开始我以为这是一种简单的魔法,但后来我花了一个月的时间在网上搜索我遇到

  • Tree Shaking实现方法指南

    目录 正文 方式一:JavaScript模拟 方式二:利用AST实现 正文 当使用JavaScript框架或库时,代码中可能会存在许多未使用的函数和变量,这些未使用的代码会使应用程序的文件大小变大,从而影响应用程序的性能.Tree shaking可以解决这个问题,它可以通过检测和删除未使用的代码来减小文件大小并提高应用程序性能. 接下来我们将通过两种方式实现Tree shaking 方式一:JavaScript模拟 1.首先,你需要使用ES6模块来导出和导入代码.ES6模块可以静态分析,从而使T

  • webpack的tree shaking的实现方法

    webpack的tree shaking util.js export const a = () => { console.log("a123456方法"); }; export const b = () => { console.log("b123456方法"); }; main.js import {a} from './utils'; a(); sideEffects 一般而言,上述代码,在 webpack 进行 tree shaking 能够不打

  • webpack4 CSS Tree Shaking的使用

    本次课程的代码目录(如下图所示): 什么是tree-shaking webpack 2 的到来带来的最棒的新特性之一就是tree-shaking .tree-shaking源自于rollup.js,先如今,webpack 2也有类似的做法. webpack 里的tree-shaking的到来不得不归功于es6规范的模块.为什么这么说,如今的前端模块规范很多,比较出流行的比如commonJS , AMD , es6 ,我简单的说一下commonJS和es6模块的区别. 1. CSS 也有 Tree

  • Numpy np.array()函数使用方法指南

    目录 1.Numpy ndarray对象 2.创建numpy数组 总结 1.Numpy ndarray对象 numpy ndarray对象是一个n维数组对象,ndarray只能存储一系列相同元素. #一维数组 [1,2,3,4] #shape(4,) #二维数组 [[1,2,3,4]] #shape(1,4) [[1,2,3,4], [5,6,7,8]] #shape(2,4) #三维数组 [ [[1,2,3],[4,5,6]], [[7,8,9],[10,11,12]] ] #shape(2,

  • MySQL 实现双向复制的方法指南

    简介 我知道有很多文章和指南介绍在互联网上实现主-从复制.在主-从复制中,主机影响从机.但从数据库中的任何更改不会影响主数据库,这篇文章将帮助你实现双向复制.(即,无论是主机还是从机的更改都将影响这两个服务器). 背景 你能参考Aadhar Joshi的这篇文章实现主从复制,或者您可以按照以下简单的步骤: 参考一下: 在机器A配置主机(192.168.1.30) 在机器B配置从机(192.168.1.29) 我们可以使用下面的步骤来实现这一点 步骤1:机器A设置主机 在主机中打开配置文件 , 默

  • uniapp开发打包多端应用完整方法指南

    一.uni-app项目介绍 用uni-app开发多端项目,一套代码可同时打包出各端小程序.h5和app,uni-app支持通过 HBuilderX可视化界面 和 vue-cli命令行 两种方式创建项目,下面示例项目采用 HBuilderX可视化界面 的方式创建,cli项目可参考uni文档,大部分流程都是通用的. 项目结构: ├── components 公用组件 ├── libs 公共方法 ├── pages 页面 ├── static 本地静态资源,注意:静态资源只能存放于此 ├── stor

  • Swift编程中数组的使用方法指南

    Swift 数组用于存储相同类型的值的顺序列表.Swift 要严格检查,它不允许错误地在数组中存放了错误的类型. 如果赋值创建数组到一个变量,它总是可变的,这意味着可以通过添加元素来改变它, 删除或更改其项目,但如果分配一个数组常量到则该数组,则数组是不可被改变的, 也就它的大小和内容不能被改变. 创建数组 可以使用下面的初始化程序语法来创建某种类型的空数组: 复制代码 代码如下: var someArray = [SomeType]() 下面是创建一个给定的大小,并用初始值的数组的语法: 复制

随机推荐