From c4fc256f1e337d93f72962e320ed37c06b83a49f Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 23 Dec 2020 02:55:09 +0100 Subject: [PATCH 1/3] perf(gatsby): cache babel config items --- .../gatsby/src/utils/babel-loader-helpers.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/gatsby/src/utils/babel-loader-helpers.js b/packages/gatsby/src/utils/babel-loader-helpers.js index e9eff624a4114..28ae8197e6c4f 100644 --- a/packages/gatsby/src/utils/babel-loader-helpers.js +++ b/packages/gatsby/src/utils/babel-loader-helpers.js @@ -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( @@ -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 = ( From 872d4c4b3dc340ea638108cd377f2818dc3271eb Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 23 Dec 2020 03:59:56 +0100 Subject: [PATCH 2/3] perf(gatsby): cache babel partial config and babel custom options --- packages/gatsby/src/utils/babel-loader.js | 39 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/gatsby/src/utils/babel-loader.js b/packages/gatsby/src/utils/babel-loader.js index 6694210c8c520..a9911d91a888c 100644 --- a/packages/gatsby/src/utils/babel-loader.js +++ b/packages/gatsby/src/utils/babel-loader.js @@ -23,8 +23,12 @@ 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() + module.exports = babelLoader.custom(babel => { - const toReturn = { + return { // Passed the loader options. customOptions({ stage = `test`, @@ -32,7 +36,11 @@ module.exports = babelLoader.custom(babel => { rootDir = process.cwd(), ...options }) { - return { + if (customOptionsCache.has(stage)) { + return customOptionsCache.get(stage) + } + + const toReturn = { custom: { stage, reactRuntime, @@ -49,11 +57,27 @@ 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}` + }) + } + let { options } = partialConfig + if (configCache.has(configCacheKey)) { + return { ...options, ...configCache.get(configCacheKey) } + } + const [ reduxPresets, reduxPlugins, @@ -101,9 +125,16 @@ 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 }) From f0bfcd74f553c4c94a9d8c7f468eea231daafb8d Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Sat, 3 Apr 2021 14:13:43 +0200 Subject: [PATCH 3/3] invalidate babel config cache when babelrc changes --- packages/gatsby/src/utils/babel-loader.js | 32 +++++++++++++++++++++ packages/gatsby/src/utils/webpack.config.js | 2 ++ 2 files changed, 34 insertions(+) diff --git a/packages/gatsby/src/utils/babel-loader.js b/packages/gatsby/src/utils/babel-loader.js index a9911d91a888c..acbab0ea76df1 100644 --- a/packages/gatsby/src/utils/babel-loader.js +++ b/packages/gatsby/src/utils/babel-loader.js @@ -26,6 +26,7 @@ const { getBrowsersList } = require(`./browserslist`) const customOptionsCache = new Map() const configCache = new Map() +const babelrcFileToCacheKey = new Map() module.exports = babelLoader.custom(babel => { return { @@ -71,6 +72,18 @@ module.exports = babelLoader.custom(babel => { 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 @@ -138,3 +151,22 @@ module.exports = babelLoader.custom(babel => { }, } }) + +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) + } + }) + } +} diff --git a/packages/gatsby/src/utils/webpack.config.js b/packages/gatsby/src/utils/webpack.config.js index c2d91efa28a9b..233ee7e789d17 100644 --- a/packages/gatsby/src/utils/webpack.config.js +++ b/packages/gatsby/src/utils/webpack.config.js @@ -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`] @@ -211,6 +212,7 @@ module.exports = async ( }), plugins.virtualModules(), + new BabelConfigItemsCacheInvalidatorPlugin(), ] switch (stage) {