-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
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
[DO-NOT-MERGE] RFC: use esbuild to significantly speedup building #4487
Conversation
[V1] Deploy preview failure Built without sensitive environment variables with commit c443ac8 https://app.netlify.com/sites/docusaurus-1/deploys/6060e7e54b1f0b000810ae52 |
Deploy preview for docusaurus-2 failed. Built without sensitive environment variables with commit c443ac8 https://app.netlify.com/sites/docusaurus-2/deploys/6060e7e53b173a0008157cd9 |
⚡️ Lighthouse report for the changes in this PR:
Lighthouse ran on https://deploy-preview-4487--docusaurus-2.netlify.app/ |
This PR improves the build time of server from 2.90m to 1.92m in this PR, that's a 30% reduction. (See the build size log) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, yes, we definitely need it! 👍 I thought about esbuild using too, especially when I looked at benchmark results https://github.com/privatenumber/minification-benchmarks#-results
Very impressive, we have to give it a chance. Manage it via env var as an experimental feature is good idea.
bdw, both links point to same buld |
@armano2 whoops, updated it |
Size Change: +3.81 MB (+663%) 🆘 Total Size: 4.39 MB
ℹ️ View Unchanged
|
Nevermind, the problem is actually code-splitting is broken. Now it's fixed, and we even have some size reductions:
ℹ️ View Unchanged
Turns out I need this change to overcome this issue in esbuild. return {
loader: require.resolve('esbuild-loader'),
- options: {loader: 'tsx', format: 'cjs', target: 'es2015'},
+ options: {
+ loader: 'tsx',
+ format: isServer ? 'cjs' : 'esm',
+ target: 'es2017',
+ },
};
} |
}, | ||
}), | ||
]; | ||
const minimizer = [new ESBuildMinifyPlugin({target: 'es2017'})]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched output to es2017, since v2 doesn't plan to support ie11, and all other major browsers have good support for es2017: https://caniuse.com/?search=es2017
"build": "tsc && yarn build-theme-esm && yarn build-client-esm", | ||
"build-theme-esm": "esbuild src/theme/hooks/useDocs.ts > lib/theme/hooks/useDocs.js", | ||
"build-client-esm": "esbuild src/client/docsClientUtils.ts > lib/client/docsClientUtils.js", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somehow esbuild doesn't like that the client files are not in esm, so I setup the script to compile the client files to esm.
const {transform, features: bubleFeatures} = require('@philpl/buble'); | ||
import {transform, features as bubleFeatures} from '@philpl/buble'; | ||
|
||
// This file is designed to mimic what's written in | ||
// https://github.com/kitten/buble/blob/mini/src/index.js, with custom transforms options, | ||
// so that webpack can consume it correctly. | ||
exports.features = bubleFeatures; | ||
|
||
exports.transform = function customTransform(source, options) { | ||
function customTransform(source, options) { | ||
return transform(source, {...options, transforms: {asyncAwait: false}}); | ||
}; | ||
} | ||
|
||
export {bubleFeatures as features, customTransform as transform}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to turn this into esm to make esbuild happy.
export function getBabelLoader( | ||
isServer: boolean, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
babelOptions?: TransformOptions | string, | ||
): Loader { | ||
return { | ||
loader: require.resolve('babel-loader'), | ||
options: getBabelOptions({isServer, babelOptions}), | ||
loader: require.resolve('esbuild-loader'), | ||
options: { | ||
loader: 'tsx', | ||
format: isServer ? 'cjs' : 'esm', | ||
target: 'es2017', | ||
}, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I plan to make this function be like
if (process.env.ESBUILD) {
return {
loader: require.resolve('esbuild-loader'),
options: {
loader: 'tsx',
format: isServer ? 'cjs' : 'esm',
target: 'es2017',
},
};
return {
loader: require.resolve('babel-loader'),
options: getBabelOptions({isServer, babelOptions}),
}
}
In this way all the existing code that uses the getBabelLoader
can work without modification. This is somehow needed since this function is exposed here. However, this is bad since the function should then really be named as getJSLoader
. Or we can create a new function called getJSLoader
and deprecate the getBabelLoader
function.
Do maintainers have any options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getJSLoader
is also something I had in mind. Would be fine to deprecate getBabelLoader()
looks like the recent bump of esbuild breaks a lot of things... |
Closing this now... looks like patching the webpack config doesn't quite work since there are more moving parts in the SSG part that I don't understand. I may revisit this when I figure out how things work. |
export function getBabelLoader( | ||
isServer: boolean, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
babelOptions?: TransformOptions | string, | ||
): Loader { | ||
return { | ||
loader: require.resolve('babel-loader'), | ||
options: getBabelOptions({isServer, babelOptions}), | ||
loader: require.resolve('esbuild-loader'), | ||
options: { | ||
loader: 'tsx', | ||
format: isServer ? 'cjs' : 'esm', | ||
target: 'es2017', | ||
}, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getJSLoader
is also something I had in mind. Would be fine to deprecate getBabelLoader()
} | ||
|
||
return minimizer; | ||
return [new ESBuildMinifyPlugin({target: 'es2017', css: true})]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder how much different this minification is compared to the old one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Earlier stat because I mess up the build shows that it reduces the output by 1%.
Thanks @SamChou19815 , that's nice to see that integrating esbuild with our Webpack setup is not too complicated and to have some perf numbers. On the short term the plan is to merge the Webpack 5 migration (#4089), add auto-generated sidebars and prepare a beta release. After that, I would be happy to enable an experimental esbuild setup, and maybe we could even make it the default if it is successful and the site does not have any custom babel config (or if it only contains our preset?). Something important to note: I'd like the i18n |
I think we can add a loader that deals with this before the esbuild loader? |
Redoing it in #4532. |
Yeah, we could try, but that would somehow defeat the purpose of using esbuild in the first place as it requires parsing twice the JS code? Not sure how expensive it would be |
I think it's truly unavoidable to run JS code in this case, unless we also write the same transformation code in go as a esbuild go plugin, which I think it's too much to maintain. We can potentially avoid double parsing by detecting whether there is import on |
We don't have to implement this optim soon anyway, it's not a big deal for now, but would just like to keep the opportunity to solve this 18n code-splitting problem someday :) If esbuild integration is successful we could build a go plugin in 6 months or 1 year to optimize things for Docusaurus even more. |
Motivation
This PR tries to use esbuild to compile all the JS code (including all the mdx-react produced js code). Since esbuild is written in Go with performance in mind, it results in a much faster build. Locally, the server build takes 1min for each locale without this change, and only 40s with this change.
However, I do recognize that this PR is not ready for merge right now. I understand that many websites might depend on using specific babel config, and probably esbuild's min target
es2015
is not good enough. The maintainers might also want to put this behind a flag (either indocusaurus.config.js
or a env-var). I only created this PR as a proof of concept how it can be done and to show how fast it can speed up build in CI, and wait on maintainers' opinion on whether and how we want to move forward with this. Therefore, I haven't setup any gating yet and simply replaces babel config and minimize plugin with esbuild stuff.I'm open to keep this behind a flag or only as a plugin that imperatively patch the webpack config, like the one I used for my personal website.
Have you read the Contributing Guidelines on pull requests?
Yes
Test Plan
CI
Related PRs
(If this PR adds or changes functionality, please take some time to update the docs at https://github.com/facebook/docusaurus, and link to your PR here.)