Skip to content

Commit ecd823f

Browse files
authored
perf(gatsby): cache babel config items (#28738)
* perf(gatsby): cache babel config items * perf(gatsby): cache babel partial config and babel custom options * invalidate babel config cache when babelrc changes
1 parent a60e92f commit ecd823f

File tree

3 files changed

+89
-7
lines changed

3 files changed

+89
-7
lines changed

packages/gatsby/src/utils/babel-loader-helpers.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,24 @@ const getCustomOptions = stage => {
2121
return pluginBabelConfig.stages[stage].options
2222
}
2323

24-
const prepareOptions = (babel, options = {}, resolve = require.resolve) => {
25-
const pluginBabelConfig = loadCachedConfig()
24+
/**
25+
* https://babeljs.io/docs/en/babel-core#createconfigitem
26+
* If this function is called multiple times for a given plugin,
27+
* Babel will call the plugin's function itself multiple times.
28+
* If you have a clear set of expected plugins and presets to inject,
29+
* pre-constructing the config items would be recommended.
30+
*/
31+
const configItemsMemoCache = new Map()
2632

33+
const prepareOptions = (babel, options = {}, resolve = require.resolve) => {
2734
const { stage, reactRuntime } = options
2835

36+
if (configItemsMemoCache.has(stage)) {
37+
return configItemsMemoCache.get(stage)
38+
}
39+
40+
const pluginBabelConfig = loadCachedConfig()
41+
2942
// Required plugins/presets
3043
const requiredPlugins = [
3144
babel.createConfigItem(
@@ -95,13 +108,17 @@ const prepareOptions = (babel, options = {}, resolve = require.resolve) => {
95108
)
96109
})
97110

98-
return [
111+
const toReturn = [
99112
reduxPresets,
100113
reduxPlugins,
101114
requiredPresets,
102115
requiredPlugins,
103116
fallbackPresets,
104117
]
118+
119+
configItemsMemoCache.set(stage, toReturn)
120+
121+
return toReturn
105122
}
106123

107124
const addRequiredPresetOptions = (

packages/gatsby/src/utils/babel-loader.js

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,25 @@ const { getBrowsersList } = require(`./browserslist`)
2323
*
2424
* You can find documentation for the custom loader here: https://babeljs.io/docs/en/next/babel-core.html#loadpartialconfig
2525
*/
26+
27+
const customOptionsCache = new Map()
28+
const configCache = new Map()
29+
const babelrcFileToCacheKey = new Map()
30+
2631
module.exports = babelLoader.custom(babel => {
27-
const toReturn = {
32+
return {
2833
// Passed the loader options.
2934
customOptions({
3035
stage = `test`,
3136
reactRuntime = `classic`,
3237
rootDir = process.cwd(),
3338
...options
3439
}) {
35-
return {
40+
if (customOptionsCache.has(stage)) {
41+
return customOptionsCache.get(stage)
42+
}
43+
44+
const toReturn = {
3645
custom: {
3746
stage,
3847
reactRuntime,
@@ -49,11 +58,39 @@ module.exports = babelLoader.custom(babel => {
4958
...options,
5059
},
5160
}
61+
62+
customOptionsCache.set(stage, toReturn)
63+
64+
return toReturn
5265
},
5366

5467
// Passed Babel's 'PartialConfig' object.
5568
config(partialConfig, { customOptions }) {
69+
let configCacheKey = customOptions.stage
70+
if (partialConfig.hasFilesystemConfig()) {
71+
// partialConfig.files is a Set that accumulates used config files (absolute paths)
72+
partialConfig.files.forEach(configFilePath => {
73+
configCacheKey += `_${configFilePath}`
74+
})
75+
76+
// after generating configCacheKey add link between babelrc files and cache keys that rely on it
77+
// so we can invalidate memoized configs when used babelrc file changes
78+
partialConfig.files.forEach(configFilePath => {
79+
let cacheKeysToInvalidate = babelrcFileToCacheKey.get(configFilePath)
80+
if (!cacheKeysToInvalidate) {
81+
cacheKeysToInvalidate = new Set()
82+
babelrcFileToCacheKey.set(configFilePath, cacheKeysToInvalidate)
83+
}
84+
85+
cacheKeysToInvalidate.add(configCacheKey)
86+
})
87+
}
88+
5689
let { options } = partialConfig
90+
if (configCache.has(configCacheKey)) {
91+
return { ...options, ...configCache.get(configCacheKey) }
92+
}
93+
5794
const [
5895
reduxPresets,
5996
reduxPlugins,
@@ -101,9 +138,35 @@ module.exports = babelLoader.custom(babel => {
101138
})
102139
})
103140

141+
// cache just plugins and presets, because config also includes things like
142+
// filenames - this is mostly to not call `mergeConfigItemOptions` for each file
143+
// as that function call `babel.createConfigItem` and is quite expensive but also
144+
// skips quite a few nested loops on top of that
145+
configCache.set(configCacheKey, {
146+
plugins: options.plugins,
147+
presets: options.presets,
148+
})
149+
104150
return options
105151
},
106152
}
107-
108-
return toReturn
109153
})
154+
155+
module.exports.BabelConfigItemsCacheInvalidatorPlugin = class BabelConfigItemsCacheInvalidatorPlugin {
156+
constructor() {
157+
this.name = `BabelConfigItemsCacheInvalidatorPlugin`
158+
}
159+
160+
apply(compiler) {
161+
compiler.hooks.invalid.tap(this.name, function (file) {
162+
const cacheKeysToInvalidate = babelrcFileToCacheKey.get(file)
163+
164+
if (cacheKeysToInvalidate) {
165+
for (const cacheKey of cacheKeysToInvalidate) {
166+
configCache.delete(cacheKey)
167+
}
168+
babelrcFileToCacheKey.delete(file)
169+
}
170+
})
171+
}
172+
}

packages/gatsby/src/utils/webpack.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { StaticQueryMapper } from "./webpack/static-query-mapper"
2121
import { ForceCssHMRForEdgeCases } from "./webpack/force-css-hmr-for-edge-cases"
2222
import { getBrowsersList } from "./browserslist"
2323
import { builtinModules } from "module"
24+
const { BabelConfigItemsCacheInvalidatorPlugin } = require(`./babel-loader`)
2425

2526
const FRAMEWORK_BUNDLES = [`react`, `react-dom`, `scheduler`, `prop-types`]
2627

@@ -211,6 +212,7 @@ module.exports = async (
211212
}),
212213

213214
plugins.virtualModules(),
215+
new BabelConfigItemsCacheInvalidatorPlugin(),
214216
]
215217

216218
switch (stage) {

0 commit comments

Comments
 (0)