Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use esbuild by default for es modules, if detected #625

Merged
merged 15 commits into from Sep 14, 2021

Conversation

charliegroll
Copy link
Member

@charliegroll charliegroll commented Aug 23, 2021

- Summary

It'd be great if we could detect ES Module functions and automagically use esbuild as the bundler, similar to the way Typescript detection works.

- Test plan

TODO (I've added a simple test for POC)

- A picture of a cute animal (not mandatory but encouraged)
cat spinning

Closes #617

@charliegroll charliegroll added the type: feature code contributing to the implementation of a feature and/or user facing functionality label Aug 23, 2021
@github-actions
Copy link
Contributor

github-actions bot commented Aug 23, 2021

⏱ Benchmark results

Comparing with 53d0281

largeDepsEsbuild: 12.5s

⬆️ 2.39% increase vs. 53d0281

^  13.2s           13.3s   13.2s                           13.3s                                          
│   ┌──┐            ┌──┐    ┌──┐                            ┌──┐                           12.9s   12.5s  
│   |  |            |  |    |  |           11.7s            |  |                            ┌──┐    ┌──┐  
│ ──┼──┼───11.1s────┼──┼────┼──┼────────────┌──┐───11.1s────┼──┼───11.6s───────────11.6s────┼──┼────|▒▒|──
│   |  |    ┌──┐    |  |    |  |   10.8s    |  |    ┌──┐    |  |    ┌──┐            ┌──┐    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |  |    |  |   10.2s    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    ┌──┐    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
Legend

largeDepsZisi: 1m 11s

⬆️ 7.46% increase vs. 53d0281

^                                                          1m 14s                          1m 15s         
│ 1m 9.5s         1m 12.5s1m 11.3s                          ┌──┐                            ┌──┐   1m 11s 
│   ┌──┐            ┌──┐    ┌──┐                            |  |  1m 6.9s                   |  |    ┌──┐  
│ ──┼──┼──1m 2.7s───┼──┼────┼──┼───────────1m 3s────────────┼──┼────┌──┐──────────1m 5.4s───┼──┼────|▒▒|──
│   |  |    ┌──┐    |  |    |  |    59s     ┌──┐   58.2s    |  |    |  |   59.9s    ┌──┐    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    ┌──┐    |  |    ┌──┐    |  |    |  |    ┌──┐    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
│   |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |  |    |▒▒|  
└───┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴────┴──┴──>
    T-12    T-11    T-10    T-9     T-8     T-7     T-6     T-5     T-4     T-3     T-2     T-1      T    
Legend


// Would love to just use await fs.promises.readFile, but it's not available in our version of Node.
// eslint-disable-next-line node/no-sync
const srcFileContents = fs.readFileSync(srcFile, { encoding: 'utf8' })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could probably use some try/catch safety with a default to false to safely maintain the status quo

src/runtimes/node/index.js Outdated Show resolved Hide resolved
src/runtimes/node/index.js Outdated Show resolved Hide resolved
@@ -0,0 +1,22 @@
const fs = require('fs')

const { init, parse } = require('es-module-lexer')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a JavaScript parser (acorn) as a dependency, which should be able to tell us whether a file uses ESM or not. If it is, I would try to use it before adding a new dependency.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure sure, I'll take a look 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering about the performance difference there considering:

  • acorn is implemented in JavaScript while es-module-lexer is in WebAssembly
  • acorn does lexing+parsing while es-module-lexer does lexing only
  • acorn parses all possible JavaScript syntax while es-module-lexer only looks for imports/exports

The author of es-module-lexer actually claims the following:

For an example of the performance, Angular 1 (720KiB) is fully parsed in 5ms, in comparison to the fastest JS parser, Acorn which takes over 100ms.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second @ehmicky 's evaluation - we need a parser and that's just not what es-module-lexer provides.

that being said, es-module-lexer does have a very light footprint... if we want, I can look into how to accomplish this with acorn, but we can't replace acorn with this

zip-it-and-ship-it on  cg2/617/DetectEsModules [$] is 📦 v4.20.0 via  v16.7.0 
❯ du -ha node_modules/es-module-lexer
4.0K    node_modules/es-module-lexer/types/lexer.d.ts
4.0K    node_modules/es-module-lexer/types
4.0K    node_modules/es-module-lexer/LICENSE
4.0K    node_modules/es-module-lexer/CHANGELOG.md
 12K    node_modules/es-module-lexer/dist/lexer.cjs
 12K    node_modules/es-module-lexer/dist/lexer.js
 24K    node_modules/es-module-lexer/dist
8.0K    node_modules/es-module-lexer/README.md
4.0K    node_modules/es-module-lexer/package.json
 48K    node_modules/es-module-lexer

@@ -272,16 +272,14 @@ testBundlers('Can use dynamic import() with esbuild', [ESBUILD, ESBUILD_ZISI], a
await zipNode(t, 'dynamic-import', { opts: { config: { '*': { nodeBundler: bundler } } } })
})

