关于前端:esbuild-为什么这么快

8次阅读

共计 3924 个字符,预计需要花费 10 分钟才能阅读完成。

前言

esbuild 是新一代的 JavaScript 打包工具。

他的作者是 Figma 的 CTO – Evan Wallace

esbuild 速度快 而著称,耗时只有 webpack 的 2% ~3%。

esbuild 我的项目次要指标是: 开拓一个构建工具性能的新时代,创立一个易用的古代打包器

它的次要性能:

  • Extreme speed without needing a cache
  • ES6 and CommonJS modules
  • Tree shaking of ES6 modules
  • An API for JavaScript and Go
  • TypeScript and JSX syntax
  • Source maps
  • Minification
  • Plugins

当初很多工具都内置了它,比方咱们熟知的:

  • vite,
  • snowpack

借助 esbuild 优异的性能,vite 更是锦上添花,快到飞起。

明天咱们就来摸索一下: 为什么 esbuild 这么快。

明天的次要内容:

  • 几组性能数据比照
  • 为什么 esbuild 这么快
  • esbuild upcoming roadmap
  • esbuild 在 vite 中的使用
  • 为什么生产环境仍需打包?
  • 为何 vite 不必 esbuild 打包?
  • 总结

注释

先看一组比照:

应用 10 份 threeJS 的生产包,比照不同打包工具在默认配置下的打包速度。

webpack5 垫底,耗时 55.25秒。

esbuild 仅耗时 0.37 秒。

差别微小。

还有更多比照:

webpack5 示意很受伤: 我还比不过 webpack 4 ?

为什么 esbuild 这么快?

有以下几个起因。

(为了保障内容的准确性,以下内容翻译自 esbuild 官网。)

1. 它是用 Go 语言编写的,并能够编译为本地代码。

大多数打包器都是用 JavaScript 编写的,然而对于 JIT 编译 的语言来说,命令行应用程序领有最差的性能体现。

每次运行打包器时,JavaScript VM 都会在没有任何优化提醒的状况下看到打包程序的代码。

在 esbuild 忙于解析 JavaScript 时,node 忙于解析打包程序的 JavaScript。

到节点实现解析打包程序代码的工夫时,esbuild 可能曾经退出,您的打包程序甚至还没有开始打包。

另外,Go 是为 并行性 而设计的,而 JavaScript 不是。

Go 在线程之间 共享内存,而 JavaScript 必须在线程之间序列化数据。

Go 和 JavaScript 都有 并行的垃圾收集器 ,然而 Go 的堆在 所有线程 之间共享,而对于 JavaScript, 每个 JavaScript 线程中都有一个 独自的堆

依据测试,这仿佛将 JavaScript worker 线程的并行能力缩小了一半,大略是因为一半 CPU 外围正忙于为另一半收集垃圾。

2. 大量应用了并行操作。

esbuild 中的 算法通过精心设计,能够充分利用 CPU 资源。

大抵分为三个阶段:

  1. 解析
  2. 链接
  3. 代码生成

解析 代码生成 是大部分工作,并且能够 齐全并行化(链接在大多数状况下是固有的串行工作)。

因为所有线程 共享内存,因而当捆绑导入同一 JavaScript 库的不同入口点时,能够轻松地共享工作。

大多数古代计算机具备 多内核,因而并行性是一个微小的胜利。

3. 代码都是本人写的,没有应用第三方依赖。

本人编写所有内容, 而不是应用第三方库,能够带来很多性能劣势。

能够从一开始就牢记性能,能够确保所有内容都应用统一的数据结构来防止低廉的转换,并且能够在必要时进行宽泛的体系结构更改。毛病当然是多了很多工作。

例如,许多捆绑程序都应用官网的 TypeScript 编译器作为解析器。

然而,它是为实现 TypeScript 编译器团队的指标而构建的,它们没有将性能作为头等大事。

4. 内存的高效利用。

现实状况下,依据数据数据的长度,编译器的复杂度为 O(n).

如果要解决大量数据,内存访问速度可能会重大影响性能。

对数据进行的遍历次数越少(将数据转换成数据所需的不同示意模式也就越少),编译器就会越快。

例如,esbuild 仅涉及整个 JavaScript AST 3 次:

  1. 进行词法剖析,解析,作用域设置和申明符号的过程
  2. 绑定符号,最小化语法。比方:将 JSX / TS 转换为 JS, ES Next 转换为 es5。
  3. 最小标识符,最小空格,生成代码。

当 AST 数据在 CPU 缓存中依然处于沉闷状态时,会最大化 AST 数据的重用。

其余打包器在独自的过程中执行这些步骤,而不是将它们交错在一起。

