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

perf(gatsby): cache babel config items #28738

Merged
merged 3 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/gatsby/src/utils/babel-loader-helpers.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
pieh marked this conversation as resolved.
Show resolved Hide resolved
},

// 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
Original file line number Diff line number Diff line change
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