testBundlers('Bundling does not crash with dynamic import() with zisi', [DEFAULT], async (bundler, t) => {
await t.throwsAsync(zipNode(t, 'dynamic-import', { opts: { config: { '*': { nodeBundler: bundler } } } }), {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no longer a reachable flow, since we'll detect imports/exports and default to esbuild

@charliegroll charliegroll marked this pull request as ready for review August 25, 2021 18:14
@charliegroll
Copy link
Member Author

charliegroll commented Aug 26, 2021

blocked until #628 is resolved we're ready to roll!

Copy link
Member

@eduardoboucas eduardoboucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! I left a few comments, but nothing major.

There are a couple of things that I would like to see before launching this:

  1. An idea of performance implications of parsing function files. We have a basic benchmarking tool in the CI, but in addition to that I'd like to take a couple of large sites and compare the bundling times with and without the parsing.

  2. I'd like to release this behind a feature flag which, when disabled, should bypass the parsing logic entirely. This will allow us to roll this out gradually to minimise risks and monitor performance implications on real sites. We'd need to add the flag here and then have Netlify Build conditionally sending it based on a LaunchDarkly flag.

src/runtimes/node/detect_es_module.js Outdated Show resolved Hide resolved
src/runtimes/node/detect_es_module.js Outdated Show resolved Hide resolved
src/runtimes/node/detect_es_module.js Outdated Show resolved Hide resolved
src/runtimes/node/index.js Outdated Show resolved Hide resolved
tests/main.js Outdated Show resolved Hide resolved
src/runtimes/node/detect_es_module.js Show resolved Hide resolved
@ehmicky
Copy link
Contributor

ehmicky commented Aug 27, 2021

An idea of performance implications of parsing function files. We have a basic benchmarking tool in the CI, but in addition to that I'd like to take a couple of large sites and compare the bundling times with and without the parsing.

I think it might be helpful to also compare the performance with fileContents.startsWith('export ') || fileContents.includes('\nexport ') (since we actually only need to check for ESM exports, not imports). While this might give some false positives, this might be faster.

Edit: after discussing, we concluded this would lead to too many false positives.

@charliegroll
Copy link
Member Author

I ran some preliminary benchmarks using the existing fixtures and got the following:

# run.sh
#!/usr/bin/env bash
exec 3>>.delta.largeDepsZisi
exec 4>>.delta.largeDepsEsbuild
exec 5>>.delta.largeDepsZisi.withFlag
exec 6>>.delta.largeDepsEsbuild.withFlag

npm ci --prefix benchmarks/fixtures
node benchmarks/zisi.js >&3
node benchmarks/esbuild.js >&4
NETLIFY_EXPERIMENTAL_DEFAULT_ES_MODULES_TO_ES_BUILD=true node benchmarks/zisi.js >&5
NETLIFY_EXPERIMENTAL_DEFAULT_ES_MODULES_TO_ES_BUILD=true node benchmarks/esbuild.js >&6

image

Copy link
Member

@eduardoboucas eduardoboucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks @charliegroll! I've added super minor comments.

README.md Show resolved Hide resolved
src/feature_flags.js Outdated Show resolved Hide resolved
src/runtimes/node/detect_es_module.js Outdated Show resolved Hide resolved
src/runtimes/node/detect_es_module.js Outdated Show resolved Hide resolved
src/runtimes/node/index.js Outdated Show resolved Hide resolved
tests/main.js Outdated Show resolved Hide resolved
src/runtimes/node/index.js Outdated Show resolved Hide resolved
Co-Authored-By: Eduardo Bouças <mail@eduardoboucas.com>
@charliegroll charliegroll changed the title feat: poc for es module detection feat: use esbuild by default for es modules, if detected Sep 14, 2021
@kodiakhq kodiakhq bot merged commit 9b0c6d5 into main Sep 14, 2021
@kodiakhq kodiakhq bot deleted the cg2/617/DetectEsModules branch September 14, 2021 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge type: feature code contributing to the implementation of a feature and/or user facing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Detect functions with ES Modules
3 participants