它们也能够在数据表示之间进行转换,将多个库组织在一起(例如: 字符串→TS→JS→字符串,而后字符串→JS→旧的 JS→字符串,而后字符串→JS→minified JS→字符串)。

这样会占用更多内存,并且会减慢速度。

Go 的另一个益处是它能够将内容紧凑地存储在内存中,从而使它能够应用更少的内存并在 CPU 缓存中包容更多内容。

所有对象字段的类型和字段都严密地包装在一起,例如几个布尔标记每个仅占用一个字节。

Go 还具备值语义,能够将一个对象间接嵌入到另一个对象中,因而它是 ’ 收费的 ’,无需另外调配。

JavaScript 不具备这些性能,还具备其余毛病,例如 JIT 开销(例如暗藏的类插槽)和低效的示意模式(例如,非整数与指针堆调配)。

以上的每一条因素,都能在肯定水平上进步编译速度。

当它们独特工作时,成果比当今通常应用的其余打包器快几个数量级。

以上内容比拟繁琐,对此,也有一些网友做了简要的总结:

  • 它是用 Go 语言编写的,该语言能够编译为本地代码。而且 Go 的执行速度很快。一般来说,JS 的操作是 毫秒级 ,而 Go 则是 纳秒级
  • 解析,生成最终打包文件和生成 source maps 的操作全副齐全并行化
  • 无需低廉的数据转换,只需很少的几步即可实现所有操作
  • 该库 以进步编译速度为编写代码时的第一准则,并尽量避免不必要的内存调配。

仅作参考。

Upcoming roadmap

以下这几个 feature 曾经在进行中了, 而且是第一优先级:

  1. Code splitting (#16, docs)
  2. CSS content type (#20, docs)
  3. Plugin API (#111)

上面这几个 fearure 比拟有后劲, 然而还不确定:

  1. HTML content type (#31)
  2. Lowering to ES5 (#297)
  3. Bundling top-level await (#253)

感兴趣的能够放弃关注。

esbuild 在 vite 中的使用

vite 中大量应用了 esbuild, 这里简略分享两点。

  1. optimizer

import {build, BuildOptions as EsbuildBuildOptions} from 'esbuild'

// ...
const result = await build({entryPoints: Object.keys(flatIdDeps),
    bundle: true,
    format: 'esm',
    external: config.optimizeDeps?.exclude,
    logLevel: 'error',
    splitting: true,
    sourcemap: true,
    outdir: cacheDir,
    treeShaking: 'ignore-annotations',
    metafile: true,
    define,
    plugins: [
      ...plugins,
      esbuildDepPlugin(flatIdDeps, flatIdToExports, config)
    ],
    ...esbuildOptions
  })

  const meta = result.metafile!

  // the paths in `meta.outputs` are relative to `process.cwd()`
  const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)

  for (const id in deps) {const entry = deps[id]
    data.optimized[id] = {file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
      src: entry,
      needsInterop: needsInterop(
        id,
        idToExports[id],
        meta.outputs,
        cacheDirOutputPath
      )
    }
  }

  writeFile(dataPath, JSON.stringify(data, null, 2))
  1. 解决 .ts 文件

为什么生产环境仍需打包?

只管原生 ESM 当初失去了 广泛支持 ,但因为嵌套导入会导致 额定的网络往返,在生产环境中公布未打包的 ESM 依然效率低下(即便应用 HTTP/2)。

为了在生产环境中取得 最佳的加载性能 ,最好还是将代码进行 tree-shaking 懒加载 chunk 宰割 (以取得更好的 缓存)。

要确保 开发 服务器和 产品构建 之间的 最佳输入 行为 达到统一,并不容易。

为解决这个问题,Vite 附带了一套 构建优化 构建命令,开箱即用。

为何 vite 不必 esbuild 打包?

尽管 esbuild 快得惊人,并且曾经是一个在构建库方面比拟杰出的工具,但一些针对构建利用的重要性能依然还在继续开发中 —— 特地是 代码宰割 CSS 解决 方面。

就目前来说,Rollup 在利用打包方面, 更加成熟和灵便。

尽管如此,当将来这些性能稳固后,也不排除应用 esbuild 作为 生产构建器 的可能。

总结

esbuild 为构建提效带来了曙光,而且 esm 的数量也在疾速减少:

心愿 esm 生态尽快欠缺起来, 造福前端。

明天的内容就这么多,心愿对大家有所启发。

满腹经纶,文章若有谬误,欢送斧正,谢谢。

参考链接

  1. https://esbuild.github.io/get…
  2. https://esbuild.github.io/faq/
  3. https://twitter.com/skypackjs…
正文完
 0