Skip to content

Commit

Permalink
perf(gatsby): cache babel config items (#28738)
Browse files Browse the repository at this point in the history
* perf(gatsby): cache babel config items

* perf(gatsby): cache babel partial config and babel custom options

* invalidate babel config cache when babelrc changes
  • Loading branch information
pieh committed Apr 7, 2021
1 parent a60e92f commit ecd823f
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 7 deletions.
23 changes: 20 additions & 3 deletions packages/gatsby/src/utils/babel-loader-helpers.js
Expand Up @@ -21,11 +21,24 @@ const getCustomOptions = stage => {
return pluginBabelConfig.stages[stage].options
}

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

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

if (configItemsMemoCache.has(stage)) {
return configItemsMemoCache.get(stage)
}

const pluginBabelConfig = loadCachedConfig()

// Required plugins/presets
const requiredPlugins = [
babel.createConfigItem(
Expand Down Expand Up @@ -95,13 +108,17 @@ const prepareOptions = (babel, options = {}, resolve = require.resolve) => {
)
})

return [
const toReturn = [
reduxPresets,
reduxPlugins,
requiredPresets,
requiredPlugins,
fallbackPresets,
]

configItemsMemoCache.set(stage, toReturn)

return toReturn
}

const addRequiredPresetOptions = (
Expand Down
71 changes: 67 additions & 4 deletions packages/gatsby/src/utils/babel-loader.js
Expand Up @@ -23,16 +23,25 @@ const { getBrowsersList } = require(`./browserslist`)
*
* You can find documentation for the custom loader here: https://babeljs.io/docs/en/next/babel-core.html#loadpartialconfig
*/

const customOptionsCache = new Map()
const configCache = new Map()
const babelrcFileToCacheKey = new Map()

module.exports = babelLoader.custom(babel => {
const toReturn = {
return {
// Passed the loader options.
customOptions({
stage = `test`,
reactRuntime = `classic`,
rootDir = process.cwd(),
...options
}) {
return {
if (customOptionsCache.has(stage)) {
return customOptionsCache.get(stage)
}

const toReturn = {
custom: {
stage,
reactRuntime,
Expand All @@ -49,11 +58,39 @@ module.exports = babelLoader.custom(babel => {
...options,
},
}

customOptionsCache.set(stage, toReturn)

return toReturn
},

// Passed Babel's 'PartialConfig' object.
config(partialConfig, { customOptions }) {
let configCacheKey = customOptions.stage
if (partialConfig.hasFilesystemConfig()) {
// partialConfig.files is a Set that accumulates used config files (absolute paths)
partialConfig.files.forEach(configFilePath => {
configCacheKey += `_${configFilePath}`
})

// after generating configCacheKey add link between babelrc files and cache keys that rely on it
// so we can invalidate memoized configs when used babelrc file changes
partialConfig.files.forEach(configFilePath => {
let cacheKeysToInvalidate = babelrcFileToCacheKey.get(configFilePath)
if (!cacheKeysToInvalidate) {
cacheKeysToInvalidate = new Set()
babelrcFileToCacheKey.set(configFilePath, cacheKeysToInvalidate)
}

cacheKeysToInvalidate.add(configCacheKey)
})
}

let { options } = partialConfig
if (configCache.has(configCacheKey)) {
return { ...options, ...configCache.get(configCacheKey) }
}

const [
reduxPresets,
reduxPlugins,
Expand Down Expand Up @@ -101,9 +138,35 @@ module.exports = babelLoader.custom(babel => {
})
})

// cache just plugins and presets, because config also includes things like
// filenames - this is mostly to not call `mergeConfigItemOptions` for each file
// as that function call `babel.createConfigItem` and is quite expensive but also
// skips quite a few nested loops on top of that
configCache.set(configCacheKey, {
plugins: options.plugins,
presets: options.presets,
})

return options
},
}

return toReturn
})

module.exports.BabelConfigItemsCacheInvalidatorPlugin = class BabelConfigItemsCacheInvalidatorPlugin {
constructor() {
this.name = `BabelConfigItemsCacheInvalidatorPlugin`
}

apply(compiler) {
compiler.hooks.invalid.tap(this.name, function (file) {
const cacheKeysToInvalidate = babelrcFileToCacheKey.get(file)

if (cacheKeysToInvalidate) {
for (const cacheKey of cacheKeysToInvalidate) {
configCache.delete(cacheKey)
}
babelrcFileToCacheKey.delete(file)
}
})
}
}
2 changes: 2 additions & 0 deletions packages/gatsby/src/utils/webpack.config.js
Expand Up @@ -21,6 +21,7 @@ import { StaticQueryMapper } from "./webpack/static-query-mapper"
import { ForceCssHMRForEdgeCases } from "./webpack/force-css-hmr-for-edge-cases"
import { getBrowsersList } from "./browserslist"
import { builtinModules } from "module"
const { BabelConfigItemsCacheInvalidatorPlugin } = require(`./babel-loader`)

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

Expand Down Expand Up @@ -211,6 +212,7 @@ module.exports = async (
}),

plugins.virtualModules(),
new BabelConfigItemsCacheInvalidatorPlugin(),
]

switch (stage) {
Expand Down

0 comments on commit ecd823f

Please sign in to comment.